diff --git a/.changeset/afraid-dingos-own.md b/.changeset/afraid-dingos-own.md new file mode 100644 index 0000000000..9f7010d330 --- /dev/null +++ b/.changeset/afraid-dingos-own.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +pass registered logger to requestLoggingHandler diff --git a/.changeset/angry-walls-mate.md b/.changeset/angry-walls-mate.md new file mode 100644 index 0000000000..5902168b2c --- /dev/null +++ b/.changeset/angry-walls-mate.md @@ -0,0 +1,5 @@ +--- +'@backstage/integration': minor +--- + +Make `ScmIntegration.resolveUrl` mandatory. diff --git a/.changeset/brown-pumpkins-impress.md b/.changeset/brown-pumpkins-impress.md new file mode 100644 index 0000000000..5748a9860b --- /dev/null +++ b/.changeset/brown-pumpkins-impress.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Refactored auth provider factories to accept options along with other internal refactoring of the auth providers. diff --git a/.changeset/chilly-cars-shout.md b/.changeset/chilly-cars-shout.md new file mode 100644 index 0000000000..60871b07a3 --- /dev/null +++ b/.changeset/chilly-cars-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Make the `TemplateCard` conform to what material-ui recommends in their examples. This fixes the extra padding around the buttons. diff --git a/.changeset/cuddly-bags-share.md b/.changeset/cuddly-bags-share.md new file mode 100644 index 0000000000..6aff6a9404 --- /dev/null +++ b/.changeset/cuddly-bags-share.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Hide the kind of the owner if it's the default kind for the `ownedBy` +relationship (group). diff --git a/.changeset/dry-llamas-wave.md b/.changeset/dry-llamas-wave.md new file mode 100644 index 0000000000..fea9b122a6 --- /dev/null +++ b/.changeset/dry-llamas-wave.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-common': patch +--- + +Implement `UrlReader.search` for the other providers (Azure, Bitbucket, GitLab) as well. + +The `UrlReader` subclasses now are implemented in terms of the respective `Integration` class. diff --git a/.changeset/eight-doors-matter.md b/.changeset/eight-doors-matter.md new file mode 100644 index 0000000000..7dbb56b6c3 --- /dev/null +++ b/.changeset/eight-doors-matter.md @@ -0,0 +1,19 @@ +--- +'@backstage/core-api': patch +'@backstage/plugin-catalog': patch +--- + +Minor refactoring of BackstageApp.getSystemIcons to support custom registered +icons. Custom Icons can be added using: + +```tsx +import AlarmIcon from '@material-ui/icons/Alarm'; +import MyPersonIcon from './MyPerson'; + +const app = createApp({ + icons: { + user: MyPersonIcon // override system icon + alert: AlarmIcon, // Custom icon + }, +}); +``` diff --git a/.changeset/five-guests-promise.md b/.changeset/five-guests-promise.md new file mode 100644 index 0000000000..1d4d0ae303 --- /dev/null +++ b/.changeset/five-guests-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Remove the "Move repository" menu entry from the catalog page, as it's just a placeholder. diff --git a/.changeset/fresh-seals-retire.md b/.changeset/fresh-seals-retire.md new file mode 100644 index 0000000000..f58eebef4e --- /dev/null +++ b/.changeset/fresh-seals-retire.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-api': patch +'@backstage/core': patch +--- + +Export `createExternalRouteRef`, as well as give it an `id` for easier debugging, and fix parameter requirements when used with `useRouteRef`. diff --git a/.changeset/itchy-camels-grin.md b/.changeset/itchy-camels-grin.md new file mode 100644 index 0000000000..eac3c6a102 --- /dev/null +++ b/.changeset/itchy-camels-grin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-splunk-on-call': patch +--- + +Added splunk-on-call plugin. diff --git a/.changeset/many-bags-sort.md b/.changeset/many-bags-sort.md new file mode 100644 index 0000000000..dbc8df8b0c --- /dev/null +++ b/.changeset/many-bags-sort.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Support globs in `FileReaderProcessor`. diff --git a/.changeset/mean-grapes-march.md b/.changeset/mean-grapes-march.md new file mode 100644 index 0000000000..6065fe0892 --- /dev/null +++ b/.changeset/mean-grapes-march.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-common': patch +'@backstage/techdocs-common': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Switched to using `'x-access-token'` for authenticating Git over HTTPS towards GitHub. diff --git a/.changeset/new-mangos-tap.md b/.changeset/new-mangos-tap.md new file mode 100644 index 0000000000..1b57510417 --- /dev/null +++ b/.changeset/new-mangos-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Tweak error message in lockfile parsing to include more information. diff --git a/.changeset/pink-coins-sniff.md b/.changeset/pink-coins-sniff.md new file mode 100644 index 0000000000..b9714f116a --- /dev/null +++ b/.changeset/pink-coins-sniff.md @@ -0,0 +1,16 @@ +--- +'@backstage/plugin-api-docs': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-react': patch +--- + +Introduce new cards to `@backstage/plugin-catalog` that can be added to entity pages: + +- `EntityHasSystemsCard` to display systems of a domain. +- `EntityHasComponentsCard` to display components of a system. +- `EntityHasSubcomponentsCard` to display subcomponents of a subcomponent. +- In addition, `EntityHasApisCard` to display APIs of a system is added to `@backstage/plugin-api-docs`. + +`@backstage/plugin-catalog-react` now provides an `EntityTable` to build own cards for entities. +The styling of the tables and new cards was also applied to the existing `EntityConsumedApisCard`, +`EntityConsumingComponentsCard`, `EntityProvidedApisCard`, and `EntityProvidingComponentsCard`. diff --git a/.changeset/rich-apricots-lick.md b/.changeset/rich-apricots-lick.md new file mode 100644 index 0000000000..f9daf861d2 --- /dev/null +++ b/.changeset/rich-apricots-lick.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fixed parsing of OIDC key timestamps when using SQLite. diff --git a/.changeset/short-pots-report.md b/.changeset/short-pots-report.md new file mode 100644 index 0000000000..4323a957f3 --- /dev/null +++ b/.changeset/short-pots-report.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-org': patch +--- + +- Fixes padding in `MembersListCard` +- Fixes email icon size in `GroupProfileCard` +- Uniform sizing across `GroupProfileCard` and `UserProfileCard` diff --git a/.changeset/six-elephants-poke.md b/.changeset/six-elephants-poke.md new file mode 100644 index 0000000000..23e575b67c --- /dev/null +++ b/.changeset/six-elephants-poke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Migrate about card to new composability API, exporting the entity cards as `EntityAboutCard`. diff --git a/.changeset/soft-rings-obey.md b/.changeset/soft-rings-obey.md new file mode 100644 index 0000000000..91308bdc50 --- /dev/null +++ b/.changeset/soft-rings-obey.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fixed the `prepare` step for when using local templates that were added to the catalog using the `file:` target configuration. +No more `EPERM: operation not permitted` error messages. diff --git a/.changeset/spicy-pants-teach.md b/.changeset/spicy-pants-teach.md new file mode 100644 index 0000000000..8e16ae089e --- /dev/null +++ b/.changeset/spicy-pants-teach.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Expose `useRelatedEntities` from `@backstage/plugin-catalog-react` to retrieve +entities references via relations from the API. diff --git a/.changeset/stupid-maps-do.md b/.changeset/stupid-maps-do.md new file mode 100644 index 0000000000..1cbac64f68 --- /dev/null +++ b/.changeset/stupid-maps-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Export Select component diff --git a/.changeset/techdocs-fast-foxes-relate.md b/.changeset/techdocs-fast-foxes-relate.md new file mode 100644 index 0000000000..7377946f86 --- /dev/null +++ b/.changeset/techdocs-fast-foxes-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/techdocs-common': patch +--- + +After TechDocs generate step, insert build timestamp to techdocs_metadata.json diff --git a/.changeset/techdocs-spotty-cooks-wait.md b/.changeset/techdocs-spotty-cooks-wait.md new file mode 100644 index 0000000000..9ec35b9257 --- /dev/null +++ b/.changeset/techdocs-spotty-cooks-wait.md @@ -0,0 +1,6 @@ +--- +'@backstage/techdocs-common': patch +'@backstage/plugin-techdocs-backend': patch +--- + +Add etag of the prepared file tree to techdocs_metadata.json in the storage diff --git a/.changeset/tough-worms-clap.md b/.changeset/tough-worms-clap.md new file mode 100644 index 0000000000..3a18593549 --- /dev/null +++ b/.changeset/tough-worms-clap.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Fix parsing of the path to default to empty string not undefined if git-url-parse throws something we don't expect. Fixes the error `The "path" argument must be of type string.` when preparing. diff --git a/.changeset/violet-maps-occur.md b/.changeset/violet-maps-occur.md new file mode 100644 index 0000000000..112095d257 --- /dev/null +++ b/.changeset/violet-maps-occur.md @@ -0,0 +1,8 @@ +--- +'@backstage/core': patch +--- + +Add support for custom empty state of `Table` components. + +You can now optionally pass `emptyContent` to `Table` that is displayed +if the table has now rows. diff --git a/.changeset/wicked-forks-sleep.md b/.changeset/wicked-forks-sleep.md new file mode 100644 index 0000000000..8f7f034094 --- /dev/null +++ b/.changeset/wicked-forks-sleep.md @@ -0,0 +1,6 @@ +--- +'@backstage/cli': patch +'@backstage/create-app': patch +--- + +Upgrading to lerna@4.0.0. diff --git a/.changeset/wise-books-turn.md b/.changeset/wise-books-turn.md new file mode 100644 index 0000000000..77a42c5304 --- /dev/null +++ b/.changeset/wise-books-turn.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Add Breadcrumbs component diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 48f2f81d9f..c5a0b02529 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -119,6 +119,7 @@ Kumar learnings lerna Lerna +lockfile Luxon magiclink mailto @@ -212,6 +213,7 @@ Sneha Snyk sourcemaps sparklines +Splunk Spotifiers spotify Spotify @@ -220,6 +222,8 @@ squidfunk src stdout stefanalund +subcomponent +subcomponents subkey subtree superfences diff --git a/app-config.yaml b/app-config.yaml index 9df6540a2c..672ba26cd2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -167,7 +167,8 @@ catalog: # - target: ldaps://ds.example.net # bind: # dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net - # secret: { $secret: { env: LDAP_SECRET } } + # secret: + # $env: LDAP_SECRET # users: # dn: ou=people,ou=example,dc=example,dc=net # options: diff --git a/catalog-info.yaml b/catalog-info.yaml index 7e60af5755..0cab0c558a 100644 --- a/catalog-info.yaml +++ b/catalog-info.yaml @@ -4,6 +4,15 @@ metadata: name: backstage description: | Backstage is an open-source developer portal that puts the developer experience first. + links: + - title: Website + url: http://backstage.io + - title: Documentation + url: https://backstage.io/docs + - title: Storybook + url: https://backstage.io/storybook + - title: Discord Chat + url: https://discord.com/invite/EBHEGzX annotations: github.com/project-slug: backstage/backstage backstage.io/techdocs-ref: url:https://github.com/backstage/backstage diff --git a/contrib/chart/backstage/values.yaml b/contrib/chart/backstage/values.yaml index 261f352f93..7926ebbc0a 100644 --- a/contrib/chart/backstage/values.yaml +++ b/contrib/chart/backstage/values.yaml @@ -127,88 +127,67 @@ appConfig: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_GOOGLE_CLIENT_ID + $env: AUTH_GOOGLE_CLIENT_ID clientSecret: - $secret: - env: AUTH_GOOGLE_CLIENT_SECRET + $env: AUTH_GOOGLE_CLIENT_SECRET github: development: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_GITHUB_CLIENT_ID + $env: AUTH_GITHUB_CLIENT_ID clientSecret: - $secret: - env: AUTH_GITHUB_CLIENT_SECRET + $env: AUTH_GITHUB_CLIENT_SECRET enterpriseInstanceUrl: - $secret: - env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL + $env: AUTH_GITHUB_ENTERPRISE_INSTANCE_URL gitlab: development: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_GITLAB_CLIENT_ID + $env: AUTH_GITLAB_CLIENT_ID clientSecret: - $secret: - env: AUTH_GITLAB_CLIENT_SECRET + $env: AUTH_GITLAB_CLIENT_SECRET audience: - $secret: - env: GITLAB_BASE_URL + $env: GITLAB_BASE_URL okta: development: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_OKTA_CLIENT_ID + $env: AUTH_OKTA_CLIENT_ID clientSecret: - $secret: - env: AUTH_OKTA_CLIENT_SECRET + $env: AUTH_OKTA_CLIENT_SECRET audience: - $secret: - env: AUTH_OKTA_AUDIENCE + $env: AUTH_OKTA_AUDIENCE oauth2: development: appOrigin: 'http://localhost:3000/' secure: false clientId: - $secret: - env: AUTH_OAUTH2_CLIENT_ID + $env: AUTH_OAUTH2_CLIENT_ID clientSecret: - $secret: - env: AUTH_OAUTH2_CLIENT_SECRET + $env: AUTH_OAUTH2_CLIENT_SECRET authorizationURL: - $secret: - env: AUTH_OAUTH2_AUTH_URL + $env: AUTH_OAUTH2_AUTH_URL tokenURL: - $secret: - env: AUTH_OAUTH2_TOKEN_URL + $env: AUTH_OAUTH2_TOKEN_URL auth0: development: clientId: - $secret: - env: AUTH_AUTH0_CLIENT_ID + $env: AUTH_AUTH0_CLIENT_ID clientSecret: - $secret: - env: AUTH_AUTH0_CLIENT_SECRET + $env: AUTH_AUTH0_CLIENT_SECRET domain: - $secret: - env: AUTH_AUTH0_DOMAIN + $env: AUTH_AUTH0_DOMAIN microsoft: development: clientId: - $secret: - env: AUTH_MICROSOFT_CLIENT_ID + $env: AUTH_MICROSOFT_CLIENT_ID clientSecret: - $secret: - env: AUTH_MICROSOFT_CLIENT_SECRET + $env: AUTH_MICROSOFT_CLIENT_SECRET tenantId: - $secret: - env: AUTH_MICROSOFT_TENANT_ID + $env: AUTH_MICROSOFT_TENANT_ID auth: google: diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index 1aab1268cd..6658dfc055 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -205,7 +205,8 @@ described below. In addition to these, you may add any number of other fields directly under `metadata`, but be aware that general plugins and tools may not be able to -understand their semantics. +understand their semantics. See [Extending the model](extending-the-model.md) +for more information. ### `name` [required] @@ -214,8 +215,8 @@ entity, and for machines and other components to reference the entity (e.g. in URLs or from other entity specification files). Names must be unique per kind, within a given namespace (if specified), at any -point in time. Names may be reused at a later time, after an entity is deleted -from the registry. +point in time. This uniqueness constraint is case insensitive. Names may be +reused at a later time, after an entity is deleted from the registry. Names are required to follow a certain format. Entities that do not follow those rules will not be accepted for registration in the catalog. The ruleset is @@ -226,19 +227,7 @@ follows. - Must consist of sequences of `[a-z0-9A-Z]` possibly separated by one of `[-_.]` -Example: `visits-tracking-service`, `CircleciBuildsDump_avro_gcs` - -In addition to this, names are passed through a normalization function and then -compared to the same normalized form of other entity names and made sure to not -collide. This rule of uniqueness exists to avoid situations where e.g. both -`my-component` and `MyComponent` are registered side by side, which leads to -confusion and risk. The normalization function is also configurable, but the -default behavior is as follows. - -- Strip out all characters outside of the set `[a-zA-Z0-9]` -- Convert to lowercase - -Example: `CircleciBuildsDs_avro_gcs` -> `circlecibuildsdsavrogcs` +Example: `visits-tracking-service`, `CircleciBuildsDumpV2_avro_gcs` ### `namespace` [optional] @@ -248,7 +237,8 @@ the same format restrictions as `name` above. This field is optional, and currently has no special semantics apart from bounding the name uniqueness constraint if specified. It is reserved for future use and may get broader semantic implication later. For now, it is recommended -to not specify a namespace unless you have specific need to do so. +to not specify a namespace unless you have specific need to do so. This means +the entity belongs to the `"default"` namespace. Namespaces may also be part of the catalog, and are `v1` / `Namespace` entities, i.e. not Backstage specific but the same as in Kubernetes. @@ -278,7 +268,6 @@ most 253 characters in total. The name part must be sequences of `[a-zA-Z0-9]` separated by any of `[-_.]`, at most 63 characters in total. The `backstage.io/` prefix is reserved for use by Backstage core components. -Some keys such as `system` also have predefined semantics. Values are strings that follow the same restrictions as `name` above. diff --git a/docs/glossary.md b/docs/glossary.md index e5a9882909..745cf5f95b 100644 --- a/docs/glossary.md +++ b/docs/glossary.md @@ -9,6 +9,11 @@ The Backstage Glossary lists all the terms, abbreviations, and phrases used in Backstage, together with their explanations. We encourage you to use the terminology below for clarity and consistency when discussing Backstage. +### Authentication Glossary + +This [page](./auth/glossary.md) directs to the terms and phrases related to +authentication and identity section of Backstage. + ### Backstage User Profiles There are three main user profiles for Backstage: the integrator, the diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index a496217875..6106fa9141 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -248,7 +248,7 @@ might be linking to, allowing the app to decide the final target. If the declare an `ExternalRouteRef` similar to this: ```ts -const headerLinkRouteRef = createExternalRouteRef(); +const headerLinkRouteRef = createExternalRouteRef({ id: 'header-link' }); ``` ### Binding External Routes in the App diff --git a/docs/plugins/github-apps.md b/docs/plugins/github-apps.md index d3b0e36cd9..27aec8a31f 100644 --- a/docs/plugins/github-apps.md +++ b/docs/plugins/github-apps.md @@ -27,17 +27,17 @@ to grant the app more permissions if needed. ### Using the CLI (public GitHub only) -You can use the `backstage-cli` to create GitHub App' using a manifest file that -we provide. This gives us a way to automate some of the work required to create -a GitHub app. +You can use the `backstage-cli` to create a GitHub App using a manifest file +that we provide. This gives us a way to automate some of the work required to +create a GitHub app. -You can read more about the `backstage-cli create-github-app` method -[here](../cli/commands.md#create-github-app) +You can read more about the +[`backstage-cli create-github-app` method](../cli/commands.md#create-github-app). -Once you've gone through the CLI command, it should produce a `yaml` file in the +Once you've gone through the CLI command, it should produce a YAML file in the root of the project which you can then use as an `include` in your -`app-config.yaml`. You can go ahead and skip to -[here](#including-in-integrations-config) if you've got to this part. +`app-config.yaml`. You can go ahead and +[skip ahead](#including-in-integrations-config) if you've already got an app. ### GitHub Enterprise @@ -46,9 +46,9 @@ You have to create the GitHub Application manually using these as GitHub Enterprise does not support creation of apps from manifests. Once the application is created you have to generate a private key for the -application it in a `yaml` file. +application and place it in a YAML file. -The yaml file must include the following information. Please note that the +The YAML file must include the following information. Please note that the indentation for the `privateKey` is required. ```yaml @@ -64,7 +64,7 @@ privateKey: | ### Including in Integrations Config -Once the credentials are stored in a yaml file generated by `create-github-app` +Once the credentials are stored in a YAML file generated by `create-github-app`, or manually by following the [GitHub Enterprise](#gitHub-enterprise) instructions, they can be included in the `app-config.yaml` under the `integrations` section. @@ -77,6 +77,6 @@ method of distributing secrets. integrations: github: - host: github.com - apps: + apps: - $include: example-backstage-app-credentials.yaml ``` diff --git a/microsite/data/plugins/splunk-on-call.yaml b/microsite/data/plugins/splunk-on-call.yaml new file mode 100644 index 0000000000..2d6deb0180 --- /dev/null +++ b/microsite/data/plugins/splunk-on-call.yaml @@ -0,0 +1,14 @@ +--- +title: Splunk On-Call +author: Spotify +authorUrl: https://github.com/spotify +category: Monitoring +description: Splunk On-Call offers a simple way to identify incidents and escalation policies. +documentation: https://github.com/backstage/backstage/tree/master/plugins/splunk-on-call +iconUrl: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIiBjbGFzcz0idm8tbGVhZi1ibGFjayI+PHBhdGggZD0iTTE1Ljk5OTcgMzJDMjQuODM2MSAzMiAzMiAyNC44MzY4IDMyIDE2LjAwMDJDMzIgNy4xNjM1NiAyNC44MzYxIDAgMTUuOTk5NyAwQzcuMTYzNCAwIDAgNy4xNjM1NiAwIDE2LjAwMDJDMCAyNC44MzY4IDcuMTYzNCAzMiAxNS45OTk3IDMyWk0yMi41Njc1IDE0LjY5MDRMMjMuNjI1OSAxNi41MTU4TDE4Ljg4NTEgMTguOTE0NUwxOC44MSAxOS43MTQxTDIyLjQwMzMgMTguNTc2MUwyMC41OTcyIDIwLjg0MzJMMjAuNDQ2MiAyMS41NjI4TDE4LjI0MzkgMjIuNTUyNUwxOC4wMTc2IDIzLjIzMjJMMTkuMDA0NSAyMi44MTU3TDE2LjM0MjIgMjUuOTEwNFYyOS42MDA0SDE1LjY1NzdWMjUuOTEwNEwxMi45OTU5IDIyLjgxNTdMMTMuOTgyOCAyMy4yMzIyTDEzLjc1NjYgMjIuNTUyNUwxMS41NTQ1IDIxLjU2MjhMMTEuNDAzNiAyMC44NDMyTDkuNTk2OTIgMTguNTc2MUwxMy4xOTA2IDE5LjcxNDFMMTMuMTE0OSAxOC45MTQ1TDguMzc0NDkgMTYuNTE1OEw5LjQzMzM5IDE0LjY5MDRMNy4yMDAyIDExLjUxODlMMTMuMzcxOCAxNC41MDA3TDEzLjQ5NyAxMy42MDM3TDExLjY5MzYgMTAuNjYyTDExLjk1MiAxMC4xNzc0TDExLjExOTUgNi43MTg2N0wxNC4zNTUzIDEwLjEyNjdMMTQuNTQ5MyA4LjI0NDU0TDEzLjU4MTggNS40MjI1M0wxNC44NzMgNS44NTYyNEwxNiAyLjQwMDM5TDE3LjEyNzMgNS44NTYyNEwxOC40MTg2IDUuNDIyNTNMMTcuNDUxMiA4LjI0NDU0TDE3LjY0NTQgMTAuMTI2N0wyMC44ODA3IDYuNzE4NjdMMjAuMDQ4MiAxMC4xNzc0TDIwLjMwNjggMTAuNjYyTDE4LjUwMzQgMTMuNjAzN0wxOC42Mjg4IDE0LjUwMDdMMjQuODAwMiAxMS41MTg5TDIyLjU2NzUgMTQuNjkwNFoiIGZpbGw9JyMyQzJDMkMnLz48L3N2Zz4K +npmPackageName: '@backstage/plugin-splunk-on-call' +tags: + - monitoring + - errors + - alerting + - splunk diff --git a/package.json b/package.json index ad0ce2cd57..59fe9d8f18 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "concurrently": "^5.2.0", "fs-extra": "^9.0.0", "husky": "^4.2.3", - "lerna": "^3.20.2", + "lerna": "^4.0.0", "lint-staged": "^10.1.0", "prettier": "^2.0.5", "recursive-readdir": "^2.2.2", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 70fceb8a01..cc673c7769 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -39,10 +39,15 @@ import { EntityPage } from './components/catalog/EntityPage'; import Root from './components/Root'; import { providers } from './identityProviders'; import * as plugins from './plugins'; +import AlarmIcon from '@material-ui/icons/Alarm'; const app = createApp({ apis, plugins: Object.values(plugins), + icons: { + // Custom icon example + alert: AlarmIcon, + }, components: { SignInPage: props => { return ( diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index e849667c19..7fb624b9e9 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -16,8 +16,10 @@ import { ApiEntity, + DomainEntity, Entity, GroupEntity, + SystemEntity, UserEntity, } from '@backstage/catalog-model'; import { EmptyState } from '@backstage/core'; @@ -25,11 +27,15 @@ import { ApiDefinitionCard, ConsumedApisCard, ConsumingComponentsCard, + EntityHasApisCard, ProvidedApisCard, ProvidingComponentsCard, } from '@backstage/plugin-api-docs'; import { AboutCard, + EntityHasComponentsCard, + EntityHasSubcomponentsCard, + EntityHasSystemsCard, EntityLinksCard, EntityPageLayout, } from '@backstage/plugin-catalog'; @@ -206,6 +212,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( )} + + + ); @@ -425,6 +434,51 @@ const GroupEntityPage = ({ entity }: { entity: Entity }) => ( ); +const SystemOverviewContent = ({ entity }: { entity: SystemEntity }) => ( + + + + + + + + + + + +); + +const SystemEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + +); + +const DomainOverviewContent = ({ entity }: { entity: DomainEntity }) => ( + + + + + + + + +); + +const DomainEntityPage = ({ entity }: { entity: Entity }) => ( + + } + /> + +); + export const EntityPage = () => { const { entity } = useEntity(); @@ -437,6 +491,10 @@ export const EntityPage = () => { return ; case 'user': return ; + case 'system': + return ; + case 'domain': + return ; default: return ; } diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index c937f8a40c..b52097baf5 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -14,18 +14,22 @@ * limitations under the License. */ -import * as os from 'os'; +import { ConfigReader } from '@backstage/config'; +import { + AzureIntegration, + readAzureIntegrationConfig, +} from '@backstage/integration'; +import { msw } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import path from 'path'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; -import { ConfigReader } from '@backstage/config'; +import * as os from 'os'; +import path from 'path'; +import { NotModifiedError } from '../errors'; import { getVoidLogger } from '../logging'; import { AzureUrlReader } from './AzureUrlReader'; -import { msw } from '@backstage/test-utils'; import { ReadTreeResponseFactory } from './tree'; -import { NotModifiedError } from '../errors'; const logger = getVoidLogger(); @@ -36,6 +40,16 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; describe('AzureUrlReader', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + const worker = setupServer(); msw.setupDefaultHandlers(worker); @@ -143,22 +157,18 @@ describe('AzureUrlReader', () => { }); describe('readTree', () => { - beforeEach(() => { - mockFs({ - [tmpDir]: mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - const repoBuffer = fs.readFileSync( path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), ); const processor = new AzureUrlReader( - { host: 'dev.azure.com' }, + new AzureIntegration( + readAzureIntegrationConfig( + new ConfigReader({ + host: 'dev.azure.com', + }), + ), + ), { treeResponseFactory }, ); @@ -257,4 +267,79 @@ describe('AzureUrlReader', () => { expect(indexMarkdownFile.toString()).toBe('# Test\n'); }); }); + + describe('search', () => { + const repoBuffer = fs.readFileSync( + path.resolve('src', 'reading', '__fixtures__', 'mock-main.zip'), + ); + + const processor = new AzureUrlReader( + new AzureIntegration( + readAzureIntegrationConfig( + new ConfigReader({ + host: 'dev.azure.com', + }), + ), + ), + { treeResponseFactory }, + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/items', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.body(repoBuffer), + ), + ), + rest.get( + // https://docs.microsoft.com/en-us/rest/api/azure/devops/git/commits/get%20commits?view=azure-devops-rest-6.0#on-a-branch + 'https://dev.azure.com/org-name/project-name/_apis/git/repositories/repo-name/commits', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + count: 2, + value: [ + { + commitId: '123abc2', + comment: 'second commit', + }, + { + commitId: '123abc1', + comment: 'first commit', + }, + ], + }), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await processor.search( + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=%2F**%2Findex.*&version=GBmaster', + ); + expect(result.etag).toBe('123abc2'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=%2Fdocs%2Findex.md&version=GBmaster', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + processor.search( + 'https://dev.azure.com/org-name/project-name/_git/repo-name?path=**/index.*&version=GBmaster', + { etag: '123abc2' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); }); diff --git a/packages/backend-common/src/reading/AzureUrlReader.ts b/packages/backend-common/src/reading/AzureUrlReader.ts index bc1b61a041..21b988e5b0 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.ts @@ -15,39 +15,41 @@ */ import { - AzureIntegrationConfig, - readAzureIntegrationConfigs, - getAzureFileFetchUrl, - getAzureDownloadUrl, - getAzureRequestOptions, + AzureIntegration, getAzureCommitsUrl, + getAzureDownloadUrl, + getAzureFileFetchUrl, + getAzureRequestOptions, + ScmIntegrations, } from '@backstage/integration'; import fetch from 'cross-fetch'; +import parseGitUrl from 'git-url-parse'; +import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '../errors'; +import { ReadTreeResponseFactory } from './tree'; +import { stripFirstDirectoryFromPath } from './tree/util'; import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchOptions, SearchResponse, UrlReader, } from './types'; -import { ReadTreeResponseFactory } from './tree'; export class AzureUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { - const configs = readAzureIntegrationConfigs( - config.getOptionalConfigArray('integrations.azure') ?? [], - ); - return configs.map(options => { - const reader = new AzureUrlReader(options, { treeResponseFactory }); - const predicate = (url: URL) => url.host === options.host; + const integrations = ScmIntegrations.fromConfig(config); + return integrations.azure.list().map(integration => { + const reader = new AzureUrlReader(integration, { treeResponseFactory }); + const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); }; constructor( - private readonly options: AzureIntegrationConfig, + private readonly integration: AzureIntegration, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) {} @@ -56,7 +58,10 @@ export class AzureUrlReader implements UrlReader { let response: Response; try { - response = await fetch(builtUrl, getAzureRequestOptions(this.options)); + response = await fetch( + builtUrl, + getAzureRequestOptions(this.integration.config), + ); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } @@ -83,7 +88,7 @@ export class AzureUrlReader implements UrlReader { const commitsAzureResponse = await fetch( getAzureCommitsUrl(url), - getAzureRequestOptions(this.options), + getAzureRequestOptions(this.integration.config), ); if (!commitsAzureResponse.ok) { const message = `Failed to read tree from ${url}, ${commitsAzureResponse.status} ${commitsAzureResponse.statusText}`; @@ -100,7 +105,9 @@ export class AzureUrlReader implements UrlReader { const archiveAzureResponse = await fetch( getAzureDownloadUrl(url), - getAzureRequestOptions(this.options, { Accept: 'application/zip' }), + getAzureRequestOptions(this.integration.config, { + Accept: 'application/zip', + }), ); if (!archiveAzureResponse.ok) { const message = `Failed to read tree from ${url}, ${archiveAzureResponse.status} ${archiveAzureResponse.statusText}`; @@ -117,12 +124,38 @@ export class AzureUrlReader implements UrlReader { }); } - async search(): Promise { - throw new Error('AzureUrlReader does not implement search'); + async search(url: string, options?: SearchOptions): Promise { + const { filepath } = parseGitUrl(url); + const matcher = new Minimatch(filepath); + + // TODO(freben): For now, read the entire repo and filter through that. In + // a future improvement, we could be smart and try to deduce that non-glob + // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used + // to get just that part of the repo. + const treeUrl = new URL(url); + treeUrl.searchParams.delete('path'); + treeUrl.pathname = treeUrl.pathname.replace(/\/+$/, ''); + + const tree = await this.readTree(treeUrl.toString(), { + etag: options?.etag, + filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + }); + const files = await tree.files(); + + return { + etag: tree.etag, + files: files.map(file => ({ + url: this.integration.resolveUrl({ + url: `/${file.path}`, + base: url, + }), + content: file.content, + })), + }; } toString() { - const { host, token } = this.options; + const { host, token } = this.integration.config; return `azure{host=${host},authed=${Boolean(token)}}`; } } diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts index 974c84b2c2..df9febb352 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.test.ts @@ -15,11 +15,16 @@ */ import { ConfigReader } from '@backstage/config'; +import { + BitbucketIntegration, + readBitbucketIntegrationConfig, +} from '@backstage/integration'; import { msw } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import os from 'os'; import path from 'path'; import { NotModifiedError } from '../errors'; import { BitbucketUrlReader } from './BitbucketUrlReader'; @@ -30,19 +35,45 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ }); const bitbucketProcessor = new BitbucketUrlReader( - { host: 'bitbucket.org', apiBaseUrl: 'https://api.bitbucket.org/2.0' }, + new BitbucketIntegration( + readBitbucketIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.org', + apiBaseUrl: 'https://api.bitbucket.org/2.0', + }), + ), + ), { treeResponseFactory }, ); const hostedBitbucketProcessor = new BitbucketUrlReader( - { - host: 'bitbucket.mycompany.net', - apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', - }, + new BitbucketIntegration( + readBitbucketIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }), + ), + ), { treeResponseFactory }, ); +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + describe('BitbucketUrlReader', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + const worker = setupServer(); + msw.setupDefaultHandlers(worker); + describe('implementation', () => { it('rejects unknown targets', async () => { await expect( @@ -54,19 +85,6 @@ describe('BitbucketUrlReader', () => { }); describe('readTree', () => { - beforeEach(() => { - mockFs({ - '/tmp': mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - - const worker = setupServer(); - msw.setupDefaultHandlers(worker); - const repoBuffer = fs.readFileSync( path.resolve( 'src', @@ -247,12 +265,153 @@ describe('BitbucketUrlReader', () => { expect(() => { /* eslint-disable no-new */ new BitbucketUrlReader( - { - host: 'bitbucket.mycompany.net', - }, + new BitbucketIntegration( + readBitbucketIntegrationConfig( + new ConfigReader({ + host: 'bitbucket.mycompany.net', + }), + ), + ), { treeResponseFactory }, ); }).toThrowError('must configure an explicit apiBaseUrl'); }); }); + + describe('search hosted', () => { + const repoBuffer = fs.readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'bitbucket-repo-with-commit-hash.zip', + ), + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + mainbranch: { + type: 'branch', + name: 'master', + }, + }), + ), + ), + rest.get( + 'https://bitbucket.org/backstage/mock/get/master.zip', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock-12ab34cd56ef.zip', + ), + ctx.body(repoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.org/2.0/repositories/backstage/mock/commits/master', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ hash: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await bitbucketProcessor.search( + 'https://bitbucket.org/backstage/mock/src/master/**/index.*', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.org/backstage/mock/src/master/docs/index.md', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + bitbucketProcessor.search( + 'https://bitbucket.org/backstage/mock/src/master/**/index.*', + { etag: '12ab34cd56ef' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); + + describe('search private', () => { + const privateBitbucketRepoBuffer = fs.readFileSync( + path.resolve( + 'src', + 'reading', + '__fixtures__', + 'bitbucket-server-repo.zip', + ), + ); + + beforeEach(() => { + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/archive?format=zip&prefix=mock&path=docs', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename=backstage-mock.zip', + ), + ctx.body(privateBitbucketRepoBuffer), + ), + ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/commits', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + values: [{ id: '12ab34cd56ef78gh90ij12kl34mn56op78qr90st' }], + }), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await hostedBitbucketProcessor.search( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', + ); + expect(result.etag).toBe('12ab34cd56ef'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/docs/index.md?at=master', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + hostedBitbucketProcessor.search( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/**/index.*?at=master', + { etag: '12ab34cd56ef' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); }); diff --git a/packages/backend-common/src/reading/BitbucketUrlReader.ts b/packages/backend-common/src/reading/BitbucketUrlReader.ts index e51c03e562..3954e7ee4a 100644 --- a/packages/backend-common/src/reading/BitbucketUrlReader.ts +++ b/packages/backend-common/src/reading/BitbucketUrlReader.ts @@ -15,22 +15,25 @@ */ import { - BitbucketIntegrationConfig, + BitbucketIntegration, getBitbucketDefaultBranch, getBitbucketDownloadUrl, getBitbucketFileFetchUrl, getBitbucketRequestOptions, - readBitbucketIntegrationConfigs, + ScmIntegrations, } from '@backstage/integration'; import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; +import { stripFirstDirectoryFromPath } from './tree/util'; import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchOptions, SearchResponse, UrlReader, } from './types'; @@ -40,45 +43,43 @@ import { * the one exposed by Bitbucket Cloud itself. */ export class BitbucketUrlReader implements UrlReader { - private readonly config: BitbucketIntegrationConfig; - private readonly treeResponseFactory: ReadTreeResponseFactory; - static factory: ReaderFactory = ({ config, treeResponseFactory }) => { - const configs = readBitbucketIntegrationConfigs( - config.getOptionalConfigArray('integrations.bitbucket') ?? [], - ); - return configs.map(provider => { - const reader = new BitbucketUrlReader(provider, { treeResponseFactory }); - const predicate = (url: URL) => url.host === provider.host; + const integrations = ScmIntegrations.fromConfig(config); + return integrations.bitbucket.list().map(integration => { + const reader = new BitbucketUrlReader(integration, { + treeResponseFactory, + }); + const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); }; constructor( - config: BitbucketIntegrationConfig, - deps: { treeResponseFactory: ReadTreeResponseFactory }, + private readonly integration: BitbucketIntegration, + private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, ) { - const { host, apiBaseUrl, token, username, appPassword } = config; + const { + host, + apiBaseUrl, + token, + username, + appPassword, + } = integration.config; if (!apiBaseUrl) { throw new Error( `Bitbucket integration for '${host}' must configure an explicit apiBaseUrl`, ); - } - - if (!token && username && !appPassword) { + } else if (!token && username && !appPassword) { throw new Error( `Bitbucket integration for '${host}' has configured a username but is missing a required appPassword.`, ); } - - this.config = config; - this.treeResponseFactory = deps.treeResponseFactory; } async read(url: string): Promise { - const bitbucketUrl = getBitbucketFileFetchUrl(url, this.config); - const options = getBitbucketRequestOptions(this.config); + const bitbucketUrl = getBitbucketFileFetchUrl(url, this.integration.config); + const options = getBitbucketRequestOptions(this.integration.config); let response: Response; try { @@ -109,10 +110,13 @@ export class BitbucketUrlReader implements UrlReader { throw new NotModifiedError(); } - const downloadUrl = await getBitbucketDownloadUrl(url, this.config); + const downloadUrl = await getBitbucketDownloadUrl( + url, + this.integration.config, + ); const archiveBitbucketResponse = await fetch( downloadUrl, - getBitbucketRequestOptions(this.config), + getBitbucketRequestOptions(this.integration.config), ); if (!archiveBitbucketResponse.ok) { const message = `Failed to read tree from ${url}, ${archiveBitbucketResponse.status} ${archiveBitbucketResponse.statusText}`; @@ -122,7 +126,7 @@ export class BitbucketUrlReader implements UrlReader { throw new Error(message); } - return await this.treeResponseFactory.fromZipArchive({ + return await this.deps.treeResponseFactory.fromZipArchive({ stream: (archiveBitbucketResponse.body as unknown) as Readable, subpath: filepath, etag: lastCommitShortHash, @@ -130,12 +134,36 @@ export class BitbucketUrlReader implements UrlReader { }); } - async search(): Promise { - throw new Error('BitbucketUrlReader does not implement search'); + async search(url: string, options?: SearchOptions): Promise { + const { filepath } = parseGitUrl(url); + const matcher = new Minimatch(filepath); + + // TODO(freben): For now, read the entire repo and filter through that. In + // a future improvement, we could be smart and try to deduce that non-glob + // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used + // to get just that part of the repo. + const treeUrl = url.replace(filepath, '').replace(/\/+$/, ''); + + const tree = await this.readTree(treeUrl, { + etag: options?.etag, + filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + }); + const files = await tree.files(); + + return { + etag: tree.etag, + files: files.map(file => ({ + url: this.integration.resolveUrl({ + url: `/${file.path}`, + base: url, + }), + content: file.content, + })), + }; } toString() { - const { host, token, username, appPassword } = this.config; + const { host, token, username, appPassword } = this.integration.config; let authed = Boolean(token); if (!authed) { authed = Boolean(username && appPassword); @@ -148,18 +176,18 @@ export class BitbucketUrlReader implements UrlReader { let branch = ref; if (!branch) { - branch = await getBitbucketDefaultBranch(url, this.config); + branch = await getBitbucketDefaultBranch(url, this.integration.config); } const isHosted = resource === 'bitbucket.org'; // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp222 const commitsApiUrl = isHosted - ? `${this.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}` - : `${this.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`; + ? `${this.integration.config.apiBaseUrl}/repositories/${project}/${repoName}/commits/${branch}` + : `${this.integration.config.apiBaseUrl}/projects/${project}/repos/${repoName}/commits`; const commitsResponse = await fetch( commitsApiUrl, - getBitbucketRequestOptions(this.config), + getBitbucketRequestOptions(this.integration.config), ); if (!commitsResponse.ok) { const message = `Failed to retrieve commits from ${commitsApiUrl}, ${commitsResponse.status} ${commitsResponse.statusText}`; diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index d4e579b143..94e4d0fd31 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -15,12 +15,17 @@ */ import { ConfigReader } from '@backstage/config'; -import { GithubCredentialsProvider } from '@backstage/integration'; +import { + GithubCredentialsProvider, + GitHubIntegration, + readGitHubIntegrationConfig, +} from '@backstage/integration'; import { msw } from '@backstage/test-utils'; import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import os from 'os'; import path from 'path'; import { NotFoundError, NotModifiedError } from '../errors'; import { @@ -41,26 +46,45 @@ const mockCredentialsProvider = ({ } as unknown) as GithubCredentialsProvider; const githubProcessor = new GithubUrlReader( - { - host: 'github.com', - apiBaseUrl: 'https://api.github.com', - }, + new GitHubIntegration( + readGitHubIntegrationConfig( + new ConfigReader({ + host: 'github.com', + apiBaseUrl: 'https://api.github.com', + }), + ), + ), { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, ); const gheProcessor = new GithubUrlReader( - { - host: 'ghe.github.com', - apiBaseUrl: 'https://ghe.github.com/api/v3', - }, + new GitHubIntegration( + readGitHubIntegrationConfig( + new ConfigReader({ + host: 'ghe.github.com', + apiBaseUrl: 'https://ghe.github.com/api/v3', + }), + ), + ), { treeResponseFactory, credentialsProvider: mockCredentialsProvider }, ); +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + describe('GithubUrlReader', () => { const worker = setupServer(); - msw.setupDefaultHandlers(worker); + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + beforeEach(() => { jest.clearAllMocks(); }); @@ -94,7 +118,7 @@ describe('GithubUrlReader', () => { worker.use( rest.get( - 'https://api.github.com/repos/backstage/mock/tree/contents/?ref=main', + 'https://ghe.github.com/api/v3/repos/backstage/mock/tree/contents/?ref=main', (req, res, ctx) => { expect(req.headers.get('authorization')).toBe( mockHeaders.Authorization, @@ -111,7 +135,7 @@ describe('GithubUrlReader', () => { ), ); - await githubProcessor.read( + await gheProcessor.read( 'https://github.com/backstage/mock/tree/blob/main', ); }); @@ -122,16 +146,6 @@ describe('GithubUrlReader', () => { */ describe('readTree', () => { - beforeEach(() => { - mockFs({ - '/tmp': mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - const repoBuffer = fs.readFileSync( path.resolve( 'src', @@ -259,7 +273,7 @@ describe('GithubUrlReader', () => { 'https://github.com/backstage/mock', ); - const dir = await response.dir({ targetDir: '/tmp' }); + const dir = await response.dir({ targetDir: tmpDir }); await expect( fs.readFile(path.join(dir, 'mkdocs.yml'), 'utf8'), @@ -340,7 +354,7 @@ describe('GithubUrlReader', () => { 'https://github.com/backstage/mock/tree/main/docs', ); - const dir = await response.dir({ targetDir: '/tmp' }); + const dir = await response.dir({ targetDir: tmpDir }); await expect( fs.readFile(path.join(dir, 'index.md'), 'utf8'), @@ -397,9 +411,13 @@ describe('GithubUrlReader', () => { expect(() => { /* eslint-disable no-new */ new GithubUrlReader( - { - host: 'ghe.mycompany.net', - }, + new GitHubIntegration( + readGitHubIntegrationConfig( + new ConfigReader({ + host: 'ghe.mycompany.net', + }), + ), + ), { treeResponseFactory, credentialsProvider: mockCredentialsProvider, @@ -414,14 +432,6 @@ describe('GithubUrlReader', () => { */ describe('search', () => { - beforeEach(() => { - mockFs({ '/tmp': mockFs.directory() }); - }); - - afterEach(() => { - mockFs.restore(); - }); - const repoBuffer = fs.readFileSync( path.resolve( 'src', diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 291740a9fc..ac5a93bd5f 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -17,8 +17,8 @@ import { getGitHubFileFetchUrl, GithubCredentialsProvider, - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, + GitHubIntegration, + ScmIntegrations, } from '@backstage/integration'; import { RestEndpointMethodTypes } from '@octokit/rest'; import fetch from 'cross-fetch'; @@ -48,36 +48,36 @@ export type GhBlobResponse = RestEndpointMethodTypes['git']['getBlob']['response */ export class GithubUrlReader implements UrlReader { static factory: ReaderFactory = ({ config, treeResponseFactory }) => { - const configs = readGitHubIntegrationConfigs( - config.getOptionalConfigArray('integrations.github') ?? [], - ); - return configs.map(provider => { - const credentialsProvider = GithubCredentialsProvider.create(provider); - const reader = new GithubUrlReader(provider, { + const integrations = ScmIntegrations.fromConfig(config); + return integrations.github.list().map(integration => { + const credentialsProvider = GithubCredentialsProvider.create( + integration.config, + ); + const reader = new GithubUrlReader(integration, { treeResponseFactory, credentialsProvider, }); - const predicate = (url: URL) => url.host === provider.host; + const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); }; constructor( - private readonly config: GitHubIntegrationConfig, + private readonly integration: GitHubIntegration, private readonly deps: { treeResponseFactory: ReadTreeResponseFactory; credentialsProvider: GithubCredentialsProvider; }, ) { - if (!config.apiBaseUrl && !config.rawBaseUrl) { + if (!integration.config.apiBaseUrl && !integration.config.rawBaseUrl) { throw new Error( - `GitHub integration for '${config.host}' must configure an explicit apiBaseUrl and rawBaseUrl`, + `GitHub integration '${integration.title}' must configure an explicit apiBaseUrl or rawBaseUrl`, ); } } async read(url: string): Promise { - const ghUrl = getGitHubFileFetchUrl(url, this.config); + const ghUrl = getGitHubFileFetchUrl(url, this.integration.config); const { headers } = await this.deps.credentialsProvider.getCredentials({ url, }); @@ -155,7 +155,7 @@ export class GithubUrlReader implements UrlReader { } toString() { - const { host, token } = this.config; + const { host, token } = this.integration.config; return `github{host=${host},authed=${Boolean(token)}}`; } @@ -258,7 +258,7 @@ export class GithubUrlReader implements UrlReader { }); const repo: GhRepoResponse = await this.fetchJson( - `${this.config.apiBaseUrl}/repos/${full_name}`, + `${this.integration.config.apiBaseUrl}/repos/${full_name}`, { headers }, ); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 5b0b4002a3..636540caa8 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -20,11 +20,16 @@ import fs from 'fs-extra'; import mockFs from 'mock-fs'; import { rest } from 'msw'; import { setupServer } from 'msw/node'; +import os from 'os'; import path from 'path'; import { getVoidLogger } from '../logging'; import { GitlabUrlReader } from './GitlabUrlReader'; import { ReadTreeResponseFactory } from './tree'; import { NotModifiedError, NotFoundError } from '../errors'; +import { + GitLabIntegration, + readGitLabIntegrationConfig, +} from '@backstage/integration'; const logger = getVoidLogger(); @@ -33,24 +38,44 @@ const treeResponseFactory = ReadTreeResponseFactory.create({ }); const gitlabProcessor = new GitlabUrlReader( - { - host: 'gitlab.com', - apiBaseUrl: 'https://gitlab.com/api/v4', - baseUrl: 'https://gitlab.com', - }, + new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader({ + host: 'gitlab.com', + apiBaseUrl: 'https://gitlab.com/api/v4', + baseUrl: 'https://gitlab.com', + }), + ), + ), { treeResponseFactory }, ); const hostedGitlabProcessor = new GitlabUrlReader( - { - host: 'gitlab.mycompany.com', - apiBaseUrl: 'https://gitlab.mycompany.com/api/v4', - baseUrl: 'https://gitlab.mycompany.com', - }, + new GitLabIntegration( + readGitLabIntegrationConfig( + new ConfigReader({ + host: 'gitlab.mycompany.com', + apiBaseUrl: 'https://gitlab.mycompany.com/api/v4', + baseUrl: 'https://gitlab.mycompany.com', + }), + ), + ), { treeResponseFactory }, ); +const tmpDir = os.platform() === 'win32' ? 'C:\\tmp' : '/tmp'; + describe('GitlabUrlReader', () => { + beforeEach(() => { + mockFs({ + [tmpDir]: mockFs.directory(), + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + const worker = setupServer(); msw.setupDefaultHandlers(worker); @@ -156,16 +181,6 @@ describe('GitlabUrlReader', () => { }); describe('readTree', () => { - beforeEach(() => { - mockFs({ - '/tmp': mockFs.directory(), - }); - }); - - afterEach(() => { - mockFs.restore(); - }); - const archiveBuffer = fs.readFileSync( path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'), ); @@ -382,4 +397,80 @@ describe('GitlabUrlReader', () => { await expect(fnGithub).rejects.toThrow(NotFoundError); }); }); + + describe('search', () => { + const archiveBuffer = fs.readFileSync( + path.resolve('src', 'reading', '__fixtures__', 'gitlab-archive.zip'), + ); + + const projectGitlabApiResponse = { + id: 11111111, + default_branch: 'main', + }; + + const branchGitlabApiResponse = { + commit: { + id: 'sha123abc', + }, + }; + + beforeEach(() => { + worker.use( + rest.get( + 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/archive.zip?sha=main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/zip'), + ctx.set( + 'content-disposition', + 'attachment; filename="mock-main-sha123abc.zip"', + ), + ctx.body(archiveBuffer), + ), + ), + rest.get( + 'https://gitlab.com/api/v4/projects/backstage%2Fmock', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(projectGitlabApiResponse), + ), + ), + rest.get( + 'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/branches/main', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(branchGitlabApiResponse), + ), + ), + ); + }); + + it('works for the naive case', async () => { + const result = await gitlabProcessor.search( + 'https://gitlab.com/backstage/mock/tree/main/**/index.*', + ); + expect(result.etag).toBe('sha123abc'); + expect(result.files.length).toBe(1); + expect(result.files[0].url).toBe( + 'https://gitlab.com/backstage/mock/tree/main/docs/index.md', + ); + await expect(result.files[0].content()).resolves.toEqual( + Buffer.from('# Test\n'), + ); + }); + + it('throws NotModifiedError when same etag', async () => { + await expect( + gitlabProcessor.search( + 'https://gitlab.com/backstage/mock/tree/main/**/index.*', + { etag: 'sha123abc' }, + ), + ).rejects.toThrow(NotModifiedError); + }); + }); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts index 66030e30d9..66ceae05c6 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.ts @@ -17,49 +17,51 @@ import { getGitLabFileFetchUrl, getGitLabRequestOptions, - GitLabIntegrationConfig, - readGitLabIntegrationConfigs, + GitLabIntegration, + ScmIntegrations, } from '@backstage/integration'; import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; +import { Minimatch } from 'minimatch'; import { Readable } from 'stream'; import { NotFoundError, NotModifiedError } from '../errors'; import { ReadTreeResponseFactory } from './tree'; +import { stripFirstDirectoryFromPath } from './tree/util'; import { ReaderFactory, ReadTreeOptions, ReadTreeResponse, + SearchOptions, SearchResponse, UrlReader, } from './types'; export class GitlabUrlReader implements UrlReader { - private readonly treeResponseFactory: ReadTreeResponseFactory; - static factory: ReaderFactory = ({ config, treeResponseFactory }) => { - const configs = readGitLabIntegrationConfigs( - config.getOptionalConfigArray('integrations.gitlab') ?? [], - ); - return configs.map(provider => { - const reader = new GitlabUrlReader(provider, { treeResponseFactory }); - const predicate = (url: URL) => url.host === provider.host; + const integrations = ScmIntegrations.fromConfig(config); + return integrations.gitlab.list().map(integration => { + const reader = new GitlabUrlReader(integration, { + treeResponseFactory, + }); + const predicate = (url: URL) => url.host === integration.config.host; return { reader, predicate }; }); }; constructor( - private readonly config: GitLabIntegrationConfig, - deps: { treeResponseFactory: ReadTreeResponseFactory }, - ) { - this.treeResponseFactory = deps.treeResponseFactory; - } + private readonly integration: GitLabIntegration, + private readonly deps: { treeResponseFactory: ReadTreeResponseFactory }, + ) {} async read(url: string): Promise { - const builtUrl = await getGitLabFileFetchUrl(url, this.config); + const builtUrl = await getGitLabFileFetchUrl(url, this.integration.config); let response: Response; try { - response = await fetch(builtUrl, getGitLabRequestOptions(this.config)); + response = await fetch( + builtUrl, + getGitLabRequestOptions(this.integration.config), + ); } catch (e) { throw new Error(`Unable to read ${url}, ${e}`); } @@ -86,9 +88,11 @@ export class GitlabUrlReader implements UrlReader { // https://docs.gitlab.com/ee/api/README.html#namespaced-path-encoding const projectGitlabResponse = await fetch( new URL( - `${this.config.apiBaseUrl}/projects/${encodeURIComponent(full_name)}`, + `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( + full_name, + )}`, ).toString(), - getGitLabRequestOptions(this.config), + getGitLabRequestOptions(this.integration.config), ); if (!projectGitlabResponse.ok) { const msg = `Failed to read tree from ${url}, ${projectGitlabResponse.status} ${projectGitlabResponse.statusText}`; @@ -106,11 +110,11 @@ export class GitlabUrlReader implements UrlReader { // the provided sha. const branchGitlabResponse = await fetch( new URL( - `${this.config.apiBaseUrl}/projects/${encodeURIComponent( + `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( full_name, )}/repository/branches/${branch}`, ).toString(), - getGitLabRequestOptions(this.config), + getGitLabRequestOptions(this.integration.config), ); if (!branchGitlabResponse.ok) { const message = `Failed to read tree (branch) from ${url}, ${branchGitlabResponse.status} ${branchGitlabResponse.statusText}`; @@ -128,10 +132,10 @@ export class GitlabUrlReader implements UrlReader { // https://docs.gitlab.com/ee/api/repositories.html#get-file-archive const archiveGitLabResponse = await fetch( - `${this.config.apiBaseUrl}/projects/${encodeURIComponent( + `${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent( full_name, )}/repository/archive.zip?sha=${branch}`, - getGitLabRequestOptions(this.config), + getGitLabRequestOptions(this.integration.config), ); if (!archiveGitLabResponse.ok) { const message = `Failed to read tree (archive) from ${url}, ${archiveGitLabResponse.status} ${archiveGitLabResponse.statusText}`; @@ -141,7 +145,7 @@ export class GitlabUrlReader implements UrlReader { throw new Error(message); } - return await this.treeResponseFactory.fromZipArchive({ + return await this.deps.treeResponseFactory.fromZipArchive({ stream: (archiveGitLabResponse.body as unknown) as Readable, subpath: filepath, etag: commitSha, @@ -149,12 +153,33 @@ export class GitlabUrlReader implements UrlReader { }); } - async search(): Promise { - throw new Error('GitlabUrlReader does not implement search'); + async search(url: string, options?: SearchOptions): Promise { + const { filepath } = parseGitUrl(url); + const matcher = new Minimatch(filepath); + + // TODO(freben): For now, read the entire repo and filter through that. In + // a future improvement, we could be smart and try to deduce that non-glob + // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used + // to get just that part of the repo. + const treeUrl = url.replace(filepath, '').replace(/\/+$/, ''); + + const tree = await this.readTree(treeUrl, { + etag: options?.etag, + filter: path => matcher.match(stripFirstDirectoryFromPath(path)), + }); + const files = await tree.files(); + + return { + etag: tree.etag, + files: files.map(file => ({ + url: this.integration.resolveUrl({ url: `/${file.path}`, base: url }), + content: file.content, + })), + }; } toString() { - const { host, token } = this.config; + const { host, token } = this.integration.config; return `gitlab{host=${host},authed=${Boolean(token)}}`; } } diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index d22529293f..e61a1df645 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -25,15 +25,12 @@ import { ReadTreeResponseDirOptions, ReadTreeResponseFile, } from '../types'; +import { stripFirstDirectoryFromPath } from './util'; // Tar types for `Parse` is not a proper constructor, but it should be const TarParseStream = (Parse as unknown) as { new (): ParseStream }; const pipeline = promisify(pipelineCb); -// Matches a directory name + one `/` at the start of any string, -// containing any character except `/` one or more times, and ending with a `/` -// e.g. Will match `dirA/` in `dirA/dirB/file.ext` -const directoryNameRegex = /^[^\/]+\//; /** * Wraps a tar archive stream into a tree response reader. @@ -84,7 +81,7 @@ export class TarArchiveResponse implements ReadTreeResponse { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. - const relativePath = entry.path.replace(directoryNameRegex, ''); + const relativePath = stripFirstDirectoryFromPath(entry.path); if (this.subPath) { if (!relativePath.startsWith(this.subPath)) { @@ -161,7 +158,7 @@ export class TarArchiveResponse implements ReadTreeResponse { filter: path => { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. - const relativePath = path.replace(directoryNameRegex, ''); + const relativePath = stripFirstDirectoryFromPath(path); if (this.subPath && !relativePath.startsWith(this.subPath)) { return false; } diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 2547c7b0e4..4aebff5c84 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -24,11 +24,7 @@ import { ReadTreeResponseDirOptions, ReadTreeResponseFile, } from '../types'; - -// Matches a directory name + one `/` at the start of any string, -// containing any character except / one or more times, and ending with a `/` -// e.g. Will match `dirA/` in `dirA/dirB/file.ext` -const directoryNameRegex = /^[^\/]+\//; +import { stripFirstDirectoryFromPath } from './util'; /** * Wraps a zip archive stream into a tree response reader. @@ -65,18 +61,13 @@ export class ZipArchiveResponse implements ReadTreeResponse { this.read = true; } - // Will remove the top level dir name from the path since its name is hard to predetermine. - private stripTopDirectory(path: string): string { - return path.replace(directoryNameRegex, ''); - } - // File path relative to the root extracted directory or a sub directory if subpath is set. private getInnerPath(path: string): string { return path.slice(this.subPath.length); } private shouldBeIncluded(entry: Entry): boolean { - const strippedPath = this.stripTopDirectory(entry.path); + const strippedPath = stripFirstDirectoryFromPath(entry.path); if (this.subPath) { if (!strippedPath.startsWith(this.subPath)) { @@ -104,7 +95,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { if (this.shouldBeIncluded(entry)) { files.push({ - path: this.getInnerPath(this.stripTopDirectory(entry.path)), + path: this.getInnerPath(stripFirstDirectoryFromPath(entry.path)), content: () => entry.buffer(), }); } else { @@ -153,7 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { // as a zip can have files with directories without directory entries if (entry.type === 'File' && this.shouldBeIncluded(entry)) { const entryPath = this.getInnerPath( - this.stripTopDirectory(entry.path), + stripFirstDirectoryFromPath(entry.path), ); const dirname = platformPath.dirname(entryPath); if (dirname) { diff --git a/packages/backend-common/src/reading/tree/util.ts b/packages/backend-common/src/reading/tree/util.ts new file mode 100644 index 0000000000..8e908a5e60 --- /dev/null +++ b/packages/backend-common/src/reading/tree/util.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +// Matches a directory name + one `/` at the start of any string, +// containing any character except `/` one or more times, and ending with a `/` +// e.g. Will match `dirA/` in `dirA/dirB/file.ext` +const directoryNameRegex = /^[^\/]+\//; + +// Removes the first segment of a forward-slash-separated path +export function stripFirstDirectoryFromPath(path: string): string { + return path.replace(directoryNameRegex, ''); +} diff --git a/packages/backend-common/src/scm/git.ts b/packages/backend-common/src/scm/git.ts index 5b24d23b99..21aa9bf5d1 100644 --- a/packages/backend-common/src/scm/git.ts +++ b/packages/backend-common/src/scm/git.ts @@ -23,12 +23,11 @@ import fs from 'fs-extra'; import { Logger } from 'winston'; /* -provider username password -GitHub token 'x-oauth-basic' -GitHub App token 'x-access-token' -BitBucket 'x-token-auth' token -GitLab 'oauth2' token -From : https://isomorphic-git.org/docs/en/onAuth +provider username password +GitHub 'x-access-token' token +BitBucket 'x-token-auth' token +GitLab 'oauth2' token +From : https://isomorphic-git.org/docs/en/onAuth with fix for GitHub Azure 'notempty' token */ diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index 9c5ac20fa3..07da975184 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -161,7 +161,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { app.use(cors(corsOptions)); } app.use(compression()); - app.use(requestLoggingHandler()); + app.use(requestLoggingHandler(logger)); for (const [root, route] of this.routers) { app.use(root, route); } diff --git a/packages/catalog-model/examples/components/artist-lookup-component.yaml b/packages/catalog-model/examples/components/artist-lookup-component.yaml index 29c892274d..edd9b8fcf9 100644 --- a/packages/catalog-model/examples/components/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/components/artist-lookup-component.yaml @@ -25,6 +25,9 @@ metadata: - url: https://example.com/web title: Website icon: web + - url: https://example.com/alert + title: Alerts + icon: alert spec: type: service lifecycle: experimental diff --git a/packages/cli/package.json b/packages/cli/package.json index dde6bc34ba..e54696ce43 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -32,8 +32,8 @@ "@backstage/config": "^0.1.2", "@backstage/config-loader": "^0.5.1", "@hot-loader/react-dom": "^16.13.0", - "@lerna/package-graph": "^3.18.5", - "@lerna/project": "^3.18.0", + "@lerna/package-graph": "^4.0.0", + "@lerna/project": "^4.0.0", "@octokit/request": "^5.4.12", "@rollup/plugin-commonjs": "^17.1.0", "@rollup/plugin-json": "^4.0.2", diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index 6fbcc65c9a..f81e055f69 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -72,8 +72,8 @@ async function readBuildInfo() { } async function loadLernaPackages(): Promise { - const LernaProject = require('@lerna/project'); - const project = new LernaProject(cliPaths.targetDir); + const { Project } = require('@lerna/project'); + const project = new Project(cliPaths.targetDir); return project.getPackages(); } diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index 30469421d3..b49b644e25 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -28,8 +28,8 @@ export async function loadCliConfig(options: Options) { const configPaths = options.args.map(arg => paths.resolveTarget(arg)); // Consider all packages in the monorepo when loading in config - const LernaProject = require('@lerna/project'); - const project = new LernaProject(paths.targetDir); + const { Project } = require('@lerna/project'); + const project = new Project(paths.targetDir); const packages = await project.getPackages(); const localPackageNames = options.fromPackage @@ -75,7 +75,7 @@ export async function loadCliConfig(options: Options) { } function findPackages(packages: any[], fromPackage: string): string[] { - const PackageGraph = require('@lerna/package-graph'); + const { PackageGraph } = require('@lerna/package-graph'); const graph = new PackageGraph(packages); diff --git a/packages/cli/src/lib/packager/index.ts b/packages/cli/src/lib/packager/index.ts index 3976998998..afe29fbd7c 100644 --- a/packages/cli/src/lib/packager/index.ts +++ b/packages/cli/src/lib/packager/index.ts @@ -219,10 +219,10 @@ async function moveToDistWorkspace( } async function findTargetPackages(pkgNames: string[]): Promise { - const LernaProject = require('@lerna/project'); - const PackageGraph = require('@lerna/package-graph'); + const { Project } = require('@lerna/project'); + const { PackageGraph } = require('@lerna/package-graph'); - const project = new LernaProject(paths.targetDir); + const project = new Project(paths.targetDir); const packages = await project.getPackages(); const graph = new PackageGraph(packages); diff --git a/packages/cli/src/lib/versioning/Lockfile.ts b/packages/cli/src/lib/versioning/Lockfile.ts index f567786b7c..f0867c6408 100644 --- a/packages/cli/src/lib/versioning/Lockfile.ts +++ b/packages/cli/src/lib/versioning/Lockfile.ts @@ -153,7 +153,7 @@ export class Lockfile { const acceptedVersion = versions.find(v => semver.satisfies(v, range)); if (!acceptedVersion) { throw new Error( - `No existing version was accepted for range ${range}, searching through ${versions}`, + `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`, ); } diff --git a/packages/cli/src/lib/versioning/packages.test.ts b/packages/cli/src/lib/versioning/packages.test.ts index 5af040809a..aca439a0e7 100644 --- a/packages/cli/src/lib/versioning/packages.test.ts +++ b/packages/cli/src/lib/versioning/packages.test.ts @@ -59,8 +59,8 @@ describe('mapDependencies', () => { it('should read dependencies', async () => { // Make sure all modules involved in package discovery are in the module cache before we mock fs - const LernaProject = require('@lerna/project'); - const project = new LernaProject(paths.targetDir); + const { Project } = require('@lerna/project'); + const project = new Project(paths.targetDir); await project.getPackages(); mockFs({ diff --git a/packages/cli/src/lib/versioning/packages.ts b/packages/cli/src/lib/versioning/packages.ts index 991a4d23ef..777dc72757 100644 --- a/packages/cli/src/lib/versioning/packages.ts +++ b/packages/cli/src/lib/versioning/packages.ts @@ -67,8 +67,8 @@ export async function fetchPackageInfo( export async function mapDependencies( targetDir: string, ): Promise> { - const LernaProject = require('@lerna/project'); - const project = new LernaProject(targetDir); + const { Project } = require('@lerna/project'); + const project = new Project(targetDir); const packages = await project.getPackages(); const dependencyMap = new Map(); diff --git a/packages/core-api/src/app/App.test.tsx b/packages/core-api/src/app/App.test.tsx index 8dd40d52b0..6a1e80334b 100644 --- a/packages/core-api/src/app/App.test.tsx +++ b/packages/core-api/src/app/App.test.tsx @@ -28,7 +28,7 @@ import { generateBoundRoutes, PrivateAppImpl } from './App'; describe('generateBoundRoutes', () => { it('runs happy path', () => { - const external = { myRoute: createExternalRouteRef() }; + const external = { myRoute: createExternalRouteRef({ id: '1' }) }; const ref = createRouteRef({ path: '', title: '' }); const result = generateBoundRoutes(({ bind }) => { bind(external, { myRoute: ref }); @@ -38,7 +38,7 @@ describe('generateBoundRoutes', () => { }); it('throws on unknown keys', () => { - const external = { myRoute: createExternalRouteRef() }; + const external = { myRoute: createExternalRouteRef({ id: '2' }) }; const ref = createRouteRef({ path: '', title: '' }); expect(() => generateBoundRoutes(({ bind }) => { @@ -51,7 +51,7 @@ describe('generateBoundRoutes', () => { describe('Integration Test', () => { const plugin1RouteRef = createRouteRef({ path: '/blah1', title: '' }); const plugin2RouteRef = createRouteRef({ path: '/blah2', title: '' }); - const externalRouteRef = createExternalRouteRef(); + const externalRouteRef = createExternalRouteRef({ id: '3' }); const plugin1 = createPlugin({ id: 'blob', @@ -77,7 +77,7 @@ describe('Integration Test', () => { Promise.resolve((_: PropsWithChildren<{ path?: string }>) => { // eslint-disable-next-line react-hooks/rules-of-hooks const routeRefFunction = useRouteRef(externalRouteRef); - return
Our Route Is: {routeRefFunction({})}
; + return
Our Route Is: {routeRefFunction()}
; }), mountPoint: plugin1RouteRef, }), diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index 2aaeba9796..f50bcdd015 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -48,7 +48,7 @@ import { routeElementDiscoverer, traverseElementTree, } from '../extensions/traversal'; -import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; +import { IconComponent, IconComponentMap, IconKey } from '../icons'; import { BackstagePlugin } from '../plugin'; import { RouteRef } from '../routing'; import { @@ -95,7 +95,7 @@ export function generateBoundRoutes( type FullAppOptions = { apis: Iterable; - icons: SystemIcons; + icons: IconComponentMap; plugins: BackstagePlugin[]; components: AppComponents; themes: AppTheme[]; @@ -144,7 +144,7 @@ export class PrivateAppImpl implements BackstageApp { private configApi?: ConfigApi; private readonly apis: Iterable; - private readonly icons: SystemIcons; + private readonly icons: IconComponentMap; private readonly plugins: BackstagePlugin[]; private readonly components: AppComponents; private readonly themes: AppTheme[]; @@ -169,7 +169,7 @@ export class PrivateAppImpl implements BackstageApp { return this.plugins; } - getSystemIcon(key: SystemIconKey): IconComponent { + getSystemIcon(key: IconKey): IconComponent { return this.icons[key]; } diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index b6b1002d12..1970868ca6 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -15,7 +15,7 @@ */ import { ComponentType } from 'react'; -import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; +import { IconComponent, IconComponentMap, IconKey } from '../icons'; import { BackstagePlugin, AnyExternalRoutes } from '../plugin/types'; import { RouteRef } from '../routing'; import { AnyApiFactory } from '../apis'; @@ -94,7 +94,7 @@ export type AppOptions = { /** * Supply icons to override the default ones. */ - icons?: Partial; + icons?: IconComponentMap; /** * A list of all plugins to include in the app. @@ -169,9 +169,9 @@ export type BackstageApp = { getPlugins(): BackstagePlugin[]; /** - * Get a common icon for this app. + * Get a common or custom icon for this app. */ - getSystemIcon(key: SystemIconKey): IconComponent; + getSystemIcon(key: IconKey): IconComponent; /** * Provider component that should wrap the Router created with getRouter() diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx index 50c4b68e43..0c2b580ea7 100644 --- a/packages/core-api/src/icons/icons.tsx +++ b/packages/core-api/src/icons/icons.tsx @@ -15,15 +15,19 @@ */ import { SvgIconProps } from '@material-ui/core'; +import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiHelpIcon from '@material-ui/icons/Help'; import PeopleIcon from '@material-ui/icons/People'; import PersonIcon from '@material-ui/icons/Person'; import React from 'react'; import { useApp } from '../app/AppContext'; -import { IconComponent, SystemIconKey, SystemIcons } from './types'; +import { IconComponent, SystemIconKey, IconComponentMap } from './types'; -export const defaultSystemIcons: SystemIcons = { +export const defaultSystemIcons: IconComponentMap = { user: PersonIcon, group: PeopleIcon, + dashboard: MuiDashboardIcon, + help: MuiHelpIcon, }; const overridableSystemIcon = (key: SystemIconKey): IconComponent => { @@ -35,5 +39,7 @@ const overridableSystemIcon = (key: SystemIconKey): IconComponent => { return Component; }; -export const UserIcon = overridableSystemIcon('user'); +export const DashboardIcon = overridableSystemIcon('dashboard'); export const GroupIcon = overridableSystemIcon('group'); +export const HelpIcon = overridableSystemIcon('help'); +export const UserIcon = overridableSystemIcon('user'); diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts index 30e0ba53b2..be24b223ae 100644 --- a/packages/core-api/src/icons/types.ts +++ b/packages/core-api/src/icons/types.ts @@ -17,6 +17,8 @@ import { ComponentType } from 'react'; import { SvgIconProps } from '@material-ui/core'; +export type SystemIconKey = 'user' | 'group' | 'dashboard' | 'help'; + export type IconComponent = ComponentType; -export type SystemIconKey = 'user' | 'group'; -export type SystemIcons = { [key in SystemIconKey]: IconComponent }; +export type IconKey = SystemIconKey | string; +export type IconComponentMap = { [key in IconKey]: IconComponent }; diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 0a3df49b58..5ae85ab553 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -65,13 +65,22 @@ export function createRouteRef< } export class ExternalRouteRef { - private constructor() {} - - toString() { - return `externalRouteRef{}`; + private constructor(id: string) { + this.toString = () => `externalRouteRef{${id}}`; } } -export function createExternalRouteRef(): ExternalRouteRef { - return new ((ExternalRouteRef as unknown) as { new (): ExternalRouteRef })(); +export type ExternalRouteRefOptions = { + /** + * An identifier for this route, used to identify it in error messages + */ + id: string; +}; + +export function createExternalRouteRef( + options: ExternalRouteRefOptions, +): ExternalRouteRef { + return new ((ExternalRouteRef as unknown) as { + new (id: string): ExternalRouteRef; + })(options.id); } diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index 84a8314807..462b44c3bd 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -59,9 +59,9 @@ const ref2 = createRouteRef(mockConfig({ path: '/wat2' })); const ref3 = createRouteRef(mockConfig({ path: '/wat3' })); const ref4 = createRouteRef(mockConfig({ path: '/wat4' })); const ref5 = createRouteRef(mockConfig({ path: '/wat5' })); -const eRefA = createExternalRouteRef(); -const eRefB = createExternalRouteRef(); -const eRefC = createExternalRouteRef(); +const eRefA = createExternalRouteRef({ id: '1' }); +const eRefB = createExternalRouteRef({ id: '2' }); +const eRefC = createExternalRouteRef({ id: '3' }); const MockRouteSource = (props: { path?: string; diff --git a/packages/core-api/src/routing/hooks.tsx b/packages/core-api/src/routing/hooks.tsx index 3a18d8b0af..c478027ae2 100644 --- a/packages/core-api/src/routing/hooks.tsx +++ b/packages/core-api/src/routing/hooks.tsx @@ -111,7 +111,7 @@ class RouteResolver { const RoutingContext = createContext(undefined); -export function useRouteRef( +export function useRouteRef( routeRef: RouteRef | ExternalRouteRef, ): RouteFunc { const sourceLocation = useLocation(); diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index af71e0c3e9..ad88f8ff02 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -21,6 +21,6 @@ export type { MutableRouteRef, } from './types'; export { FlatRoutes } from './FlatRoutes'; -export { createRouteRef } from './RouteRef'; +export { createRouteRef, createExternalRouteRef } from './RouteRef'; export type { RouteRefConfig } from './RouteRef'; export { useRouteRef } from './hooks'; diff --git a/packages/core/src/components/Table/Table.stories.tsx b/packages/core/src/components/Table/Table.stories.tsx index 4be85e494f..64ceb761f8 100644 --- a/packages/core/src/components/Table/Table.stories.tsx +++ b/packages/core/src/components/Table/Table.stories.tsx @@ -14,8 +14,10 @@ * limitations under the License. */ +import { makeStyles } from '@material-ui/core'; import React from 'react'; -import { Table, SubvalueCell, TableColumn } from './'; +import { Link } from '../Link'; +import { SubvalueCell, Table, TableColumn } from './'; import { TableFilter } from './Table'; export default { @@ -23,7 +25,16 @@ export default { component: Table, }; -const containerStyle = { width: 850 }; +const useStyles = makeStyles(theme => ({ + container: { + width: 850, + }, + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); const generateTestData: (number: number) => Array<{}> = (rows = 10) => { const data: Array<{}> = []; @@ -43,6 +54,7 @@ const generateTestData: (number: number) => Array<{}> = (rows = 10) => { const testData10 = generateTestData(10); export const DefaultTable = () => { + const classes = useStyles(); const columns: TableColumn[] = [ { title: 'Column 1', @@ -66,7 +78,7 @@ export const DefaultTable = () => { ]; return ( -
+
{ ); }; -export const SubtitleTable = () => { +export const EmptyTable = () => { + const classes = useStyles(); const columns: TableColumn[] = [ { title: 'Column 1', @@ -101,7 +114,49 @@ export const SubtitleTable = () => { ]; return ( -
+
+
+ No data was added yet,  + learn how to add data. + + } + title="Backstage Table" + /> + + ); +}; + +export const SubtitleTable = () => { + const classes = useStyles(); + const columns: TableColumn[] = [ + { + title: 'Column 1', + field: 'col1', + highlight: true, + }, + { + title: 'Column 2', + field: 'col2', + }, + { + title: 'Numeric value', + field: 'number', + type: 'numeric', + }, + { + title: 'A Date', + field: 'date', + type: 'date', + }, + ]; + + return ( +
{ }; export const HiddenSearchTable = () => { + const classes = useStyles(); const columns: TableColumn[] = [ { title: 'Column 1', @@ -137,7 +193,7 @@ export const HiddenSearchTable = () => { ]; return ( -
+
{ }; export const SubvalueTable = () => { + const classes = useStyles(); const columns: TableColumn[] = [ { title: 'Column 1', @@ -181,13 +238,14 @@ export const SubvalueTable = () => { ]; return ( -
+
); }; export const DenseTable = () => { + const classes = useStyles(); const columns: TableColumn[] = [ { title: 'Column 1', @@ -211,7 +269,7 @@ export const DenseTable = () => { ]; return ( -
+
{ }; export const FilterTable = () => { + const classes = useStyles(); const columns: TableColumn[] = [ { title: 'Column 1', @@ -261,7 +320,7 @@ export const FilterTable = () => { ]; return ( -
+
', () => { ); expect(rendered.getByText('subtitle')).toBeInTheDocument(); }); + + it('renders custom empty component if empty', async () => { + const rendered = await renderInTestApp( +
EMPTY} + columns={minProps.columns} + data={[]} + />, + ); + expect(rendered.getByText('EMPTY')).toBeInTheDocument(); + }); }); diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index dbb2964845..3892d82bb3 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -42,12 +42,14 @@ import MTable, { Column, Icons, MaterialTableProps, + MTableBody, MTableHeader, MTableToolbar, Options, } from 'material-table'; import React, { forwardRef, + ReactNode, useCallback, useEffect, useRef, @@ -202,6 +204,7 @@ export interface TableProps subtitle?: string; filters?: TableFilter[]; initialState?: TableState; + emptyContent?: ReactNode; onStateChange?: (state: TableState) => any; } @@ -212,6 +215,7 @@ export function Table({ subtitle, filters, initialState, + emptyContent, onStateChange, ...props }: TableProps) { @@ -423,6 +427,23 @@ export function Table({ ], ); + const Body = useCallback( + bodyProps => { + if (emptyContent && data.length === 0) { + return ( + + + + + + ); + } + + return ; + }, + [data, emptyContent, columns], + ); + return (
{filtersOpen && data && filters?.length && ( @@ -438,6 +459,7 @@ export function Table({ ), Toolbar, + Body, }} options={{ ...defaultOptions, ...options }} columns={MTColumns} diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 77eba9759a..28056b33bf 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -31,6 +31,7 @@ export * from './MarkdownContent'; export * from './OAuthRequestDialog'; export * from './Progress'; export * from './ProgressBars'; +export * from './Select'; export * from './SimpleStepper'; export * from './Status'; export * from './StructuredMetadataTable'; diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx new file mode 100644 index 0000000000..6f0ef0cb69 --- /dev/null +++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.stories.tsx @@ -0,0 +1,137 @@ +/* + * 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 { Box, List, ListItem, Popover, Typography } from '@material-ui/core'; +import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import React, { Fragment } from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { Breadcrumbs } from '.'; +import { Header, Page } from '..'; +import { Link } from '../../components/Link'; + +export default { + title: 'Layout/Breadcrumbs', + component: Breadcrumbs, +}; + +export const InHeader = () => ( + +

Standard breadcrumbs

+

+ Underlined pages are links. This should show a hierarchical relationship. +

+ + +
+ + +); + +export const OutsideOfHeader = () => { + const [anchorEl, setAnchorEl] = React.useState( + null, + ); + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + return ( + +

+ It might be the case that you want to keep your breadcrumbs outside of + the header. In that case, they should be positioned above the title of + the page. +

+ +

Standard breadcrumbs

+

+ Underlined pages are links. This should show a hierarchical + relationship. +

+ + + + + General Page + Second Page + Current page + + +

Hidden breadcrumbs

+

+ Use this when you have more than three breadcrumbs. When user clicks on + ellipses, expand the breadcrumbs out. +

+ + + General Page + Second Page + Third Page + Fourth Page + Current page + + +

Layered breadcrumbs

+

+ Use this when you want to show alternative breadcrumbs on the same + hierarchical level. +

+ + + + General Page + + + Second Page + {open ? : } + + + Current page + + + + + Parallel second page + + + Another parallel second page + + + Yet another, parallel second page + + + + +
+ ); +}; diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx new file mode 100644 index 0000000000..0a490afb76 --- /dev/null +++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderInTestApp } from '@backstage/test-utils'; +import { Typography } from '@material-ui/core'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { Link } from '../..'; +import { Breadcrumbs } from './Breadcrumbs'; + +describe('', () => { + it('should render', async () => { + const rendered = await renderInTestApp( + + General Page + Current Page + , + ); + expect(rendered.getByLabelText('breadcrumb')).toBeVisible(); + expect(rendered.getByText('General Page')).toBeVisible(); + expect(rendered.getByText('Current Page')).toBeVisible(); + }); + + it('should render hidden breadcrumbs', async () => { + const rendered = await renderInTestApp( + + General Page + Second Page + Third Page + Fourth Page + Current page + , + ); + expect(rendered.getByText('...')).toBeVisible(); + expect(rendered.queryByText('Third Page')).not.toBeInTheDocument(); + expect(rendered.queryByText('Fourth Page')).not.toBeInTheDocument(); + fireEvent.click(rendered.getByText('...')); + expect(rendered.getByText('Third Page')).toBeVisible(); + expect(rendered.getByText('Fourth Page')).toBeVisible(); + }); +}); diff --git a/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx new file mode 100644 index 0000000000..d8cf88497f --- /dev/null +++ b/packages/core/src/layout/Breadcrumbs/Breadcrumbs.tsx @@ -0,0 +1,99 @@ +/* + * 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 { + Box, + Breadcrumbs as MaterialBreadcrumbs, + List, + ListItem, + Popover, + Typography, + withStyles, +} from '@material-ui/core'; +import React, { ComponentProps, Fragment } from 'react'; + +type Props = ComponentProps; + +const ClickableText = withStyles({ + root: { + textDecoration: 'underline', + cursor: 'pointer', + }, +})(Typography); + +const StyledBox = withStyles({ + root: { + textDecoration: 'underline', + color: 'inherit', + }, +})(Box); + +export const Breadcrumbs = ({ children, ...props }: Props) => { + const [anchorEl, setAnchorEl] = React.useState( + null, + ); + + const childrenArray = React.Children.toArray(children); + + const [firstPage, secondPage, ...expandablePages] = childrenArray; + const currentPage = expandablePages.length + ? expandablePages.pop() + : childrenArray[childrenArray.length - 1]; + const hasHiddenBreadcrumbs = childrenArray.length > 3; + + const handleClick = (event: React.MouseEvent) => { + setAnchorEl(event.currentTarget); + }; + + const handleClose = () => { + setAnchorEl(null); + }; + + const open = Boolean(anchorEl); + return ( + + + {childrenArray.length > 1 && {firstPage}} + {childrenArray.length > 2 && {secondPage}} + {hasHiddenBreadcrumbs && ( + ... + )} + {currentPage} + + + + {expandablePages.map(pageLink => ( + + {pageLink} + + ))} + + + + ); +}; diff --git a/plugins/api-docs/src/components/EmptyState/index.ts b/packages/core/src/layout/Breadcrumbs/index.ts similarity index 78% rename from plugins/api-docs/src/components/EmptyState/index.ts rename to packages/core/src/layout/Breadcrumbs/index.ts index d195c43eb1..6c5c2539df 100644 --- a/plugins/api-docs/src/components/EmptyState/index.ts +++ b/packages/core/src/layout/Breadcrumbs/index.ts @@ -14,5 +14,4 @@ * limitations under the License. */ -export { MissingConsumesApisEmptyState } from './MissingConsumesApisEmptyState'; -export { MissingProvidesApisEmptyState } from './MissingProvidesApisEmptyState'; +export { Breadcrumbs } from './Breadcrumbs'; diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index 98db36ab14..73fa3066ea 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -16,15 +16,10 @@ import React, { ReactNode, CSSProperties, PropsWithChildren } from 'react'; import { Helmet } from 'react-helmet'; -import { - Link, - Typography, - Tooltip, - makeStyles, - Breadcrumbs, -} from '@material-ui/core'; -import ChevronRightIcon from '@material-ui/icons/ChevronRight'; +import { Typography, Tooltip, makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; +import { Breadcrumbs } from '..'; +import { Link } from '../../components'; const useStyles = makeStyles(theme => ({ header: { @@ -136,15 +131,9 @@ const TypeFragment = ({ } return ( - } - className={classes.breadcrumb} - > - - {type} - - {pageTitle} + + {type} + {pageTitle} ); }; diff --git a/packages/core/src/layout/index.ts b/packages/core/src/layout/index.ts index d97d30982e..2ab1ebaf5b 100644 --- a/packages/core/src/layout/index.ts +++ b/packages/core/src/layout/index.ts @@ -28,3 +28,4 @@ export * from './Page'; export * from './Sidebar'; export * from './SignInPage'; export * from './TabbedCard'; +export * from './Breadcrumbs'; diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index 3fa350fa7c..58fb20ba05 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -30,7 +30,7 @@ "devDependencies": { "@backstage/cli": "^{{version '@backstage/cli'}}", "@spotify/prettier-config": "^7.0.0", - "lerna": "^3.20.2", + "lerna": "^4.0.0", "prettier": "^1.19.1" }, "prettier": "@spotify/prettier-config", diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index c20fdcef9c..dddf6ed5d0 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -19,6 +19,7 @@ import { AzureIntegration } from './azure/AzureIntegration'; import { BitbucketIntegration } from './bitbucket/BitbucketIntegration'; import { GitHubIntegration } from './github/GitHubIntegration'; import { GitLabIntegration } from './gitlab/GitLabIntegration'; +import { defaultScmResolveUrl } from './helpers'; import { ScmIntegration, ScmIntegrationRegistry, @@ -83,11 +84,11 @@ export class ScmIntegrations implements ScmIntegrationRegistry { } resolveUrl(options: { url: string; base: string }): string { - const resolve = this.byUrl(options.base)?.resolveUrl; - if (!resolve) { - return new URL(options.url, options.base).toString(); + const integration = this.byUrl(options.base); + if (!integration) { + return defaultScmResolveUrl(options); } - return resolve(options); + return integration.resolveUrl(options); } } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index 8a10e40bcf..3f9e165b90 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -58,6 +58,16 @@ describe('AzureIntegration', () => { 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml', ); + expect( + integration.resolveUrl({ + url: '/a.yaml', + base: + 'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml', + }), + ).toBe( + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml', + ); + expect( integration.resolveUrl({ url: './a.yaml', diff --git a/packages/integration/src/azure/index.ts b/packages/integration/src/azure/index.ts index 6d57437779..2d95ef19b9 100644 --- a/packages/integration/src/azure/index.ts +++ b/packages/integration/src/azure/index.ts @@ -14,14 +14,15 @@ * limitations under the License. */ +export { AzureIntegration } from './AzureIntegration'; export { readAzureIntegrationConfig, readAzureIntegrationConfigs, } from './config'; export type { AzureIntegrationConfig } from './config'; export { + getAzureCommitsUrl, getAzureDownloadUrl, getAzureFileFetchUrl, getAzureRequestOptions, - getAzureCommitsUrl, } from './core'; diff --git a/packages/integration/src/bitbucket/BitbucketIntegration.ts b/packages/integration/src/bitbucket/BitbucketIntegration.ts index f3e69b946a..10b69877e4 100644 --- a/packages/integration/src/bitbucket/BitbucketIntegration.ts +++ b/packages/integration/src/bitbucket/BitbucketIntegration.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { basicIntegrations } from '../helpers'; +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { BitbucketIntegrationConfig, @@ -47,4 +47,8 @@ export class BitbucketIntegration implements ScmIntegration { get config(): BitbucketIntegrationConfig { return this.integrationConfig; } + + resolveUrl(options: { url: string; base: string }): string { + return defaultScmResolveUrl(options); + } } diff --git a/packages/integration/src/bitbucket/index.ts b/packages/integration/src/bitbucket/index.ts index 9df4d3d3fe..124fe4a2a2 100644 --- a/packages/integration/src/bitbucket/index.ts +++ b/packages/integration/src/bitbucket/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +export { BitbucketIntegration } from './BitbucketIntegration'; export { readBitbucketIntegrationConfig, readBitbucketIntegrationConfigs, diff --git a/packages/integration/src/github/GitHubIntegration.ts b/packages/integration/src/github/GitHubIntegration.ts index c103597d74..c60ab462c9 100644 --- a/packages/integration/src/github/GitHubIntegration.ts +++ b/packages/integration/src/github/GitHubIntegration.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { basicIntegrations } from '../helpers'; +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitHubIntegrationConfig, @@ -45,4 +45,8 @@ export class GitHubIntegration implements ScmIntegration { get config(): GitHubIntegrationConfig { return this.integrationConfig; } + + resolveUrl(options: { url: string; base: string }): string { + return defaultScmResolveUrl(options); + } } diff --git a/packages/integration/src/github/index.ts b/packages/integration/src/github/index.ts index 6491e8dcc5..99d2f56d0f 100644 --- a/packages/integration/src/github/index.ts +++ b/packages/integration/src/github/index.ts @@ -21,3 +21,4 @@ export { export type { GitHubIntegrationConfig } from './config'; export { getGitHubFileFetchUrl, getGitHubRequestOptions } from './core'; export { GithubCredentialsProvider } from './GithubCredentialsProvider'; +export { GitHubIntegration } from './GitHubIntegration'; diff --git a/packages/integration/src/gitlab/GitLabIntegration.ts b/packages/integration/src/gitlab/GitLabIntegration.ts index d939917366..131825edb5 100644 --- a/packages/integration/src/gitlab/GitLabIntegration.ts +++ b/packages/integration/src/gitlab/GitLabIntegration.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { basicIntegrations } from '../helpers'; +import { basicIntegrations, defaultScmResolveUrl } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { GitLabIntegrationConfig, @@ -45,4 +45,8 @@ export class GitLabIntegration implements ScmIntegration { get config(): GitLabIntegrationConfig { return this.integrationConfig; } + + resolveUrl(options: { url: string; base: string }): string { + return defaultScmResolveUrl(options); + } } diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 6836854eae..8e48871423 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -50,6 +50,21 @@ describe('gitlab core', () => { describe('getGitLabFileFetchUrl', () => { it.each([ // Project URLs + { + config: configWithNoToken, + url: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + }, + { + config: configWithNoToken, + // Works with non URI encoded link + url: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file with spaces.yaml', + result: + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile%20with%20spaces.yaml/raw?ref=branch', + }, { config: configWithNoToken, url: diff --git a/packages/integration/src/gitlab/index.ts b/packages/integration/src/gitlab/index.ts index 8dc4e90764..8886e0e3ce 100644 --- a/packages/integration/src/gitlab/index.ts +++ b/packages/integration/src/gitlab/index.ts @@ -20,3 +20,4 @@ export { } from './config'; export type { GitLabIntegrationConfig } from './config'; export { getGitLabFileFetchUrl, getGitLabRequestOptions } from './core'; +export { GitLabIntegration } from './GitLabIntegration'; diff --git a/packages/integration/src/helpers.test.ts b/packages/integration/src/helpers.test.ts index 9f4c531ba6..2899f2ed98 100644 --- a/packages/integration/src/helpers.test.ts +++ b/packages/integration/src/helpers.test.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { isValidHost } from './helpers'; +import { defaultScmResolveUrl, isValidHost } from './helpers'; describe('isValidHost', () => { it.each([ @@ -51,3 +51,69 @@ describe('isValidHost', () => { expect(isValidHost(str)).toBe(expected); }); }); + +describe('defaultScmResolveUrl', () => { + it('works for relative paths and retains query params', () => { + expect( + defaultScmResolveUrl({ + url: './b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml', + ); + + expect( + defaultScmResolveUrl({ + url: './b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml?at=master', + ); + + expect( + defaultScmResolveUrl({ + url: 'b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/b.yaml', + ); + }); + + it('works for absolute paths and retains query params', () => { + expect( + defaultScmResolveUrl({ + url: '/other/b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml', + ); + + expect( + defaultScmResolveUrl({ + url: '/other/b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master', + }), + ).toBe( + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/other/b.yaml?at=master', + ); + }); + + it('works for full urls and throws away query params', () => { + expect( + defaultScmResolveUrl({ + url: 'https://b.com/b.yaml', + base: + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/folder/a.yaml?at=master', + }), + ).toBe('https://b.com/b.yaml'); + }); +}); diff --git a/packages/integration/src/helpers.ts b/packages/integration/src/helpers.ts index 73cef18297..c09f808011 100644 --- a/packages/integration/src/helpers.ts +++ b/packages/integration/src/helpers.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import parseGitUrl from 'git-url-parse'; import { ScmIntegration, ScmIntegrationsGroup } from './types'; /** Checks whether the given argument is a valid URL hostname */ @@ -51,3 +52,43 @@ export function basicIntegrations( }, }; } + +/** + * Default implementation of ScmIntegration.resolveUrl, that only works with + * URL pathname based providers. + */ +export function defaultScmResolveUrl(options: { + url: string; + base: string; +}): string { + const { url, base } = options; + + // If it is a fully qualified URL - then return it verbatim + try { + // eslint-disable-next-line no-new + new URL(url); + return url; + } catch { + // ignore intentionally + } + + let updated: URL; + + if (url.startsWith('/')) { + // If it is an absolute path, move relative to the repo root + const { filepath } = parseGitUrl(base); + updated = new URL(base); + const repoRootPath = updated.pathname + .substring(0, updated.pathname.length - filepath.length) + .replace(/\/+$/, ''); + updated.pathname = `${repoRootPath}${url}`; + } else { + // For relative URLs, just let the default URL constructor handle the + // resolving. Note that this essentially will treat the last segment of the + // base as a file - NOT a folder - unless the url ends in a slash. + updated = new URL(url, base); + } + + updated.search = new URL(base).search; + return updated.toString(); +} diff --git a/packages/integration/src/index.ts b/packages/integration/src/index.ts index fdcd7da676..c39608fe8e 100644 --- a/packages/integration/src/index.ts +++ b/packages/integration/src/index.ts @@ -18,5 +18,6 @@ export * from './azure'; export * from './bitbucket'; export * from './github'; export * from './gitlab'; +export { defaultScmResolveUrl } from './helpers'; export { ScmIntegrations } from './ScmIntegrations'; export type { ScmIntegration, ScmIntegrationRegistry } from './types'; diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index 7d5b0fbe74..c63a323dc3 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -36,16 +36,21 @@ export interface ScmIntegration { title: string; /** - * Works like the two-argument form of the URL constructor, resolving an - * absolute or relative URL in relation to a base URL. + * Resolves an absolute or relative URL in relation to a base URL. * - * If this method is not implemented, the URL constructor is used instead for - * URLs that match this integration. + * This method is adapted for use within SCM systems, so relative URLs are + * within the context of the root of the hierarchy pointed to by the base + * URL. + * + * For example, if the base URL is `/folder/a.yaml`, i.e. + * within the file tree of a certain repo, an absolute path of `/b.yaml` does + * not resolve to `https://hostname/b.yaml` but rather to + * `/b.yaml` inside the file tree of that same repo. * * @param options.url The (absolute or relative) URL or path to resolve * @param options.base The base URL onto which this resolution happens */ - resolveUrl?(options: { url: string; base: string }): string; + resolveUrl(options: { url: string; base: string }): string; } /** @@ -83,8 +88,16 @@ export interface ScmIntegrationRegistry gitlab: ScmIntegrationsGroup; /** - * Works like the two-argument form of the URL constructor, resolving an - * absolute or relative URL in relation to a base URL. + * Resolves an absolute or relative URL in relation to a base URL. + * + * This method is adapted for use within SCM systems, so relative URLs are + * within the context of the root of the hierarchy pointed to by the base + * URL. + * + * For example, if the base URL is `/folder/a.yaml`, i.e. + * within the file tree of a certain repo, an absolute path of `/b.yaml` does + * not resolve to `https://hostname/b.yaml` but rather to + * `/b.yaml` inside the file tree of that same repo. * * @param options.url The (absolute or relative) URL or path to resolve * @param options.base The base URL onto which this resolution happens diff --git a/packages/techdocs-common/__mocks__/aws-sdk.ts b/packages/techdocs-common/__mocks__/aws-sdk.ts index 987b3d2800..f0fb13c642 100644 --- a/packages/techdocs-common/__mocks__/aws-sdk.ts +++ b/packages/techdocs-common/__mocks__/aws-sdk.ts @@ -71,11 +71,7 @@ export class S3 { return { promise: () => new Promise((resolve, reject) => { - if (!fs.existsSync(Key)) { - reject(''); - } else { - resolve(''); - } + resolve(''); }), }; } diff --git a/packages/techdocs-common/src/helpers.ts b/packages/techdocs-common/src/helpers.ts index beff0b2a6e..ec58bb7b40 100644 --- a/packages/techdocs-common/src/helpers.ts +++ b/packages/techdocs-common/src/helpers.ts @@ -133,11 +133,11 @@ export const checkoutGitRepository = async ( switch (type) { case 'github': git = Git.fromAuth({ - username: token, - password: 'x-oauth-basic', + username: 'x-access-token', + password: token, logger, }); - parsedGitLocation.token = `${token}:x-oauth-basic`; + parsedGitLocation.token = `x-access-token:${token}`; break; case 'gitlab': git = Git.fromAuth({ diff --git a/packages/techdocs-common/src/stages/generate/helpers.test.ts b/packages/techdocs-common/src/stages/generate/helpers.test.ts index 4d62648a7d..c4bf102610 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.test.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.test.ts @@ -13,22 +13,24 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import fs from 'fs-extra'; -import os from 'os'; -import { resolve as resolvePath } from 'path'; -import Stream, { PassThrough } from 'stream'; +import { getVoidLogger } from '@backstage/backend-common'; import Docker from 'dockerode'; +import fs from 'fs-extra'; import mockFs from 'mock-fs'; -import * as winston from 'winston'; -import { - runDockerContainer, - getGeneratorKey, - isValidRepoUrlForMkdocs, - getRepoUrlFromLocationAnnotation, - patchMkdocsYmlPreBuild, -} from './helpers'; -import { RemoteProtocol } from '../prepare/types'; +import os from 'os'; +import path, { resolve as resolvePath } from 'path'; +import Stream, { PassThrough } from 'stream'; import { ParsedLocationAnnotation } from '../../helpers'; +import { RemoteProtocol } from '../prepare/types'; +import { + addBuildTimestampMetadata, + getGeneratorKey, + getRepoUrlFromLocationAnnotation, + isValidRepoUrlForMkdocs, + patchMkdocsYmlPreBuild, + runDockerContainer, + storeEtagMetadata, +} from './helpers'; const mockEntity = { apiVersion: 'version', @@ -46,7 +48,8 @@ const mkdocsYml = fs.readFileSync( const mkdocsYmlWithRepoUrl = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_repo_url.yml'), ); -const mockLogger = winston.createLogger(); +const mockLogger = getVoidLogger(); +const rootDir = os.platform() === 'win32' ? 'C:\\rootDir' : '/rootDir'; describe('helpers', () => { describe('getGeneratorKey', () => { @@ -329,4 +332,80 @@ describe('helpers', () => { ); }); }); + + describe('addBuildTimestampMetadata', () => { + beforeEach(() => { + mockFs.restore(); + mockFs({ + [rootDir]: { + 'invalid_techdocs_metadata.json': 'dsds', + 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should create the file if it does not exist', async () => { + const filePath = path.join(rootDir, 'wrong_techdocs_metadata.json'); + await addBuildTimestampMetadata(filePath, mockLogger); + + // Check if the file exists + await expect( + fs.access(filePath, fs.constants.F_OK), + ).resolves.not.toThrowError(); + }); + + it('should throw error when the JSON is invalid', async () => { + const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json'); + + await expect( + addBuildTimestampMetadata(filePath, mockLogger), + ).rejects.toThrowError('Unexpected token d in JSON at position 0'); + }); + + it('should add build timestamp to the metadata json', async () => { + const filePath = path.join(rootDir, 'techdocs_metadata.json'); + + await addBuildTimestampMetadata(filePath, mockLogger); + + const json = await fs.readJson(filePath); + expect(json.build_timestamp).toBeLessThanOrEqual(Date.now()); + }); + }); + + describe('storeEtagMetadata', () => { + beforeEach(() => { + mockFs.restore(); + mockFs({ + [rootDir]: { + 'invalid_techdocs_metadata.json': 'dsds', + 'techdocs_metadata.json': '{"site_name": "Tech Docs"}', + }, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should throw error when the JSON is invalid', async () => { + const filePath = path.join(rootDir, 'invalid_techdocs_metadata.json'); + + await expect( + storeEtagMetadata(filePath, 'etag123abc'), + ).rejects.toThrowError('Unexpected token d in JSON at position 0'); + }); + + it('should add etag to the metadata json', async () => { + const filePath = path.join(rootDir, 'techdocs_metadata.json'); + + await storeEtagMetadata(filePath, 'etag123abc'); + + const json = await fs.readJson(filePath); + expect(json.etag).toBe('etag123abc'); + }); + }); }); diff --git a/packages/techdocs-common/src/stages/generate/helpers.ts b/packages/techdocs-common/src/stages/generate/helpers.ts index e396157b92..32da5d19e8 100644 --- a/packages/techdocs-common/src/stages/generate/helpers.ts +++ b/packages/techdocs-common/src/stages/generate/helpers.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import fs from 'fs-extra'; -import { spawn } from 'child_process'; -import { Writable, PassThrough } from 'stream'; -import Docker from 'dockerode'; -import yaml from 'js-yaml'; -import { Logger } from 'winston'; import { Entity } from '@backstage/catalog-model'; -import { SupportedGeneratorKey } from './types'; +import { spawn } from 'child_process'; +import Docker from 'dockerode'; +import fs from 'fs-extra'; +import yaml from 'js-yaml'; +import { PassThrough, Writable } from 'stream'; +import { Logger } from 'winston'; import { ParsedLocationAnnotation } from '../../helpers'; import { RemoteProtocol } from '../prepare/types'; +import { SupportedGeneratorKey } from './types'; // TODO: Implement proper support for more generators. export function getGeneratorKey(entity: Entity): SupportedGeneratorKey { @@ -275,3 +275,51 @@ export const patchMkdocsYmlPreBuild = async ( return; } }; + +/** + * Update the techdocs_metadata.json to add a new build timestamp metadata. Create the .json file if it doesn't exist. + * + * @param {string} techdocsMetadataPath File path to techdocs_metadata.json + */ +export const addBuildTimestampMetadata = async ( + techdocsMetadataPath: string, + logger: Logger, +): Promise => { + // check if file exists, create if it does not. + try { + await fs.access(techdocsMetadataPath, fs.constants.F_OK); + } catch (err) { + // Bootstrap file with empty JSON + await fs.writeJson(techdocsMetadataPath, JSON.parse('{}')); + } + // check if valid Json + let json; + try { + json = await fs.readJson(techdocsMetadataPath); + } catch (err) { + const message = `Invalid JSON at ${techdocsMetadataPath} with error ${err.message}`; + logger.error(message); + throw new Error(message); + } + + json.build_timestamp = Date.now(); + await fs.writeJson(techdocsMetadataPath, json); + return; +}; + +/** + * Update the techdocs_metadata.json to add etag of the prepared tree (e.g. commit SHA or actual Etag of the resource). + * This is helpful to check if a TechDocs site in storage has gone outdated, without maintaining an in-memory build info + * per Backstage instance. + * + * @param {string} techdocsMetadataPath File path to techdocs_metadata.json + * @param {string} etag + */ +export const storeEtagMetadata = async ( + techdocsMetadataPath: string, + etag: string, +): Promise => { + const json = await fs.readJson(techdocsMetadataPath); + json.etag = etag; + await fs.writeJson(techdocsMetadataPath, json); +}; diff --git a/packages/techdocs-common/src/stages/generate/techdocs.ts b/packages/techdocs-common/src/stages/generate/techdocs.ts index 507b9da6b8..133755ecd2 100644 --- a/packages/techdocs-common/src/stages/generate/techdocs.ts +++ b/packages/techdocs-common/src/stages/generate/techdocs.ts @@ -14,17 +14,18 @@ * limitations under the License. */ -import path from 'path'; -import { Logger } from 'winston'; -import { PassThrough } from 'stream'; import { Config } from '@backstage/config'; - -import { GeneratorBase, GeneratorRunOptions } from './types'; +import path from 'path'; +import { PassThrough } from 'stream'; +import { Logger } from 'winston'; import { - runDockerContainer, - runCommand, + addBuildTimestampMetadata, patchMkdocsYmlPreBuild, + runCommand, + runDockerContainer, + storeEtagMetadata, } from './helpers'; +import { GeneratorBase, GeneratorRunOptions } from './types'; type TechdocsGeneratorOptions = { // This option enables users to configure if they want to use TechDocs container @@ -62,6 +63,7 @@ export class TechdocsGenerator implements GeneratorBase { outputDir, dockerClient, parsedLocationAnnotation, + etag, }: GeneratorRunOptions): Promise { const [log, logStream] = createStream(); @@ -118,5 +120,25 @@ export class TechdocsGenerator implements GeneratorBase { `Failed to generate docs from ${inputDir} into ${outputDir} with error ${error.message}`, ); } + + /** + * Post Generate steps + */ + + // Add build timestamp to techdocs_metadata.json + // Creates techdocs_metadata.json if file does not exist. + await addBuildTimestampMetadata( + path.join(outputDir, 'techdocs_metadata.json'), + this.logger, + ); + + // Add etag of the prepared tree to techdocs_metadata.json + // Assumes that the file already exists. + if (etag) { + await storeEtagMetadata( + path.join(outputDir, 'techdocs_metadata.json'), + etag, + ); + } } } diff --git a/packages/techdocs-common/src/stages/generate/types.ts b/packages/techdocs-common/src/stages/generate/types.ts index 7724107dac..fda41ed442 100644 --- a/packages/techdocs-common/src/stages/generate/types.ts +++ b/packages/techdocs-common/src/stages/generate/types.ts @@ -13,9 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Writable } from 'stream'; -import Docker from 'dockerode'; import { Entity } from '@backstage/catalog-model'; +import Docker from 'dockerode'; +import { Writable } from 'stream'; import { ParsedLocationAnnotation } from '../../helpers'; /** @@ -25,6 +25,7 @@ import { ParsedLocationAnnotation } from '../../helpers'; * @param {string} outputDir Directory to store generated docs in. Usually - a newly created temporary directory. * @param {Docker} dockerClient A docker client to run any generator on top of your directory * @param {ParsedLocationAnnotation} parsedLocationAnnotation backstage.io/techdocs-ref annotation of an entity + * @param {string} etag A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. * @param {Writable} [logStream] A dedicated log stream */ export type GeneratorRunOptions = { @@ -32,6 +33,7 @@ export type GeneratorRunOptions = { outputDir: string; dockerClient: Docker; parsedLocationAnnotation?: ParsedLocationAnnotation; + etag?: string; logStream?: Writable; }; diff --git a/packages/techdocs-common/src/stages/publish/awsS3.test.ts b/packages/techdocs-common/src/stages/publish/awsS3.test.ts index 813f6b8554..f8450a07a0 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.test.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.test.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import type { Entity, EntityName } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; import mockFs from 'mock-fs'; import path from 'path'; import * as winston from 'winston'; -import { ConfigReader } from '@backstage/config'; import { AwsS3Publish } from './awsS3'; import { PublisherBase, TechDocsMetadata } from './types'; -import type { Entity, EntityName } from '@backstage/catalog-model'; const createMockEntity = (annotations = {}): Entity => { return { @@ -127,9 +127,9 @@ describe('AwsS3Publish', () => { directory: wrongPathToGeneratedDirectory, }) .catch(error => - expect(error).toEqual( - new Error( - `Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory '${wrongPathToGeneratedDirectory}'`, + expect(error.message).toEqual( + expect.stringContaining( + 'Unable to upload file(s) to AWS S3. Error Failed to read template directory: ENOENT, no such file or directory', ), ), ); @@ -205,16 +205,12 @@ describe('AwsS3Publish', () => { 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 ${entityRootDir}/techdocs_metadata.json doest not exist !`, - ), + expect(error.message).toEqual( + expect.stringContaining('TechDocs metadata fetch'), ), ); }); diff --git a/packages/techdocs-common/src/stages/publish/awsS3.ts b/packages/techdocs-common/src/stages/publish/awsS3.ts index 28c545c420..52d5647e71 100644 --- a/packages/techdocs-common/src/stages/publish/awsS3.ts +++ b/packages/techdocs-common/src/stages/publish/awsS3.ts @@ -13,20 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import path from 'path'; -import express from 'express'; -import aws, { Credentials } from 'aws-sdk'; -import { ManagedUpload } from 'aws-sdk/clients/s3'; -import { Logger } from 'winston'; import { Entity, EntityName } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; -import { getHeadersForFileExtension, getFileTreeRecursively } from './helpers'; -import { PublisherBase, PublishRequest, TechDocsMetadata } from './types'; +import aws from 'aws-sdk'; +import { ManagedUpload } from 'aws-sdk/clients/s3'; +import express from 'express'; import fs from 'fs-extra'; -import { Readable } from 'stream'; import JSON5 from 'json5'; import createLimiter from 'p-limit'; import { CredentialsOptions } from 'aws-sdk/lib/credentials'; +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) => { diff --git a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx b/plugins/api-docs/src/components/ApisCards/ApisTable.tsx deleted file mode 100644 index 30716a61fb..0000000000 --- a/plugins/api-docs/src/components/ApisCards/ApisTable.tsx +++ /dev/null @@ -1,151 +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 { - ApiEntity, - EntityName, - RELATION_OWNED_BY, - RELATION_PART_OF, -} from '@backstage/catalog-model'; -import { Table, TableColumn } from '@backstage/core'; -import { - EntityRefLink, - EntityRefLinks, - formatEntityRefTitle, - getEntityRelations, -} from '@backstage/plugin-catalog-react'; -import React from 'react'; -import { ApiTypeTitle } from '../ApiDefinitionCard'; - -type EntityRow = { - entity: ApiEntity; - resolved: { - partOfSystemRelationTitle?: string; - partOfSystemRelations: EntityName[]; - ownedByRelationsTitle?: string; - ownedByRelations: EntityName[]; - }; -}; - -const columns: TableColumn[] = [ - { - title: 'Name', - field: 'entity.metadata.name', - highlight: true, - render: ({ entity }) => ( - {entity.metadata.name} - ), - }, - { - title: 'System', - field: 'resolved.partOfSystemRelationTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Owner', - field: 'resolved.ownedByRelationsTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Lifecycle', - field: 'entity.spec.lifecycle', - }, - { - title: 'Type', - field: 'entity.spec.type', - render: ({ entity }) => , - }, - { - title: 'Description', - field: 'entity.metadata.description', - width: 'auto', - }, -]; - -type Props = { - title: string; - variant?: string; - entities: (ApiEntity | undefined)[]; -}; - -export const ApisTable = ({ entities, title, variant = 'gridItem' }: Props) => { - const tableStyle: React.CSSProperties = { - minWidth: '0', - width: '100%', - }; - - if (variant === 'gridItem') { - tableStyle.height = 'calc(100% - 10px)'; - } - - const rows = entities - // TODO: For now we skip all APIs that we can't find without a warning! - .filter(e => e !== undefined) - .map(entity => { - const partOfSystemRelations = getEntityRelations( - entity, - RELATION_PART_OF, - { - kind: 'system', - }, - ); - const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); - - return { - entity: entity as ApiEntity, - resolved: { - ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) - .join(', '), - ownedByRelations, - partOfSystemRelationTitle: partOfSystemRelations - .map(r => - formatEntityRefTitle(r, { - defaultKind: 'system', - }), - ) - .join(', '), - partOfSystemRelations, - }, - }; - }); - - return ( - - columns={columns} - title={title} - style={tableStyle} - options={{ - // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px; - search: false, - paging: false, - actionsColumnIndex: -1, - padding: 'dense', - }} - data={rows} - /> - ); -}; diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index 051b9588a0..5d2978a198 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -14,12 +14,7 @@ * limitations under the License. */ -import { - Entity, - RELATION_CONSUMES_API, - RELATION_OWNED_BY, - RELATION_PART_OF, -} from '@backstage/catalog-model'; +import { Entity, RELATION_CONSUMES_API } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; import { CatalogApi, @@ -79,7 +74,7 @@ describe('', () => { ); expect(getByText(/Consumed APIs/i)).toBeInTheDocument(); - expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument(); + expect(getByText(/No Component consumes this API/i)).toBeInTheDocument(); }); it('shows consumed APIs', async () => { @@ -108,34 +103,7 @@ describe('', () => { name: 'target-name', namespace: 'my-namespace', }, - spec: { - type: 'openapi', - lifecycle: 'production', - definition: '...', - }, - relations: [ - { - type: RELATION_PART_OF, - target: { - kind: 'System', - name: 'MySystem', - namespace: 'default', - }, - }, - { - type: RELATION_OWNED_BY, - target: { - kind: 'Group', - name: 'Test', - namespace: 'default', - }, - }, - ], - }); - apiDocsConfig.getApiDefinitionWidget.mockReturnValue({ - type: 'openapi', - title: 'OpenAPI', - component: () =>
, + spec: {}, }); const { getByText } = await renderInTestApp( @@ -147,12 +115,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText(/Consumed APIs/i)).toBeInTheDocument(); + expect(getByText('Consumed APIs')).toBeInTheDocument(); expect(getByText(/target-name/i)).toBeInTheDocument(); - expect(getByText(/OpenAPI/)).toBeInTheDocument(); - expect(getByText(/Test/i)).toBeInTheDocument(); - expect(getByText(/MySystem/i)).toBeInTheDocument(); - expect(getByText(/production/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx index 8a808a6488..adfa3cd34c 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx @@ -19,70 +19,67 @@ import { Entity, RELATION_CONSUMES_API, } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { EmptyState, InfoCard, Progress } from '@backstage/core'; -import React, { PropsWithChildren } from 'react'; -import { ApisTable } from './ApisTable'; -import { MissingConsumesApisEmptyState } from '../EmptyState'; -import { useRelatedEntities } from '../useRelatedEntities'; - -const ApisCard = ({ - children, - variant = 'gridItem', -}: PropsWithChildren<{ variant?: string }>) => { - return ( - - {children} - - ); -}; +import { + CodeSnippet, + InfoCard, + Link, + Progress, + WarningPanel, +} from '@backstage/core'; +import { + EntityTable, + useEntity, + useRelatedEntities, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { apiEntityColumns } from './presets'; type Props = { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; - variant?: string; + variant?: 'gridItem'; }; export const ConsumedApisCard = ({ variant = 'gridItem' }: Props) => { const { entity } = useEntity(); - const { entities, loading, error } = useRelatedEntities( - entity, - RELATION_CONSUMES_API, - ); + const { entities, loading, error } = useRelatedEntities(entity, { + type: RELATION_CONSUMES_API, + }); if (loading) { return ( - + - + ); } - if (error) { + if (error || !entities) { return ( - - + } /> - - ); - } - - if (!entities || entities.length === 0) { - return ( - - - + ); } return ( - + No Component consumes this API.{' '} + + Learn how to consume APIs. + +
+ } + columns={apiEntityColumns} + entities={entities as ApiEntity[]} /> ); }; diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx new file mode 100644 index 0000000000..7b97f0867e --- /dev/null +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.test.tsx @@ -0,0 +1,122 @@ +/* + * 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, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, +} from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { ApiDocsConfig, apiDocsConfigRef } from '../../config'; +import { HasApisCard } from './HasApisCard'; + +describe('', () => { + const apiDocsConfig: jest.Mocked = { + getApiDefinitionWidget: jest.fn(), + } as any; + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( + apiDocsConfigRef, + apiDocsConfig, + ); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'System', + metadata: { + name: 'my-system', + namespace: 'my-namespace', + }, + relations: [], + }; + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText('APIs')).toBeInTheDocument(); + expect(getByText(/No API is part of this system/i)).toBeInTheDocument(); + }); + + it('shows related APIs', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'System', + metadata: { + name: 'my-system', + namespace: 'my-namespace', + }, + relations: [ + { + target: { + kind: 'API', + namespace: 'my-namespace', + name: 'target-name', + }, + type: RELATION_HAS_PART, + }, + ], + }; + catalogApi.getEntityByName.mockResolvedValue({ + apiVersion: 'v1', + kind: 'API', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: {}, + }); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + await waitFor(() => { + expect(getByText('APIs')).toBeInTheDocument(); + expect(getByText(/target-name/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx new file mode 100644 index 0000000000..9f1d487b65 --- /dev/null +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx @@ -0,0 +1,80 @@ +/* + * 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 { ApiEntity, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { + CodeSnippet, + InfoCard, + Link, + Progress, + WarningPanel, +} from '@backstage/core'; +import { + EntityTable, + useEntity, + useRelatedEntities, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { apiEntityColumns } from './presets'; + +type Props = { + variant?: 'gridItem'; +}; + +export const HasApisCard = ({ variant = 'gridItem' }: Props) => { + const { entity } = useEntity(); + const { entities, loading, error } = useRelatedEntities(entity, { + type: RELATION_HAS_PART, + kind: 'API', + }); + + if (loading) { + return ( + + + + ); + } + + if (error || !entities) { + return ( + + } + /> + + ); + } + + return ( + + No API is part of this system.{' '} + + Learn how to add APIs. + +
+ } + columns={apiEntityColumns} + entities={entities as ApiEntity[]} + /> + ); +}; diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index 482534f58a..0d49cc328d 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -14,12 +14,7 @@ * limitations under the License. */ -import { - Entity, - RELATION_OWNED_BY, - RELATION_PART_OF, - RELATION_PROVIDES_API, -} from '@backstage/catalog-model'; +import { Entity, RELATION_PROVIDES_API } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; import { CatalogApi, @@ -79,7 +74,7 @@ describe('', () => { ); expect(getByText(/Provided APIs/i)).toBeInTheDocument(); - expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument(); + expect(getByText(/No component provides this API/i)).toBeInTheDocument(); }); it('shows consumed APIs', async () => { @@ -108,34 +103,7 @@ describe('', () => { name: 'target-name', namespace: 'my-namespace', }, - spec: { - type: 'openapi', - lifecycle: 'production', - definition: '...', - }, - relations: [ - { - type: RELATION_PART_OF, - target: { - kind: 'System', - name: 'MySystem', - namespace: 'default', - }, - }, - { - type: RELATION_OWNED_BY, - target: { - kind: 'Group', - name: 'Test', - namespace: 'default', - }, - }, - ], - }); - apiDocsConfig.getApiDefinitionWidget.mockReturnValue({ - type: 'openapi', - title: 'OpenAPI', - component: () =>
, + spec: {}, }); const { getByText } = await renderInTestApp( @@ -149,10 +117,6 @@ describe('', () => { await waitFor(() => { expect(getByText(/Provided APIs/i)).toBeInTheDocument(); expect(getByText(/target-name/i)).toBeInTheDocument(); - expect(getByText(/OpenAPI/)).toBeInTheDocument(); - expect(getByText(/MySystem/)).toBeInTheDocument(); - expect(getByText(/Test/i)).toBeInTheDocument(); - expect(getByText(/production/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx index ed6e893a03..f02390b6d6 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx @@ -19,70 +19,67 @@ import { Entity, RELATION_PROVIDES_API, } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { EmptyState, InfoCard, Progress } from '@backstage/core'; -import React, { PropsWithChildren } from 'react'; -import { ApisTable } from './ApisTable'; -import { MissingProvidesApisEmptyState } from '../EmptyState'; -import { useRelatedEntities } from '../useRelatedEntities'; - -const ApisCard = ({ - children, - variant = 'gridItem', -}: PropsWithChildren<{ variant?: string }>) => { - return ( - - {children} - - ); -}; +import { + CodeSnippet, + InfoCard, + Link, + Progress, + WarningPanel, +} from '@backstage/core'; +import { + EntityTable, + useEntity, + useRelatedEntities, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { apiEntityColumns } from './presets'; type Props = { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; - variant?: string; + variant?: 'gridItem'; }; export const ProvidedApisCard = ({ variant = 'gridItem' }: Props) => { const { entity } = useEntity(); - const { entities, loading, error } = useRelatedEntities( - entity, - RELATION_PROVIDES_API, - ); + const { entities, loading, error } = useRelatedEntities(entity, { + type: RELATION_PROVIDES_API, + }); if (loading) { return ( - + - + ); } - if (error) { + if (error || !entities) { return ( - - + } /> - - ); - } - - if (!entities || entities.length === 0) { - return ( - - - + ); } return ( - + No component provides this API.{' '} + + Learn how to provide APIs. + +
+ } + columns={apiEntityColumns} + entities={entities as ApiEntity[]} /> ); }; diff --git a/plugins/api-docs/src/components/ApisCards/index.ts b/plugins/api-docs/src/components/ApisCards/index.ts index 2a01a1dc6e..9243bdedd2 100644 --- a/plugins/api-docs/src/components/ApisCards/index.ts +++ b/plugins/api-docs/src/components/ApisCards/index.ts @@ -15,4 +15,5 @@ */ export { ConsumedApisCard } from './ConsumedApisCard'; +export { HasApisCard } from './HasApisCard'; export { ProvidedApisCard } from './ProvidedApisCard'; diff --git a/plugins/api-docs/src/components/ApisCards/presets.tsx b/plugins/api-docs/src/components/ApisCards/presets.tsx new file mode 100644 index 0000000000..151cccf846 --- /dev/null +++ b/plugins/api-docs/src/components/ApisCards/presets.tsx @@ -0,0 +1,42 @@ +/* + * 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 { ApiEntity } from '@backstage/catalog-model'; +import { TableColumn } from '@backstage/core'; +import { EntityTable } from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { ApiTypeTitle } from '../ApiDefinitionCard'; + +export function createSpecApiTypeColumn(): TableColumn { + return { + title: 'Type', + field: 'spec.type', + render: entity => , + }; +} + +// TODO: This could be moved to plugin-catalog-react if we wouldn't have a +// special createSpecApiTypeColumn. But this is required to use ApiTypeTitle to +// resolve the display name of an entity. Is the display name really worth it? + +export const apiEntityColumns: TableColumn[] = [ + EntityTable.columns.createEntityRefColumn({ defaultKind: 'API' }), + EntityTable.columns.createSystemColumn(), + EntityTable.columns.createOwnerColumn(), + EntityTable.columns.createSpecLifecycleColumn(), + createSpecApiTypeColumn(), + EntityTable.columns.createMetadataDescriptionColumn(), +]; diff --git a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx b/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx deleted file mode 100644 index bd9b0c765f..0000000000 --- a/plugins/api-docs/src/components/ComponentsCards/ComponentsTable.tsx +++ /dev/null @@ -1,154 +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 { - ComponentEntity, - EntityName, - RELATION_OWNED_BY, - RELATION_PART_OF, -} from '@backstage/catalog-model'; -import { Table, TableColumn } from '@backstage/core'; -import { - EntityRefLink, - EntityRefLinks, - formatEntityRefTitle, - getEntityRelations, -} from '@backstage/plugin-catalog-react'; -import React from 'react'; - -type EntityRow = { - entity: ComponentEntity; - resolved: { - partOfSystemRelationTitle?: string; - partOfSystemRelations: EntityName[]; - ownedByRelationsTitle?: string; - ownedByRelations: EntityName[]; - }; -}; - -const columns: TableColumn[] = [ - { - title: 'Name', - field: 'entity.metadata.name', - highlight: true, - render: ({ entity }) => ( - {entity.metadata.name} - ), - }, - { - title: 'System', - field: 'resolved.partOfSystemRelationTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Owner', - field: 'resolved.ownedByRelationsTitle', - render: ({ resolved }) => ( - - ), - }, - { - title: 'Lifecycle', - field: 'entity.spec.lifecycle', - }, - { - title: 'Type', - field: 'entity.spec.type', - }, - { - title: 'Description', - field: 'entity.metadata.description', - width: 'auto', - }, -]; - -type Props = { - title: string; - variant?: string; - entities: (ComponentEntity | undefined)[]; -}; - -// TODO: In theory this could also be systems! -export const ComponentsTable = ({ - entities, - title, - variant = 'gridItem', -}: Props) => { - const tableStyle: React.CSSProperties = { - minWidth: '0', - width: '100%', - }; - - if (variant === 'gridItem') { - tableStyle.height = 'calc(100% - 10px)'; - } - - const rows = entities - // TODO: For now we skip all Components that we can't find without a warning! - .filter(e => e !== undefined) - .map(entity => { - const partOfSystemRelations = getEntityRelations( - entity, - RELATION_PART_OF, - { - kind: 'system', - }, - ); - const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); - - return { - entity: entity as ComponentEntity, - resolved: { - ownedByRelationsTitle: ownedByRelations - .map(r => formatEntityRefTitle(r, { defaultKind: 'group' })) - .join(', '), - ownedByRelations, - partOfSystemRelationTitle: partOfSystemRelations - .map(r => - formatEntityRefTitle(r, { - defaultKind: 'system', - }), - ) - .join(', '), - partOfSystemRelations, - }, - }; - }); - - return ( - - columns={columns} - title={title} - style={tableStyle} - options={{ - // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px; - search: false, - paging: false, - actionsColumnIndex: -1, - padding: 'dense', - }} - data={rows} - /> - ); -}; diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx index f45a48842d..7944b3f90a 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.test.tsx @@ -14,12 +14,7 @@ * limitations under the License. */ -import { - Entity, - RELATION_API_CONSUMED_BY, - RELATION_OWNED_BY, - RELATION_PART_OF, -} from '@backstage/catalog-model'; +import { Entity, RELATION_API_CONSUMED_BY } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; import { CatalogApi, @@ -77,8 +72,8 @@ describe('', () => { , ); - expect(getByText(/Consumers/i)).toBeInTheDocument(); - expect(getByText(/No APIs consumed by this entity/i)).toBeInTheDocument(); + expect(getByText('Consumers')).toBeInTheDocument(); + expect(getByText(/No component consumes this API/i)).toBeInTheDocument(); }); it('shows consuming components', async () => { @@ -113,28 +108,7 @@ describe('', () => { name: 'target-name', namespace: 'my-namespace', }, - spec: { - type: 'service', - lifecycle: 'production', - }, - relations: [ - { - type: RELATION_PART_OF, - target: { - kind: 'System', - name: 'MySystem', - namespace: 'default', - }, - }, - { - type: RELATION_OWNED_BY, - target: { - kind: 'Group', - name: 'Test', - namespace: 'default', - }, - }, - ], + spec: {}, }); const { getByText } = await renderInTestApp( @@ -146,11 +120,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText(/Consumers/i)).toBeInTheDocument(); + expect(getByText('Consumers')).toBeInTheDocument(); expect(getByText(/target-name/i)).toBeInTheDocument(); - expect(getByText(/Test/i)).toBeInTheDocument(); - expect(getByText(/MySystem/i)).toBeInTheDocument(); - expect(getByText(/production/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx index 56d2fb977d..7e4d97fece 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx @@ -19,70 +19,66 @@ import { Entity, RELATION_API_CONSUMED_BY, } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { EmptyState, InfoCard, Progress } from '@backstage/core'; -import React, { PropsWithChildren } from 'react'; -import { MissingConsumesApisEmptyState } from '../EmptyState'; -import { useRelatedEntities } from '../useRelatedEntities'; -import { ComponentsTable } from './ComponentsTable'; - -const ComponentsCard = ({ - children, - variant = 'gridItem', -}: PropsWithChildren<{ variant?: string }>) => { - return ( - - {children} - - ); -}; +import { + CodeSnippet, + InfoCard, + Link, + Progress, + WarningPanel, +} from '@backstage/core'; +import { + EntityTable, + useEntity, + useRelatedEntities, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; type Props = { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; - variant?: string; + variant?: 'gridItem'; }; export const ConsumingComponentsCard = ({ variant = 'gridItem' }: Props) => { const { entity } = useEntity(); - const { entities, loading, error } = useRelatedEntities( - entity, - RELATION_API_CONSUMED_BY, - ); + const { entities, loading, error } = useRelatedEntities(entity, { + type: RELATION_API_CONSUMED_BY, + }); if (loading) { return ( - + - + ); } - if (error) { + if (error || !entities) { return ( - - + } /> - - ); - } - - if (!entities || entities.length === 0) { - return ( - - - + ); } return ( - + No component consumes this API.{' '} + + Learn how to consume APIs. + + + } + columns={EntityTable.componentEntityColumns} + entities={entities as ComponentEntity[]} /> ); }; diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx index 4c55393d13..ab1751b9ba 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.test.tsx @@ -14,12 +14,7 @@ * limitations under the License. */ -import { - Entity, - RELATION_API_PROVIDED_BY, - RELATION_OWNED_BY, - RELATION_PART_OF, -} from '@backstage/catalog-model'; +import { Entity, RELATION_API_PROVIDED_BY } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; import { CatalogApi, @@ -77,8 +72,8 @@ describe('', () => { , ); - expect(getByText(/Providers/i)).toBeInTheDocument(); - expect(getByText(/No APIs provided by this entity/i)).toBeInTheDocument(); + expect(getByText('Providers')).toBeInTheDocument(); + expect(getByText(/No component provides this API/i)).toBeInTheDocument(); }); it('shows providing components', async () => { @@ -113,28 +108,7 @@ describe('', () => { name: 'target-name', namespace: 'my-namespace', }, - spec: { - type: 'service', - lifecycle: 'production', - }, - relations: [ - { - type: RELATION_PART_OF, - target: { - kind: 'System', - name: 'MySystem', - namespace: 'default', - }, - }, - { - type: RELATION_OWNED_BY, - target: { - kind: 'Group', - name: 'Test', - namespace: 'default', - }, - }, - ], + spec: {}, }); const { getByText } = await renderInTestApp( @@ -146,11 +120,8 @@ describe('', () => { ); await waitFor(() => { - expect(getByText(/Providers/i)).toBeInTheDocument(); + expect(getByText('Providers')).toBeInTheDocument(); expect(getByText(/target-name/i)).toBeInTheDocument(); - expect(getByText(/Test/i)).toBeInTheDocument(); - expect(getByText(/MySystem/i)).toBeInTheDocument(); - expect(getByText(/production/i)).toBeInTheDocument(); }); }); }); diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx index 959ff3feed..d755d8b5af 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx @@ -19,70 +19,66 @@ import { Entity, RELATION_API_PROVIDED_BY, } from '@backstage/catalog-model'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { EmptyState, InfoCard, Progress } from '@backstage/core'; -import React, { PropsWithChildren } from 'react'; -import { MissingProvidesApisEmptyState } from '../EmptyState'; -import { useRelatedEntities } from '../useRelatedEntities'; -import { ComponentsTable } from './ComponentsTable'; - -const ComponentsCard = ({ - children, - variant = 'gridItem', -}: PropsWithChildren<{ variant?: string }>) => { - return ( - - {children} - - ); -}; +import { + CodeSnippet, + InfoCard, + Link, + Progress, + WarningPanel, +} from '@backstage/core'; +import { + EntityTable, + useEntity, + useRelatedEntities, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; type Props = { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; - variant?: string; + variant?: 'gridItem'; }; export const ProvidingComponentsCard = ({ variant = 'gridItem' }: Props) => { const { entity } = useEntity(); - const { entities, loading, error } = useRelatedEntities( - entity, - RELATION_API_PROVIDED_BY, - ); + const { entities, loading, error } = useRelatedEntities(entity, { + type: RELATION_API_PROVIDED_BY, + }); if (loading) { return ( - + - + ); } - if (error) { + if (error || !entities) { return ( - - + } /> - - ); - } - - if (!entities || entities.length === 0) { - return ( - - - + ); } return ( - + No component provides this API.{' '} + + Learn how to provide APIs. + + + } + columns={EntityTable.componentEntityColumns} + entities={entities as ComponentEntity[]} /> ); }; diff --git a/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx b/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx deleted file mode 100644 index 3e71168dde..0000000000 --- a/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.tsx +++ /dev/null @@ -1,81 +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 React from 'react'; -import { Button, makeStyles, Typography } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; -import { CodeSnippet, EmptyState } from '@backstage/core'; - -const COMPONENT_YAML = `# Example -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: example -spec: - type: service - lifecycle: production - owner: guest - consumesApis: - - example-api -`; - -const useStyles = makeStyles(theme => ({ - code: { - borderRadius: 6, - margin: `${theme.spacing(2)}px 0px`, - background: theme.palette.type === 'dark' ? '#444' : '#fff', - }, -})); - -export const MissingConsumesApisEmptyState = () => { - const classes = useStyles(); - return ( - - Components can consume APIs that are displayed on this page. You need - to fill the consumesApis field to enable this tool. - - } - action={ - <> - - Link an API to your component as shown in the highlighted example - below: - -
- -
- - - } - /> - ); -}; diff --git a/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx b/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx deleted file mode 100644 index 9bf3465a34..0000000000 --- a/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.tsx +++ /dev/null @@ -1,81 +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 React from 'react'; -import { Button, makeStyles, Typography } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; -import { CodeSnippet, EmptyState } from '@backstage/core'; - -const COMPONENT_YAML = `# Example -apiVersion: backstage.io/v1alpha1 -kind: Component -metadata: - name: example -spec: - type: service - lifecycle: production - owner: guest - providesApis: - - example-api -`; - -const useStyles = makeStyles(theme => ({ - code: { - borderRadius: 6, - margin: `${theme.spacing(2)}px 0px`, - background: theme.palette.type === 'dark' ? '#444' : '#fff', - }, -})); - -export const MissingProvidesApisEmptyState = () => { - const classes = useStyles(); - return ( - - Components can implement APIs that are displayed on this page. You - need to fill the providesApis field to enable this tool. - - } - action={ - <> - - Link an API to your component as shown in the highlighted example - below: - -
- -
- - - } - /> - ); -}; diff --git a/plugins/api-docs/src/index.ts b/plugins/api-docs/src/index.ts index e7484f77c3..8bab7f28f4 100644 --- a/plugins/api-docs/src/index.ts +++ b/plugins/api-docs/src/index.ts @@ -24,4 +24,5 @@ export { EntityConsumingComponentsCard, EntityProvidedApisCard, EntityProvidingComponentsCard, + EntityHasApisCard, } from './plugin'; diff --git a/plugins/api-docs/src/plugin.ts b/plugins/api-docs/src/plugin.ts index 429fd60095..1d57cc6cc2 100644 --- a/plugins/api-docs/src/plugin.ts +++ b/plugins/api-docs/src/plugin.ts @@ -106,3 +106,11 @@ export const EntityProvidingComponentsCard = apiDocsPlugin.provide( }, }), ); + +export const EntityHasApisCard = apiDocsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./components/ApisCards').then(m => m.HasApisCard), + }, + }), +); diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c50789d4c1..281ea063fd 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -34,6 +34,7 @@ "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", + "@types/passport": "^1.0.3", "compression": "^1.7.4", "cookie-parser": "^1.4.5", "cors": "^2.8.5", @@ -70,7 +71,6 @@ "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", "@types/jwt-decode": "^3.1.0", - "@types/passport": "^1.0.3", "@types/passport-github2": "^1.2.4", "@types/passport-google-oauth20": "^2.0.3", "@types/passport-microsoft": "^0.0.0", diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts index e53102e333..dd2941a3d5 100644 --- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts +++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts @@ -39,7 +39,7 @@ type Options = { const parseDate = (date: string | Date) => { const parsedDate = typeof date === 'string' - ? DateTime.fromSQL(date, { locale: 'UTC' }) + ? DateTime.fromSQL(date, { zone: 'UTC' }) : DateTime.fromJSDate(date); if (!parsedDate.isValid) { diff --git a/plugins/auth-backend/src/lib/oauth/index.ts b/plugins/auth-backend/src/lib/oauth/index.ts index 564a0e1e7c..bf3e0658e4 100644 --- a/plugins/auth-backend/src/lib/oauth/index.ts +++ b/plugins/auth-backend/src/lib/oauth/index.ts @@ -25,4 +25,5 @@ export type { OAuthState, OAuthStartRequest, OAuthRefreshRequest, + OAuthResult, } from './types'; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index b2b7915a4e..dd477388bf 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -15,6 +15,7 @@ */ import express from 'express'; +import { Profile as PassportProfile } from 'passport'; import { AuthResponse, RedirectInfo } from '../../providers/types'; /** @@ -35,6 +36,17 @@ export type OAuthProviderOptions = { callbackUrl: string; }; +export type OAuthResult = { + fullProfile: PassportProfile; + params: { + id_token?: string; + scope: string; + expires_in: number; + }; + accessToken: string; + refreshToken?: string; +}; + export type OAuthResponse = AuthResponse; export type OAuthProviderInfo = { diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts index f7a22e9ad9..c27eebbd9a 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.test.ts @@ -87,7 +87,7 @@ describe('PassportStrategyHelper', () => { expect(spyAuthenticate).toBeCalledTimes(1); await expect(frameHandlerStrategyPromise).resolves.toStrictEqual( expect.objectContaining({ - response: { accessToken: 'ACCESS_TOKEN' }, + result: { accessToken: 'ACCESS_TOKEN' }, privateInfo: { refreshToken: 'REFRESH_TOKEN' }, }), ); diff --git a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts index 28f15ee9e8..8819da0a2f 100644 --- a/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/passport/PassportStrategyHelper.ts @@ -39,7 +39,7 @@ export const makeProfileInfo = ( } let picture: string | undefined = undefined; - if (profile.photos) { + if (profile.photos && profile.photos.length > 0) { const [firstPhoto] = profile.photos; picture = firstPhoto.value; } @@ -80,15 +80,15 @@ export const executeRedirectStrategy = async ( }); }; -export const executeFrameHandlerStrategy = async ( +export const executeFrameHandlerStrategy = async ( req: express.Request, providerStrategy: passport.Strategy, ) => { - return new Promise<{ response: T; privateInfo: PrivateInfo }>( + return new Promise<{ result: Result; privateInfo: PrivateInfo }>( (resolve, reject) => { const strategy = Object.create(providerStrategy); - strategy.success = (response: any, privateInfo: any) => { - resolve({ response, privateInfo }); + strategy.success = (result: any, privateInfo: any) => { + resolve({ result, privateInfo }); }; strategy.fail = ( info: { type: 'success' | 'error'; message?: string }, @@ -190,19 +190,17 @@ type ProviderStrategy = { export const executeFetchUserProfileStrategy = async ( providerStrategy: passport.Strategy, accessToken: string, - idToken?: string, -): Promise => { +): Promise => { return new Promise((resolve, reject) => { const anyStrategy = (providerStrategy as unknown) as ProviderStrategy; anyStrategy.userProfile( accessToken, - (error: Error, passportProfile: passport.Profile) => { + (error: Error, rawProfile: passport.Profile) => { if (error) { reject(error); + } else { + resolve(rawProfile); } - - const profile = makeProfileInfo(passportProfile, idToken); - resolve(profile); }, ); }); diff --git a/plugins/auth-backend/src/providers/auth0/index.ts b/plugins/auth-backend/src/providers/auth0/index.ts index 87c9aceaa4..480552a91d 100644 --- a/plugins/auth-backend/src/providers/auth0/index.ts +++ b/plugins/auth-backend/src/providers/auth0/index.ts @@ -15,3 +15,4 @@ */ export { createAuth0Provider } from './provider'; +export type { Auth0ProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/auth0/provider.ts b/plugins/auth-backend/src/providers/auth0/provider.ts index 8772842e2a..2a2f743d12 100644 --- a/plugins/auth-backend/src/providers/auth0/provider.ts +++ b/plugins/auth-backend/src/providers/auth0/provider.ts @@ -26,6 +26,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -61,20 +62,16 @@ export class Auth0AuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + fullProfile, + accessToken, + refreshToken, + params, }, { refreshToken, @@ -96,13 +93,23 @@ export class Auth0AuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -114,11 +121,11 @@ export class Auth0AuthProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(fullProfile, params.id_token); return this.populateIdentity({ providerInfo: { @@ -148,28 +155,29 @@ export class Auth0AuthProvider implements OAuthHandlers { } } -export const createAuth0Provider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const domain = envConfig.getString('domain'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type Auth0ProviderOptions = {}; - const provider = new Auth0AuthProvider({ - clientId, - clientSecret, - callbackUrl, - domain, - }); +export const createAuth0Provider = ( + _options?: Auth0ProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const domain = envConfig.getString('domain'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - providerId, - tokenIssuer, + const provider = new Auth0AuthProvider({ + clientId, + clientSecret, + callbackUrl, + domain, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/aws-alb/index.ts b/plugins/auth-backend/src/providers/aws-alb/index.ts index f8b5c9e5d7..cd6d41f580 100644 --- a/plugins/auth-backend/src/providers/aws-alb/index.ts +++ b/plugins/auth-backend/src/providers/aws-alb/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { createAwsAlbProvider } from './provider'; +export type { AwsAlbProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/aws-alb/provider.ts b/plugins/auth-backend/src/providers/aws-alb/provider.ts index 61ea10947e..27baaded0f 100644 --- a/plugins/auth-backend/src/providers/aws-alb/provider.ts +++ b/plugins/auth-backend/src/providers/aws-alb/provider.ts @@ -106,22 +106,26 @@ export class AwsAlbAuthProvider implements AuthProviderRouteHandlers { } } -export const createAwsAlbProvider = ({ - logger, - catalogApi, - config, - identityResolver, -}: AuthProviderFactoryOptions) => { - const region = config.getString('region'); - const issuer = config.getOptionalString('iss'); - if (identityResolver !== undefined) { - return new AwsAlbAuthProvider(logger, catalogApi, { - region, - issuer, - identityResolutionCallback: identityResolver, - }); - } - throw new Error( - 'Identity resolver is required to use this authentication provider', - ); +export type AwsAlbProviderOptions = {}; + +export const createAwsAlbProvider = (_options?: AwsAlbProviderOptions) => { + return ({ + logger, + catalogApi, + config, + identityResolver, + }: AuthProviderFactoryOptions) => { + const region = config.getString('region'); + const issuer = config.getOptionalString('iss'); + if (identityResolver !== undefined) { + return new AwsAlbAuthProvider(logger, catalogApi, { + region, + issuer, + identityResolutionCallback: identityResolver, + }); + } + throw new Error( + 'Identity resolver is required to use this authentication provider', + ); + }; }; diff --git a/plugins/auth-backend/src/providers/factories.ts b/plugins/auth-backend/src/providers/factories.ts index 619fb1c706..670dc84eec 100644 --- a/plugins/auth-backend/src/providers/factories.ts +++ b/plugins/auth-backend/src/providers/factories.ts @@ -28,15 +28,15 @@ import { AuthProviderFactory } from './types'; import { createAwsAlbProvider } from './aws-alb'; export const factories: { [providerId: string]: AuthProviderFactory } = { - google: createGoogleProvider, - github: createGithubProvider, - gitlab: createGitlabProvider, - saml: createSamlProvider, - okta: createOktaProvider, - auth0: createAuth0Provider, - microsoft: createMicrosoftProvider, - oauth2: createOAuth2Provider, - oidc: createOidcProvider, - onelogin: createOneLoginProvider, - awsalb: createAwsAlbProvider, + google: createGoogleProvider(), + github: createGithubProvider(), + gitlab: createGitlabProvider(), + saml: createSamlProvider(), + okta: createOktaProvider(), + auth0: createAuth0Provider(), + microsoft: createMicrosoftProvider(), + oauth2: createOAuth2Provider(), + oidc: createOidcProvider(), + onelogin: createOneLoginProvider(), + awsalb: createAwsAlbProvider(), }; diff --git a/plugins/auth-backend/src/providers/github/index.ts b/plugins/auth-backend/src/providers/github/index.ts index 60ad6998b7..f2ba73c2b9 100644 --- a/plugins/auth-backend/src/providers/github/index.ts +++ b/plugins/auth-backend/src/providers/github/index.ts @@ -15,3 +15,4 @@ */ export { createGithubProvider } from './provider'; +export type { GithubProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/github/provider.test.ts b/plugins/auth-backend/src/providers/github/provider.test.ts index 61d4bb887f..6394ed9f2a 100644 --- a/plugins/auth-backend/src/providers/github/provider.test.ts +++ b/plugins/auth-backend/src/providers/github/provider.test.ts @@ -14,13 +14,31 @@ * limitations under the License. */ +import { Profile as PassportProfile } from 'passport'; import { GithubAuthProvider } from './provider'; +import * as helpers from '../../lib/passport'; +import { OAuthResult } from '../../lib/oauth'; + +const mockFrameHandler = (jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown) as jest.MockedFunction< + () => Promise<{ + result: Omit & { params: { scope: string } }; + }> +>; describe('GithubAuthProvider', () => { + const provider = new GithubAuthProvider({ + callbackUrl: 'mock', + clientId: 'mock', + clientSecret: 'mock', + }); + describe('should transform to type OAuthResponse', () => { - it('when all fields are present, it should be able to map them', () => { + it('when all fields are present, it should be able to map them', async () => { const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const rawProfile = { + const fullProfile = { id: 'uid-123', username: 'jimmymarkum', provider: 'github', @@ -59,18 +77,17 @@ describe('GithubAuthProvider', () => { 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', }, }; - expect( - GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ), - ).toEqual(expected); + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile, accessToken, params }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); }); - it('when "email" is missing, it should be able to create the profile without it', () => { + it('when "email" is missing, it should be able to create the profile without it', async () => { const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const rawProfile = { + const fullProfile = ({ id: 'uid-123', username: 'jimmymarkum', provider: 'github', @@ -82,7 +99,7 @@ describe('GithubAuthProvider', () => { 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', }, ], - }; + } as unknown) as PassportProfile; const params = { scope: 'read:scope', @@ -105,18 +122,16 @@ describe('GithubAuthProvider', () => { }, }; - expect( - GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ), - ).toEqual(expected); + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile, accessToken, params }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); }); - it('when "displayName" is missing, it should be able to create the profile and map "displayName" with "username"', () => { + it('when "displayName" is missing, it should be able to create the profile and map "displayName" with "username"', async () => { const accessToken = '19xasczxcm9n7gacn9jdgm19me'; - const rawProfile = { + const fullProfile = ({ id: 'uid-123', username: 'jimmymarkum', provider: 'github', @@ -128,7 +143,7 @@ describe('GithubAuthProvider', () => { 'https://a1cf74336522e87f135f-2f21ace9a6cf0052456644b80fa06d4f.ssl.cf2.rackcdn.com/images/characters_opt/p-mystic-river-sean-penn.jpg', }, ], - }; + } as unknown) as PassportProfile; const params = { scope: 'read:scope', @@ -150,19 +165,17 @@ describe('GithubAuthProvider', () => { }, }; - expect( - GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ), - ).toEqual(expected); + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile, accessToken, params }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); }); - it('when "photos" is missing, it should be able to create the profile without it', () => { + it('when "photos" is missing, it should be able to create the profile without it', async () => { const accessToken = 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe'; - const rawProfile = { + const fullProfile = { id: 'ipd12039', username: 'daveboyle', provider: 'gitlab', @@ -195,13 +208,11 @@ describe('GithubAuthProvider', () => { }, }; - expect( - GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ), - ).toEqual(expected); + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile, accessToken, params }, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); }); }); }); diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 6e3f9a6000..438cbca07b 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -27,12 +27,11 @@ import { OAuthAdapter, OAuthProviderOptions, OAuthHandlers, - OAuthResponse, OAuthEnvironmentHandler, OAuthStartRequest, encodeState, + OAuthResult, } from '../../lib/oauth'; -import passport from 'passport'; export type GithubAuthProviderOptions = OAuthProviderOptions & { tokenUrl?: string; @@ -43,55 +42,6 @@ export type GithubAuthProviderOptions = OAuthProviderOptions & { export class GithubAuthProvider implements OAuthHandlers { private readonly _strategy: GithubStrategy; - static transformPassportProfile(rawProfile: any): passport.Profile { - const profile: passport.Profile = { - id: rawProfile.username, - username: rawProfile.username, - provider: rawProfile.provider, - displayName: rawProfile.displayName || rawProfile.username, - photos: rawProfile.photos, - emails: rawProfile.emails, - }; - - return profile; - } - - static transformOAuthResponse( - accessToken: string, - rawProfile: any, - params: any = {}, - ): OAuthResponse { - const passportProfile = GithubAuthProvider.transformPassportProfile( - rawProfile, - ); - - const profile = makeProfileInfo(passportProfile, params.id_token); - const providerInfo = { - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - idToken: params.id_token, - }; - - // GitHub provides an id numeric value (123) - // as a fallback - const id = passportProfile!.id; - - if (params.expires_in) { - providerInfo.expiresInSeconds = params.expires_in; - } - if (params.id_token) { - providerInfo.idToken = params.id_token; - } - return { - providerInfo, - profile, - backstageIdentity: { - id, - }, - }; - } - constructor(options: GithubAuthProviderOptions) { this._strategy = new GithubStrategy( { @@ -104,17 +54,12 @@ export class GithubAuthProvider implements OAuthHandlers { }, ( accessToken: any, - _: any, + refreshToken: any, params: any, - rawProfile: any, - done: PassportDoneCallback, + fullProfile: any, + done: PassportDoneCallback, ) => { - const oauthResponse = GithubAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ); - done(undefined, oauthResponse); + done(undefined, { fullProfile, params, accessToken, refreshToken }); }, ); } @@ -127,51 +72,73 @@ export class GithubAuthProvider implements OAuthHandlers { } async handler(req: express.Request) { - const { response } = await executeFrameHandlerStrategy( - req, - this._strategy, + const { + result: { fullProfile, accessToken, params }, + } = await executeFrameHandlerStrategy(req, this._strategy); + + const profile = makeProfileInfo( + { + ...fullProfile, + id: fullProfile.username || fullProfile.id, + displayName: + fullProfile.displayName || fullProfile.username || fullProfile.id, + }, + params.id_token, ); - return { response }; + return { + response: { + profile, + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + }, + backstageIdentity: { + id: fullProfile.username || fullProfile.id, + }, + }, + }; } } -export const createGithubProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const enterpriseInstanceUrl = envConfig.getOptionalString( - 'enterpriseInstanceUrl', - ); - const authorizationUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/authorize` - : undefined; - const tokenUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/login/oauth/access_token` - : undefined; - const userProfileUrl = enterpriseInstanceUrl - ? `${enterpriseInstanceUrl}/api/v3/user` - : undefined; - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type GithubProviderOptions = {}; - const provider = new GithubAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenUrl, - userProfileUrl, - authorizationUrl, - }); +export const createGithubProvider = ( + _options?: GithubProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const enterpriseInstanceUrl = envConfig.getOptionalString( + 'enterpriseInstanceUrl', + ); + const authorizationUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/authorize` + : undefined; + const tokenUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/login/oauth/access_token` + : undefined; + const userProfileUrl = enterpriseInstanceUrl + ? `${enterpriseInstanceUrl}/api/v3/user` + : undefined; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - persistScopes: true, - providerId, - tokenIssuer, + const provider = new GithubAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenUrl, + userProfileUrl, + authorizationUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + persistScopes: true, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/gitlab/index.ts b/plugins/auth-backend/src/providers/gitlab/index.ts index 7fba2dd95c..0a516021cd 100644 --- a/plugins/auth-backend/src/providers/gitlab/index.ts +++ b/plugins/auth-backend/src/providers/gitlab/index.ts @@ -15,3 +15,4 @@ */ export { createGitlabProvider } from './provider'; +export type { GitlabProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index d4d1de17ec..b57d846f23 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -15,14 +15,21 @@ */ import { GitlabAuthProvider } from './provider'; +import * as helpers from '../../lib/passport'; +import { OAuthResult } from '../../lib/oauth'; + +const mockFrameHandler = (jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown) as jest.MockedFunction<() => Promise<{ result: OAuthResult }>>; describe('GitlabAuthProvider', () => { - it('should transform to type OAuthResponse', () => { + it('should transform to type OAuthResponse', async () => { const tests = [ { - arguments: { + result: { accessToken: '19xasczxcm9n7gacn9jdgm19me', - rawProfile: { + fullProfile: { id: 'uid-123', username: 'jimmymarkum', provider: 'gitlab', @@ -58,10 +65,10 @@ describe('GitlabAuthProvider', () => { }, }, { - arguments: { + result: { accessToken: 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', - rawProfile: { + fullProfile: { id: 'ipd12039', username: 'daveboyle', provider: 'gitlab', @@ -74,6 +81,7 @@ describe('GitlabAuthProvider', () => { }, params: { scope: 'read_repository', + expires_in: 200, }, }, expect: { @@ -83,6 +91,7 @@ describe('GitlabAuthProvider', () => { providerInfo: { accessToken: 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', + expiresInSeconds: 200, scope: 'read_repository', }, profile: { @@ -93,14 +102,16 @@ describe('GitlabAuthProvider', () => { }, ]; + const provider = new GitlabAuthProvider({ + clientId: 'mock', + clientSecret: 'mock', + callbackUrl: 'mock', + baseUrl: 'mock', + }); for (const test of tests) { - expect( - GitlabAuthProvider.transformOAuthResponse( - test.arguments.accessToken, - test.arguments.rawProfile, - test.arguments.params, - ), - ).toEqual(test.expect); + mockFrameHandler.mockResolvedValueOnce({ result: test.result }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(test.expect); } }); }); diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index df6d2daeb7..7c24757833 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -31,8 +31,8 @@ import { OAuthEnvironmentHandler, OAuthStartRequest, encodeState, + OAuthResult, } from '../../lib/oauth'; -import passport from 'passport'; export type GitlabAuthProviderOptions = OAuthProviderOptions & { baseUrl: string; @@ -41,64 +41,6 @@ export type GitlabAuthProviderOptions = OAuthProviderOptions & { export class GitlabAuthProvider implements OAuthHandlers { private readonly _strategy: GitlabStrategy; - static transformPassportProfile(rawProfile: any): passport.Profile { - const profile: passport.Profile = { - id: rawProfile.id, - username: rawProfile.username, - provider: rawProfile.provider, - displayName: rawProfile.displayName, - }; - - if (rawProfile.emails && rawProfile.emails.length > 0) { - profile.emails = rawProfile.emails; - } - if (rawProfile.avatarUrl) { - profile.photos = [{ value: rawProfile.avatarUrl }]; - } - - return profile; - } - - static transformOAuthResponse( - accessToken: string, - rawProfile: any, - params: any = {}, - ): OAuthResponse { - const passportProfile = GitlabAuthProvider.transformPassportProfile( - rawProfile, - ); - - const profile = makeProfileInfo(passportProfile, params.id_token); - const providerInfo = { - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - idToken: params.id_token, - }; - - // gitlab provides an id numeric value (123) - // as a fallback - let id = passportProfile!.id; - - if (profile.email) { - id = profile.email.split('@')[0]; - } - - if (params.expires_in) { - providerInfo.expiresInSeconds = params.expires_in; - } - if (params.id_token) { - providerInfo.idToken = params.id_token; - } - return { - providerInfo, - profile, - backstageIdentity: { - id, - }, - }; - } - constructor(options: GitlabAuthProviderOptions) { this._strategy = new GitlabStrategy( { @@ -109,17 +51,12 @@ export class GitlabAuthProvider implements OAuthHandlers { }, ( accessToken: any, - _: any, + refreshToken: any, params: any, - rawProfile: any, - done: PassportDoneCallback, + fullProfile: any, + done: PassportDoneCallback, ) => { - const oauthResponse = GitlabAuthProvider.transformOAuthResponse( - accessToken, - rawProfile, - params, - ); - done(undefined, oauthResponse); + done(undefined, { fullProfile, params, accessToken, refreshToken }); }, ); } @@ -132,36 +69,74 @@ export class GitlabAuthProvider implements OAuthHandlers { } async handler(req: express.Request): Promise<{ response: OAuthResponse }> { - return await executeFrameHandlerStrategy( + const { result } = await executeFrameHandlerStrategy( req, this._strategy, ); + const { accessToken, params } = result; + const fullProfile = result.fullProfile as OAuthResult['fullProfile'] & { + avatarUrl?: string; + }; + + const profile = makeProfileInfo( + { + ...fullProfile, + photos: [ + ...(fullProfile.photos ?? []), + ...(fullProfile.avatarUrl ? [{ value: fullProfile.avatarUrl }] : []), + ], + }, + params.id_token, + ); + + // gitlab provides an id numeric value (123) + // as a fallback + let id = fullProfile.id; + if (profile.email) { + id = profile.email.split('@')[0]; + } + + return { + response: { + profile, + providerInfo: { + accessToken, + scope: params.scope, + expiresInSeconds: params.expires_in, + idToken: params.id_token, + }, + backstageIdentity: { + id, + }, + }, + }; } } -export const createGitlabProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const baseUrl = audience || 'https://gitlab.com'; - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type GitlabProviderOptions = {}; - const provider = new GitlabAuthProvider({ - clientId, - clientSecret, - callbackUrl, - baseUrl, - }); +export const createGitlabProvider = ( + _options?: GitlabProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const baseUrl = audience || 'https://gitlab.com'; + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: true, - providerId, - tokenIssuer, + const provider = new GitlabAuthProvider({ + clientId, + clientSecret, + callbackUrl, + baseUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: true, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/google/index.ts b/plugins/auth-backend/src/providers/google/index.ts index b2cd85e6de..2615a2d8e5 100644 --- a/plugins/auth-backend/src/providers/google/index.ts +++ b/plugins/auth-backend/src/providers/google/index.ts @@ -15,3 +15,4 @@ */ export { createGoogleProvider } from './provider'; +export type { GoogleProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 848a02bad6..a00644a90d 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -28,6 +28,7 @@ import { OAuthRefreshRequest, OAuthResponse, OAuthStartRequest, + OAuthResult, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -44,7 +45,7 @@ type PrivateInfo = { refreshToken: string; }; -export type GoogleAuthProviderOptions = OAuthProviderOptions & { +type Options = OAuthProviderOptions & { logger: Logger; identityClient: CatalogIdentityClient; tokenIssuer: TokenIssuer; @@ -56,7 +57,7 @@ export class GoogleAuthProvider implements OAuthHandlers { private readonly identityClient: CatalogIdentityClient; private readonly tokenIssuer: TokenIssuer; - constructor(options: GoogleAuthProviderOptions) { + constructor(options: Options) { this.logger = options.logger; this.identityClient = options.identityClient; this.tokenIssuer = options.tokenIssuer; @@ -74,20 +75,16 @@ export class GoogleAuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + fullProfile, + params, + accessToken, + refreshToken, }, { refreshToken, @@ -109,13 +106,23 @@ export class GoogleAuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + }), refreshToken: privateInfo.refreshToken, }; } @@ -127,11 +134,11 @@ export class GoogleAuthProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(fullProfile, params.id_token); return this.populateIdentity({ providerInfo: { @@ -184,31 +191,37 @@ export class GoogleAuthProvider implements OAuthHandlers { } } -export const createGoogleProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - logger, - tokenIssuer, - catalogApi, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type GoogleProviderOptions = {}; - const provider = new GoogleAuthProvider({ - clientId, - clientSecret, - callbackUrl, - logger, - tokenIssuer, - identityClient: new CatalogIdentityClient({ catalogApi }), - }); +export const createGoogleProvider = ( + _options?: GoogleProviderOptions, +): AuthProviderFactory => { + return ({ + providerId, + globalConfig, + config, + logger, + tokenIssuer, + catalogApi, + }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new GoogleAuthProvider({ + clientId, + clientSecret, + callbackUrl, + logger, + tokenIssuer, + identityClient: new CatalogIdentityClient({ catalogApi }), + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/microsoft/index.ts b/plugins/auth-backend/src/providers/microsoft/index.ts index 2e4abd2d2c..f6c2ce40cd 100644 --- a/plugins/auth-backend/src/providers/microsoft/index.ts +++ b/plugins/auth-backend/src/providers/microsoft/index.ts @@ -15,3 +15,4 @@ */ export { createMicrosoftProvider } from './provider'; +export type { MicrosoftProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/microsoft/provider.ts b/plugins/auth-backend/src/providers/microsoft/provider.ts index b0c3635be8..4dee90d4c6 100644 --- a/plugins/auth-backend/src/providers/microsoft/provider.ts +++ b/plugins/auth-backend/src/providers/microsoft/provider.ts @@ -38,6 +38,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import got from 'got'; @@ -54,32 +55,6 @@ export type MicrosoftAuthProviderOptions = OAuthProviderOptions & { export class MicrosoftAuthProvider implements OAuthHandlers { private readonly _strategy: MicrosoftStrategy; - static transformAuthResponse( - accessToken: string, - params: any, - rawProfile: any, - photoURL: any, - ): OAuthResponse { - let passportProfile: passport.Profile = rawProfile; - passportProfile = { - ...passportProfile, - photos: [{ value: photoURL }], - }; - - const profile = makeProfileInfo(passportProfile, params.id_token); - const providerInfo = { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }; - - return { - providerInfo, - profile, - }; - } - constructor(options: MicrosoftAuthProviderOptions) { this._strategy = new MicrosoftStrategy( { @@ -94,22 +69,10 @@ export class MicrosoftAuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - this.getUserPhoto(accessToken) - .then(photoURL => { - const authResponse = MicrosoftAuthProvider.transformAuthResponse( - accessToken, - params, - rawProfile, - photoURL, - ); - done(undefined, authResponse, { refreshToken }); - }) - .catch(error => { - throw new Error(`Error processing auth response: ${error}`); - }); + done(undefined, { fullProfile, accessToken, refreshToken, params }); }, ); } @@ -124,15 +87,37 @@ export class MicrosoftAuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); - return { - response: await this.populateIdentity(response), - refreshToken: privateInfo.refreshToken, - }; + try { + const photoUrl = await this.getUserPhoto(result.accessToken); + + const profile = makeProfileInfo( + { + ...result.fullProfile, + photos: photoUrl ? [{ value: photoUrl }] : undefined, + }, + result.params.id_token, + ); + + return { + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), + refreshToken: privateInfo.refreshToken, + }; + } catch (error) { + throw new Error(`Error processing auth response: ${error}`); + } } async refresh(req: OAuthRefreshRequest): Promise { @@ -142,11 +127,11 @@ export class MicrosoftAuthProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(fullProfile, params.id_token); const photo = await this.getUserPhoto(accessToken); if (photo) { profile.picture = photo; @@ -205,32 +190,33 @@ export class MicrosoftAuthProvider implements OAuthHandlers { } } -export const createMicrosoftProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const tenantID = envConfig.getString('tenantId'); +export type MicrosoftProviderOptions = {}; - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/authorize`; - const tokenUrl = `https://login.microsoftonline.com/${tenantID}/oauth2/v2.0/token`; +export const createMicrosoftProvider = ( + _options?: MicrosoftProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const tenantId = envConfig.getString('tenantId'); - const provider = new MicrosoftAuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`; + const tokenUrl = `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/token`; + + const provider = new MicrosoftAuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, - }); - }); +}; diff --git a/plugins/auth-backend/src/providers/oauth2/index.ts b/plugins/auth-backend/src/providers/oauth2/index.ts index e607aa10e1..97848f4319 100644 --- a/plugins/auth-backend/src/providers/oauth2/index.ts +++ b/plugins/auth-backend/src/providers/oauth2/index.ts @@ -15,3 +15,4 @@ */ export { createOAuth2Provider } from './provider'; +export type { OAuth2ProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/oauth2/provider.ts b/plugins/auth-backend/src/providers/oauth2/provider.ts index a44c4fb50b..fb347b836c 100644 --- a/plugins/auth-backend/src/providers/oauth2/provider.ts +++ b/plugins/auth-backend/src/providers/oauth2/provider.ts @@ -26,6 +26,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import { executeFetchUserProfileStrategy, @@ -65,21 +66,16 @@ export class OAuth2AuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); - done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + fullProfile, + accessToken, + refreshToken, + params, }, { refreshToken, @@ -101,13 +97,23 @@ export class OAuth2AuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -124,11 +130,11 @@ export class OAuth2AuthProvider implements OAuthHandlers { refreshToken: updatedRefreshToken, } = refreshTokenResponse; - const profile = await executeFetchUserProfileStrategy( + const rawProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(rawProfile, params.id_token); return this.populateIdentity({ providerInfo: { @@ -158,32 +164,33 @@ export class OAuth2AuthProvider implements OAuthHandlers { } } -export const createOAuth2Provider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const authorizationUrl = envConfig.getString('authorizationUrl'); - const tokenUrl = envConfig.getString('tokenUrl'); - const scope = envConfig.getOptionalString('scope'); +export type OAuth2ProviderOptions = {}; - const provider = new OAuth2AuthProvider({ - clientId, - clientSecret, - callbackUrl, - authorizationUrl, - tokenUrl, - scope, - }); +export const createOAuth2Provider = ( + _options?: OAuth2ProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = envConfig.getString('authorizationUrl'); + const tokenUrl = envConfig.getString('tokenUrl'); + const scope = envConfig.getOptionalString('scope'); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new OAuth2AuthProvider({ + clientId, + clientSecret, + callbackUrl, + authorizationUrl, + tokenUrl, + scope, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/oidc/index.ts b/plugins/auth-backend/src/providers/oidc/index.ts index acacbde73d..63c530acfc 100644 --- a/plugins/auth-backend/src/providers/oidc/index.ts +++ b/plugins/auth-backend/src/providers/oidc/index.ts @@ -15,3 +15,4 @@ */ export { createOidcProvider } from './provider'; +export type { OidcProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.test.ts b/plugins/auth-backend/src/providers/oidc/provider.test.ts index fcbe9d7abc..a746c0573b 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.test.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.test.ts @@ -121,7 +121,7 @@ describe('OidcAuthProvider', () => { })), } as any) as Config, } as AuthProviderFactoryOptions; - const provider = createOidcProvider(options) as OAuthAdapter; + const provider = createOidcProvider()(options) as OAuthAdapter; expect(provider.start).toBeDefined(); await new Promise(resolve => process.nextTick(resolve)); // advance a tick to give nock a chance to intercept the request expect(scope.isDone()).toBeTruthy(); diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 6ea7b8a8d3..0ec8d0bbf7 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -37,10 +37,10 @@ import { executeRedirectStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { RedirectInfo, AuthProviderFactory, ProfileInfo } from '../types'; +import { RedirectInfo, AuthProviderFactory } from '../types'; type PrivateInfo = { - refreshToken: string; + refreshToken?: string; }; type OidcImpl = { @@ -48,7 +48,12 @@ type OidcImpl = { client: Client; }; -export type OidcAuthProviderOptions = OAuthProviderOptions & { +type AuthResult = { + tokenset: TokenSet; + userinfo: UserinfoResponse; +}; + +export type Options = OAuthProviderOptions & { metadataUrl: string; tokenSignedResponseAlg?: string; }; @@ -56,7 +61,7 @@ export type OidcAuthProviderOptions = OAuthProviderOptions & { export class OidcAuthProvider implements OAuthHandlers { private readonly implementation: Promise; - constructor(options: OidcAuthProviderOptions) { + constructor(options: Options) { this.implementation = this.setupStrategy(options); } @@ -72,15 +77,30 @@ export class OidcAuthProvider implements OAuthHandlers { async handler( req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken: string }> { + ): Promise<{ response: OAuthResponse; refreshToken?: string }> { const { strategy } = await this.implementation; - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, - PrivateInfo - >(req, strategy); + const { + result: { userinfo, tokenset }, + privateInfo, + } = await executeFrameHandlerStrategy( + req, + strategy, + ); return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile: { + displayName: userinfo.name, + email: userinfo.email, + picture: userinfo.picture, + }, + providerInfo: { + idToken: tokenset.id_token, + accessToken: tokenset.access_token || '', + scope: tokenset.scope || '', + expiresInSeconds: tokenset.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -105,9 +125,7 @@ export class OidcAuthProvider implements OAuthHandlers { }); } - private async setupStrategy( - options: OidcAuthProviderOptions, - ): Promise { + private async setupStrategy(options: Options): Promise { const issuer = await Issuer.discover(options.metadataUrl); const client = new issuer.Client({ client_id: options.clientId, @@ -125,27 +143,13 @@ export class OidcAuthProvider implements OAuthHandlers { ( tokenset: TokenSet, userinfo: UserinfoResponse, - done: PassportDoneCallback, + done: PassportDoneCallback, ) => { - const profile: ProfileInfo = { - displayName: userinfo.name, - email: userinfo.email, - picture: userinfo.picture, - }; - done( undefined, + { tokenset, userinfo }, { - providerInfo: { - idToken: tokenset.id_token || '', - accessToken: tokenset.access_token || '', - scope: tokenset.scope || '', - expiresInSeconds: tokenset.expires_in, - }, - profile, - }, - { - refreshToken: tokenset.refresh_token || '', + refreshToken: tokenset.refresh_token, }, ); }, @@ -170,32 +174,33 @@ export class OidcAuthProvider implements OAuthHandlers { } } -export const createOidcProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - const metadataUrl = envConfig.getString('metadataUrl'); - const tokenSignedResponseAlg = envConfig.getString( - 'tokenSignedResponseAlg', - ); +export type OidcProviderOptions = {}; - const provider = new OidcAuthProvider({ - clientId, - clientSecret, - callbackUrl, - tokenSignedResponseAlg, - metadataUrl, - }); +export const createOidcProvider = ( + _options?: OidcProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const metadataUrl = envConfig.getString('metadataUrl'); + const tokenSignedResponseAlg = envConfig.getString( + 'tokenSignedResponseAlg', + ); - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new OidcAuthProvider({ + clientId, + clientSecret, + callbackUrl, + tokenSignedResponseAlg, + metadataUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/okta/index.ts b/plugins/auth-backend/src/providers/okta/index.ts index 05cc398f43..47a2549587 100644 --- a/plugins/auth-backend/src/providers/okta/index.ts +++ b/plugins/auth-backend/src/providers/okta/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export { createOktaProvider } from './provider'; +export type { OktaProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/okta/provider.ts b/plugins/auth-backend/src/providers/okta/provider.ts index 84b258e489..6320230e67 100644 --- a/plugins/auth-backend/src/providers/okta/provider.ts +++ b/plugins/auth-backend/src/providers/okta/provider.ts @@ -23,6 +23,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import { Strategy as OktaStrategy } from 'passport-okta-oauth'; import passport from 'passport'; @@ -80,21 +81,16 @@ export class OktaAuthProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); - done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + accessToken, + refreshToken, + params, + fullProfile, }, { refreshToken, @@ -116,13 +112,23 @@ export class OktaAuthProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -134,11 +140,11 @@ export class OktaAuthProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(fullProfile, params.id_token); return this.populateIdentity({ providerInfo: { @@ -167,28 +173,29 @@ export class OktaAuthProvider implements OAuthHandlers { } } -export const createOktaProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const audience = envConfig.getString('audience'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type OktaProviderOptions = {}; - const provider = new OktaAuthProvider({ - audience, - clientId, - clientSecret, - callbackUrl, - }); +export const createOktaProvider = ( + _options?: OktaProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const audience = envConfig.getString('audience'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new OktaAuthProvider({ + audience, + clientId, + clientSecret, + callbackUrl, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/onelogin/index.ts b/plugins/auth-backend/src/providers/onelogin/index.ts index 79af226cd1..60fd2ffca4 100644 --- a/plugins/auth-backend/src/providers/onelogin/index.ts +++ b/plugins/auth-backend/src/providers/onelogin/index.ts @@ -15,3 +15,4 @@ */ export { createOneLoginProvider } from './provider'; +export type { OneLoginProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 838b655d59..5cb0f84415 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -25,6 +25,7 @@ import { OAuthStartRequest, encodeState, OAuthRefreshRequest, + OAuthResult, } from '../../lib/oauth'; import passport from 'passport'; import { @@ -41,14 +42,14 @@ type PrivateInfo = { refreshToken: string; }; -export type OneLoginProviderOptions = OAuthProviderOptions & { +export type Options = OAuthProviderOptions & { issuer: string; }; export class OneLoginProvider implements OAuthHandlers { private readonly _strategy: any; - constructor(options: OneLoginProviderOptions) { + constructor(options: Options) { this._strategy = new OneLoginStrategy( { issuer: options.issuer, @@ -61,21 +62,16 @@ export class OneLoginProvider implements OAuthHandlers { accessToken: any, refreshToken: any, params: any, - rawProfile: passport.Profile, - done: PassportDoneCallback, + fullProfile: passport.Profile, + done: PassportDoneCallback, ) => { - const profile = makeProfileInfo(rawProfile, params.id_token); - done( undefined, { - providerInfo: { - idToken: params.id_token, - accessToken, - scope: params.scope, - expiresInSeconds: params.expires_in, - }, - profile, + accessToken, + refreshToken, + params, + fullProfile, }, { refreshToken, @@ -96,13 +92,23 @@ export class OneLoginProvider implements OAuthHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const { response, privateInfo } = await executeFrameHandlerStrategy< - OAuthResponse, + const { result, privateInfo } = await executeFrameHandlerStrategy< + OAuthResult, PrivateInfo >(req, this._strategy); + const profile = makeProfileInfo(result.fullProfile, result.params.id_token); + return { - response: await this.populateIdentity(response), + response: await this.populateIdentity({ + profile, + providerInfo: { + idToken: result.params.id_token, + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + }), refreshToken: privateInfo.refreshToken, }; } @@ -114,11 +120,11 @@ export class OneLoginProvider implements OAuthHandlers { req.scope, ); - const profile = await executeFetchUserProfileStrategy( + const fullProfile = await executeFetchUserProfileStrategy( this._strategy, accessToken, - params.id_token, ); + const profile = makeProfileInfo(fullProfile, params.id_token); return this.populateIdentity({ providerInfo: { @@ -146,28 +152,29 @@ export class OneLoginProvider implements OAuthHandlers { } } -export const createOneLoginProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const issuer = envConfig.getString('issuer'); - const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; +export type OneLoginProviderOptions = {}; - const provider = new OneLoginProvider({ - clientId, - clientSecret, - callbackUrl, - issuer, - }); +export const createOneLoginProvider = ( + _options?: OneLoginProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const issuer = envConfig.getString('issuer'); + const callbackUrl = `${globalConfig.baseUrl}/${providerId}/handler/frame`; - return OAuthAdapter.fromConfig(globalConfig, provider, { - disableRefresh: false, - providerId, - tokenIssuer, + const provider = new OneLoginProvider({ + clientId, + clientSecret, + callbackUrl, + issuer, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + disableRefresh: false, + providerId, + tokenIssuer, + }); }); - }); +}; diff --git a/plugins/auth-backend/src/providers/saml/index.ts b/plugins/auth-backend/src/providers/saml/index.ts index 582deb1608..597b20ee8d 100644 --- a/plugins/auth-backend/src/providers/saml/index.ts +++ b/plugins/auth-backend/src/providers/saml/index.ts @@ -15,3 +15,4 @@ */ export { createSamlProvider } from './provider'; +export type { SamlProviderOptions } from './provider'; diff --git a/plugins/auth-backend/src/providers/saml/provider.ts b/plugins/auth-backend/src/providers/saml/provider.ts index 74541d8294..c3a91b3b77 100644 --- a/plugins/auth-backend/src/providers/saml/provider.ts +++ b/plugins/auth-backend/src/providers/saml/provider.ts @@ -26,17 +26,17 @@ import { executeRedirectStrategy, PassportDoneCallback, } from '../../lib/passport'; -import { - AuthProviderRouteHandlers, - ProfileInfo, - AuthProviderFactory, -} from '../types'; +import { AuthProviderRouteHandlers, AuthProviderFactory } from '../types'; import { postMessageResponse } from '../../lib/flow'; import { TokenIssuer } from '../../identity'; type SamlInfo = { - userId: string; - profile: ProfileInfo; + fullProfile: any; +}; + +type Options = SamlConfig & { + tokenIssuer: TokenIssuer; + appUrl: string; }; export class SamlAuthProvider implements AuthProviderRouteHandlers { @@ -44,11 +44,11 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { private readonly tokenIssuer: TokenIssuer; private readonly appUrl: string; - constructor(options: SAMLProviderOptions) { + constructor(options: Options) { this.appUrl = options.appUrl; this.tokenIssuer = options.tokenIssuer; this.strategy = new SamlStrategy({ ...options }, (( - profile: SamlProfile, + fullProfile: SamlProfile, done: PassportDoneCallback, ) => { // TODO: There's plenty more validation and profile handling to do here, @@ -56,13 +56,7 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { // for non-oauth auth flows. // TODO: This flow doesn't issue an identity token that can be used to validate // the identity of the user in other backends, which we need in some form. - done(undefined, { - userId: profile.nameID!, - profile: { - email: profile.email!, - displayName: profile.displayName as string, - }, - }); + done(undefined, { fullProfile }); }) as VerifyWithoutRequest); } @@ -76,11 +70,13 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { res: express.Response, ): Promise { try { - const { - response: { userId, profile }, - } = await executeFrameHandlerStrategy(req, this.strategy); + const { result } = await executeFrameHandlerStrategy( + req, + this.strategy, + ); + + const id = result.fullProfile.nameID; - const id = userId; const idToken = await this.tokenIssuer.issueToken({ claims: { sub: id }, }); @@ -88,8 +84,11 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { return postMessageResponse(res, this.appUrl, { type: 'authorization_response', response: { + profile: { + email: result.fullProfile.email, + displayName: result.fullProfile.displayName, + }, providerInfo: {}, - profile, backstageIdentity: { id, idToken }, }, }); @@ -113,40 +112,36 @@ export class SamlAuthProvider implements AuthProviderRouteHandlers { } } -type SAMLProviderOptions = SamlConfig & { - tokenIssuer: TokenIssuer; - appUrl: string; -}; - type SignatureAlgorithm = 'sha1' | 'sha256' | 'sha512'; -export const createSamlProvider: AuthProviderFactory = ({ - providerId, - globalConfig, - config, - tokenIssuer, -}) => { - const opts = { - callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, - entryPoint: config.getString('entryPoint'), - logoutUrl: config.getOptionalString('logoutUrl'), - issuer: config.getString('issuer'), - cert: config.getOptionalString('cert'), - privateCert: config.getOptionalString('privateKey'), - decryptionPvk: config.getOptionalString('decryptionPvk'), - signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as - | SignatureAlgorithm - | undefined, - digestAlgorithm: config.getOptionalString('digestAlgorithm'), +export type SamlProviderOptions = {}; - tokenIssuer, - appUrl: globalConfig.appUrl, +export const createSamlProvider = ( + _options?: SamlProviderOptions, +): AuthProviderFactory => { + return ({ providerId, globalConfig, config, tokenIssuer }) => { + const opts = { + callbackUrl: `${globalConfig.baseUrl}/${providerId}/handler/frame`, + entryPoint: config.getString('entryPoint'), + logoutUrl: config.getOptionalString('logoutUrl'), + issuer: config.getString('issuer'), + cert: config.getOptionalString('cert'), + privateCert: config.getOptionalString('privateKey'), + decryptionPvk: config.getOptionalString('decryptionPvk'), + signatureAlgorithm: config.getOptionalString('signatureAlgorithm') as + | SignatureAlgorithm + | undefined, + digestAlgorithm: config.getOptionalString('digestAlgorithm'), + + tokenIssuer, + appUrl: globalConfig.appUrl, + }; + + // passport-saml will return an error if the `cert` key is set, and the value is empty. + // Since we read from config (such as environment variables) an empty string should be equal to being unset. + if (!opts.cert) { + delete opts.cert; + } + return new SamlAuthProvider(opts); }; - - // passport-saml will return an error if the `cert` key is set, and the value is empty. - // Since we read from config (such as environment variables) an empty string should be equal to being unset. - if (!opts.cert) { - delete opts.cert; - } - return new SamlAuthProvider(opts); }; diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 254898a086..c595c8dd5a 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -45,6 +45,7 @@ "express-promise-router": "^3.0.3", "fs-extra": "^9.0.0", "git-url-parse": "^11.4.4", + "glob": "^7.1.6", "knex": "^0.21.6", "ldapjs": "^2.2.0", "lodash": "^4.17.15", diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts new file mode 100644 index 0000000000..0155f93916 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.test.ts @@ -0,0 +1,84 @@ +/* + * 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 { FileReaderProcessor } from './FileReaderProcessor'; +import { + CatalogProcessorEntityResult, + CatalogProcessorErrorResult, + CatalogProcessorResult, +} from './types'; +import path from 'path'; + +describe('FileReaderProcessor', () => { + const fixturesRoot = path.join( + 'src', + 'ingestion', + 'processors', + '__fixtures__', + 'fileReaderProcessor', + ); + + it('should load from file', async () => { + const processor = new FileReaderProcessor(); + const spec = { + type: 'file', + target: `${path.join(fixturesRoot, 'component.yaml')}`, + }; + + const generated = (await new Promise(emit => + processor.readLocation(spec, false, emit), + )) as CatalogProcessorEntityResult; + + expect(generated.type).toBe('entity'); + expect(generated.location).toEqual(spec); + expect(generated.entity).toEqual({ kind: 'Component' }); + }); + + it('should fail load from file with error', async () => { + const processor = new FileReaderProcessor(); + const spec = { + type: 'file', + target: `${path.join(fixturesRoot, 'missing.yaml')}`, + }; + + const generated = (await new Promise(emit => + processor.readLocation(spec, false, emit), + )) as CatalogProcessorErrorResult; + + expect(generated.type).toBe('error'); + expect(generated.location).toBe(spec); + expect(generated.error.name).toBe('NotFoundError'); + expect(generated.error.message).toBe( + `file ${path.join(fixturesRoot, 'missing.yaml')} does not exist`, + ); + }); + + it('should support globs', async () => { + const processor = new FileReaderProcessor(); + + const emit = jest.fn(); + + await processor.readLocation( + { type: 'file', target: `${path.join(fixturesRoot, '**', '*.yaml')}` }, + false, + emit, + ); + + expect(emit).toBeCalledTimes(2); + expect(emit.mock.calls[0][0].entity).toEqual({ kind: 'Component' }); + expect(emit.mock.calls[1][0].entity).toEqual({ kind: 'API' }); + }); +}); diff --git a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts index 2328ec3c7a..db069913a2 100644 --- a/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/FileReaderProcessor.ts @@ -19,6 +19,10 @@ import fs from 'fs-extra'; import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; import { parseEntityYaml } from './util/parse'; +import { promisify } from 'util'; +import g from 'glob'; + +const glob = promisify(g); export class FileReaderProcessor implements CatalogProcessor { async readLocation( @@ -31,12 +35,15 @@ export class FileReaderProcessor implements CatalogProcessor { } try { - const exists = await fs.pathExists(location.target); - if (exists) { - const data = await fs.readFile(location.target); + const fileMatches = await glob(location.target); - for (const parseResult of parseEntityYaml(data, location)) { - emit(parseResult); + if (fileMatches.length > 0) { + for (const fileMatch of fileMatches) { + const data = await fs.readFile(fileMatch); + + for (const parseResult of parseEntityYaml(data, location)) { + emit(parseResult); + } } } else if (!optional) { const message = `${location.type} ${location.target} does not exist`; diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml new file mode 100644 index 0000000000..8524aaf14a --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/component.yaml @@ -0,0 +1 @@ +kind: Component diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml new file mode 100644 index 0000000000..a894e34f70 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/dir/api.yaml @@ -0,0 +1 @@ +kind: API diff --git a/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt new file mode 100644 index 0000000000..f332206601 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/processors/__fixtures__/fileReaderProcessor/test.txt @@ -0,0 +1 @@ +Just a text file diff --git a/plugins/catalog-react/src/components/EntityTable/EntityTable.test.tsx b/plugins/catalog-react/src/components/EntityTable/EntityTable.test.tsx new file mode 100644 index 0000000000..33bbbd007d --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/EntityTable.test.tsx @@ -0,0 +1,68 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { EntityTable } from './EntityTable'; + +describe('', () => { + it('shows empty table', async () => { + const { getByText } = await renderInTestApp( + EMPTY} + columns={[]} + />, + ); + + expect(getByText('Entities')).toBeInTheDocument(); + expect(getByText('EMPTY')).toBeInTheDocument(); + }); + + it('shows entities', async () => { + const entities: Entity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'System', + metadata: { + name: 'my-entity', + }, + spec: {}, + }, + ]; + + const { getByText } = await renderInTestApp( + EMPTY} + columns={[ + { + title: 'Name', + field: 'metadata.name', + }, + ]} + />, + ); + + await waitFor(() => { + expect(getByText('my-entity')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx new file mode 100644 index 0000000000..afcaaea305 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/EntityTable.tsx @@ -0,0 +1,81 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { Table, TableColumn } from '@backstage/core'; +import { makeStyles } from '@material-ui/core'; +import React, { ReactNode } from 'react'; +import * as columnFactories from './columns'; +import { componentEntityColumns, systemEntityColumns } from './presets'; + +type Props = { + title: string; + variant?: 'gridItem'; + entities: T[]; + emptyContent?: ReactNode; + columns: TableColumn[]; +}; + +const useStyles = makeStyles(theme => ({ + empty: { + padding: theme.spacing(2), + display: 'flex', + justifyContent: 'center', + }, +})); + +export function EntityTable({ + entities, + title, + emptyContent, + variant = 'gridItem', + columns, +}: Props) { + const classes = useStyles(); + const tableStyle: React.CSSProperties = { + minWidth: '0', + width: '100%', + }; + + if (variant === 'gridItem') { + tableStyle.height = 'calc(100% - 10px)'; + } + + return ( + + columns={columns} + title={title} + style={tableStyle} + emptyContent={ + emptyContent &&
{emptyContent}
+ } + options={{ + // TODO: Toolbar padding if off compared to other cards, should be: padding: 16px 24px; + search: false, + paging: false, + actionsColumnIndex: -1, + padding: 'dense', + }} + data={entities} + /> + ); +} + +EntityTable.columns = columnFactories; + +EntityTable.systemEntityColumns = systemEntityColumns; + +EntityTable.componentEntityColumns = componentEntityColumns; diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx new file mode 100644 index 0000000000..7ca99b1100 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -0,0 +1,158 @@ +/* + * 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, + RELATION_OWNED_BY, + RELATION_PART_OF, +} from '@backstage/catalog-model'; +import { TableColumn } from '@backstage/core'; +import React from 'react'; +import { getEntityRelations } from '../../utils'; +import { + EntityRefLink, + EntityRefLinks, + formatEntityRefTitle, +} from '../EntityRefLink'; + +export function createEntityRefColumn({ + defaultKind, +}: { + defaultKind?: string; +}): TableColumn { + function formatContent(entity: T): string { + return formatEntityRefTitle(entity, { + defaultKind, + }); + } + + return { + title: 'Name', + highlight: true, + customFilterAndSearch(filter, entity) { + // TODO: We could implement this more efficiently, like searching over + // each field that is displayed individually (kind, namespace, name). + // but that migth confuse the user as it will behave different than a + // simple text search. + // Another alternative would be to cache the values. But writing them + // into the entity feels bad too. + return formatContent(entity).includes(filter); + }, + customSort(entity1, entity2) { + // TODO: We could implement this more efficiently by comparing field by field. + // This has similar issues as above. + return formatContent(entity1).localeCompare(formatContent(entity2)); + }, + render: entity => ( + + ), + }; +} + +export function createEntityRelationColumn({ + title, + relation, + defaultKind, + filter: entityFilter, +}: { + title: string; + relation: string; + defaultKind?: string; + filter?: { kind: string }; +}): TableColumn { + function getRelations(entity: T): EntityName[] { + return getEntityRelations(entity, relation, entityFilter); + } + + function formatContent(entity: T): string { + return getRelations(entity) + .map(r => formatEntityRefTitle(r, { defaultKind })) + .join(', '); + } + + return { + title, + customFilterAndSearch(filter, entity) { + return formatContent(entity).includes(filter); + }, + customSort(entity1, entity2) { + return formatContent(entity1).localeCompare(formatContent(entity2)); + }, + render: entity => { + return ( + + ); + }, + }; +} + +export function createOwnerColumn(): TableColumn { + return createEntityRelationColumn({ + title: 'Owner', + relation: RELATION_OWNED_BY, + defaultKind: 'group', + }); +} + +export function createDomainColumn(): TableColumn { + return createEntityRelationColumn({ + title: 'Domain', + relation: RELATION_PART_OF, + defaultKind: 'domain', + filter: { + kind: 'domain', + }, + }); +} + +export function createSystemColumn(): TableColumn { + return createEntityRelationColumn({ + title: 'System', + relation: RELATION_PART_OF, + defaultKind: 'system', + filter: { + kind: 'system', + }, + }); +} + +export function createMetadataDescriptionColumn< + T extends Entity +>(): TableColumn { + return { + title: 'Description', + field: 'metadata.description', + width: 'auto', + }; +} + +export function createSpecLifecycleColumn(): TableColumn { + return { + title: 'Lifecycle', + field: 'spec.lifecycle', + }; +} + +export function createSpecTypeColumn(): TableColumn { + return { + title: 'Type', + field: 'spec.type', + }; +} diff --git a/plugins/catalog-react/src/components/EntityTable/index.ts b/plugins/catalog-react/src/components/EntityTable/index.ts new file mode 100644 index 0000000000..8a01b9baae --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityTable } from './EntityTable'; diff --git a/plugins/catalog-react/src/components/EntityTable/presets.test.tsx b/plugins/catalog-react/src/components/EntityTable/presets.test.tsx new file mode 100644 index 0000000000..b6b8c26e37 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/presets.test.tsx @@ -0,0 +1,137 @@ +/* + * 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 { + ComponentEntity, + RELATION_OWNED_BY, + RELATION_PART_OF, + SystemEntity, +} from '@backstage/catalog-model'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { EntityTable } from './EntityTable'; +import { componentEntityColumns, systemEntityColumns } from './presets'; + +describe('systemEntityColumns', () => { + it('shows systems', async () => { + const entities: SystemEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'System', + metadata: { + name: 'my-system', + namespace: 'my-namespace', + description: 'Some description', + }, + spec: { + owner: 'owner-data', + }, + relations: [ + { + type: RELATION_PART_OF, + target: { + kind: 'Domain', + name: 'my-domain', + namespace: 'my-namespace', + }, + }, + { + type: RELATION_OWNED_BY, + target: { + kind: 'Group', + name: 'Test', + namespace: 'default', + }, + }, + ], + }, + ]; + + const { getByText } = await renderInTestApp( + EMPTY} + columns={systemEntityColumns} + />, + ); + + await waitFor(() => { + expect(getByText('my-namespace/my-system')).toBeInTheDocument(); + expect(getByText('my-namespace/my-domain')).toBeInTheDocument(); + expect(getByText('Test')).toBeInTheDocument(); + expect(getByText('Some description')).toBeInTheDocument(); + }); + }); +}); + +describe('componentEntityColumns', () => { + it('shows components', async () => { + const entities: ComponentEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'my-component', + namespace: 'my-namespace', + description: 'Some description', + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'owner-data', + }, + relations: [ + { + type: RELATION_PART_OF, + target: { + kind: 'System', + name: 'my-system', + namespace: 'my-namespace', + }, + }, + { + type: RELATION_OWNED_BY, + target: { + kind: 'Group', + name: 'Test', + namespace: 'default', + }, + }, + ], + }, + ]; + + const { getByText } = await renderInTestApp( + EMPTY} + columns={componentEntityColumns} + />, + ); + + await waitFor(() => { + expect(getByText('my-namespace/my-component')).toBeInTheDocument(); + expect(getByText('my-namespace/my-system')).toBeInTheDocument(); + expect(getByText('Test')).toBeInTheDocument(); + expect(getByText('production')).toBeInTheDocument(); + expect(getByText('service')).toBeInTheDocument(); + expect(getByText('Some description')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityTable/presets.tsx b/plugins/catalog-react/src/components/EntityTable/presets.tsx new file mode 100644 index 0000000000..7ce6b75532 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/presets.tsx @@ -0,0 +1,43 @@ +/* + * 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 { ComponentEntity, SystemEntity } from '@backstage/catalog-model'; +import { TableColumn } from '@backstage/core'; +import { + createDomainColumn, + createEntityRefColumn, + createMetadataDescriptionColumn, + createOwnerColumn, + createSpecLifecycleColumn, + createSpecTypeColumn, + createSystemColumn, +} from './columns'; + +export const systemEntityColumns: TableColumn[] = [ + createEntityRefColumn({ defaultKind: 'system' }), + createDomainColumn(), + createOwnerColumn(), + createMetadataDescriptionColumn(), +]; + +export const componentEntityColumns: TableColumn[] = [ + createEntityRefColumn({ defaultKind: 'component' }), + createSystemColumn(), + createOwnerColumn(), + createSpecTypeColumn(), + createSpecLifecycleColumn(), + createMetadataDescriptionColumn(), +]; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index fcfa8207b3..5181b8f0ea 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -13,5 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export * from './EntityRefLink'; export * from './EntityProvider'; +export * from './EntityRefLink'; +export * from './EntityTable'; diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index 77de70b8d4..2ecf6e9a90 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -15,3 +15,4 @@ */ export { EntityContext, useEntity, useEntityFromUrl } from './useEntity'; export { useEntityCompoundName } from './useEntityCompoundName'; +export { useRelatedEntities } from './useRelatedEntities'; diff --git a/plugins/api-docs/src/components/useRelatedEntities.ts b/plugins/catalog-react/src/hooks/useRelatedEntities.ts similarity index 51% rename from plugins/api-docs/src/components/useRelatedEntities.ts rename to plugins/catalog-react/src/hooks/useRelatedEntities.ts index 5c01d38189..baf6cf5fc7 100644 --- a/plugins/api-docs/src/components/useRelatedEntities.ts +++ b/plugins/catalog-react/src/hooks/useRelatedEntities.ts @@ -15,36 +15,45 @@ */ import { Entity } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { useAsyncRetry } from 'react-use'; +import { useAsync } from 'react-use'; +import { catalogApiRef } from '../api'; -// TODO: Maybe this hook is interesting for others too? export function useRelatedEntities( entity: Entity, - type: string, + { type, kind }: { type?: string; kind?: string }, ): { - entities: (Entity | undefined)[] | undefined; + entities: Entity[] | undefined; loading: boolean; error: Error | undefined; } { const catalogApi = useApi(catalogApiRef); - const { loading, value, error } = useAsyncRetry< - (Entity | undefined)[] - >(async () => { + const { loading, value: entities, error } = useAsync(async () => { const relations = - entity.relations && entity.relations.filter(r => r.type === type); + entity.relations && + entity.relations.filter( + r => + (!type || r.type.toLowerCase() === type.toLowerCase()) && + (!kind || r.target.kind.toLowerCase() === kind.toLowerCase()), + ); if (!relations) { return []; } - return await Promise.all( + // TODO: This code could be more efficient if there was an endpoint in the + // backend that either returns the relations of entity (filtered by type) + // or if there is a way to perform a batch request by entity name. However, + // such an implementation would probably be better placed in the graphql API. + const results = await Promise.all( relations?.map(r => catalogApi.getEntityByName(r.target)), ); + // Skip entities that where not found, for example if a relation references + // an entity that doesn't exist. + return results.filter(e => e) as Entity[]; }, [entity, type]); return { - entities: value, + entities, loading, error, }; diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx index 3038fc3c5c..ada827115d 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.test.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ -import React from 'react'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; import { render } from '@testing-library/react'; +import React from 'react'; import { AboutCard } from './AboutCard'; describe(' GitHub', () => { @@ -36,7 +37,11 @@ describe(' GitHub', () => { lifecycle: 'production', }, }; - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText('service')).toBeInTheDocument(); expect(getByText('View Source').closest('a')).toHaveAttribute( 'href', @@ -67,7 +72,11 @@ describe(' GitLab', () => { lifecycle: 'production', }, }; - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText('service')).toBeInTheDocument(); expect(getByText('View Source').closest('a')).toHaveAttribute( 'href', @@ -98,7 +107,11 @@ describe(' BitBucket', () => { lifecycle: 'production', }, }; - const { getByText } = render(); + const { getByText } = render( + + + , + ); expect(getByText('service')).toBeInTheDocument(); expect(getByText('View Source').closest('a')).toHaveAttribute( 'href', diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 1549d87b8a..04c2a78869 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -19,6 +19,8 @@ import { ENTITY_DEFAULT_NAMESPACE, RELATION_PROVIDES_API, } from '@backstage/catalog-model'; +import { HeaderIconLinkRow } from '@backstage/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Card, CardContent, @@ -27,14 +29,13 @@ import { IconButton, makeStyles, } from '@material-ui/core'; -import ExtensionIcon from '@material-ui/icons/Extension'; 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 } from '../../data/utils'; import { createEditLink, determineUrlType } from '../createEditLink'; -import { HeaderIconLinkRow } from '@backstage/core'; import { AboutContent } from './AboutContent'; const useStyles = makeStyles({ @@ -76,12 +77,14 @@ function getCodeLinkInfo(entity: Entity): CodeLinkInfo { } type AboutCardProps = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; variant?: string; }; -export function AboutCard({ entity, variant }: 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); diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index 75b7827111..57ea894570 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -66,7 +66,7 @@ export const AboutContent = ({ entity }: Props) => { - + {isSystem && ( { Unregister entity - - - - - Move repository - diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index 60fb9c3cf5..b0c3db2333 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { IconComponent, InfoCard, useApp } from '@backstage/core'; +import { IconComponent, IconKey, InfoCard, useApp } from '@backstage/core'; import { useEntity } from '@backstage/plugin-catalog-react'; import LanguageIcon from '@material-ui/icons/Language'; import React from 'react'; @@ -33,9 +33,8 @@ export const EntityLinksCard = ({ cols = undefined }: Props) => { const { entity } = useEntity(); const app = useApp(); - // TODO: Refactor App.icons & App.getSystemIcon to support custom icons - const iconResolver = (key: string | undefined): IconComponent => { - return app.getSystemIcon(key as any) ?? LanguageIcon; + const iconResolver = (key: IconKey | undefined): IconComponent => { + return app.getSystemIcon(key ?? '') ?? LanguageIcon; }; const links = entity?.metadata?.links; diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx new file mode 100644 index 0000000000..58df0a53f8 --- /dev/null +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.test.tsx @@ -0,0 +1,117 @@ +/* + * 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, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, +} from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { HasComponentsCard } from './HasComponentsCard'; + +describe('', () => { + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(catalogApiRef, catalogApi); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'System', + metadata: { + name: 'my-system', + namespace: 'my-namespace', + }, + relations: [], + }; + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText('Components')).toBeInTheDocument(); + expect( + getByText(/No component is part of this system/i), + ).toBeInTheDocument(); + }); + + it('shows related components', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'System', + metadata: { + name: 'my-system', + namespace: 'my-namespace', + }, + relations: [ + { + target: { + kind: 'Component', + namespace: 'my-namespace', + name: 'target-name', + }, + type: RELATION_HAS_PART, + }, + ], + }; + catalogApi.getEntityByName.mockResolvedValue({ + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: {}, + }); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + await waitFor(() => { + expect(getByText('Components')).toBeInTheDocument(); + expect(getByText(/target-name/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx new file mode 100644 index 0000000000..6485d4f525 --- /dev/null +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -0,0 +1,79 @@ +/* + * 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 { ComponentEntity, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { + CodeSnippet, + InfoCard, + Link, + Progress, + WarningPanel, +} from '@backstage/core'; +import { + EntityTable, + useEntity, + useRelatedEntities, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; + +type Props = { + variant?: 'gridItem'; +}; + +export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => { + const { entity } = useEntity(); + const { entities, loading, error } = useRelatedEntities(entity, { + type: RELATION_HAS_PART, + kind: 'Component', + }); + + if (loading) { + return ( + + + + ); + } + + if (error || !entities) { + return ( + + } + /> + + ); + } + + return ( + + No component is part of this system.{' '} + + Learn how to add components. + + + } + columns={EntityTable.componentEntityColumns} + entities={entities as ComponentEntity[]} + /> + ); +}; diff --git a/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx b/plugins/catalog/src/components/HasComponentsCard/index.ts similarity index 57% rename from plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx rename to plugins/catalog/src/components/HasComponentsCard/index.ts index de753713c3..059392c3e8 100644 --- a/plugins/api-docs/src/components/EmptyState/MissingConsumesApisEmptyState.test.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/index.ts @@ -14,15 +14,4 @@ * limitations under the License. */ -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; -import { MissingConsumesApisEmptyState } from './MissingConsumesApisEmptyState'; - -describe('', () => { - it('renders without exploding', async () => { - const { getByText } = await renderInTestApp( - , - ); - expect(getByText(/consumesApis:/i)).toBeInTheDocument(); - }); -}); +export { HasComponentsCard } from './HasComponentsCard'; diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx new file mode 100644 index 0000000000..79b69d35ff --- /dev/null +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.test.tsx @@ -0,0 +1,117 @@ +/* + * 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, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, +} from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { HasSubcomponentsCard } from './HasSubcomponentsCard'; + +describe('', () => { + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(catalogApiRef, catalogApi); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-components', + namespace: 'my-namespace', + }, + relations: [], + }; + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText('Subcomponents')).toBeInTheDocument(); + expect( + getByText(/No subcomponent is part of this component/i), + ).toBeInTheDocument(); + }); + + it('shows related subcomponents', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'my-component', + namespace: 'my-namespace', + }, + relations: [ + { + target: { + kind: 'Component', + namespace: 'my-namespace', + name: 'target-name', + }, + type: RELATION_HAS_PART, + }, + ], + }; + catalogApi.getEntityByName.mockResolvedValue({ + apiVersion: 'v1', + kind: 'Component', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: {}, + }); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + await waitFor(() => { + expect(getByText('Subcomponents')).toBeInTheDocument(); + expect(getByText(/target-name/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx new file mode 100644 index 0000000000..c2635b68e1 --- /dev/null +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -0,0 +1,79 @@ +/* + * 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 { ComponentEntity, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { + CodeSnippet, + InfoCard, + Link, + Progress, + WarningPanel, +} from '@backstage/core'; +import { + EntityTable, + useEntity, + useRelatedEntities, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; + +type Props = { + variant?: 'gridItem'; +}; + +export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => { + const { entity } = useEntity(); + const { entities, loading, error } = useRelatedEntities(entity, { + type: RELATION_HAS_PART, + kind: 'Component', + }); + + if (loading) { + return ( + + + + ); + } + + if (error || !entities) { + return ( + + } + /> + + ); + } + + return ( + + No subcomponent is part of this component.{' '} + + Learn how to add subcomponents. + + + } + columns={EntityTable.componentEntityColumns} + entities={entities as ComponentEntity[]} + /> + ); +}; diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/index.ts b/plugins/catalog/src/components/HasSubcomponentsCard/index.ts new file mode 100644 index 0000000000..cef0221d3b --- /dev/null +++ b/plugins/catalog/src/components/HasSubcomponentsCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { HasSubcomponentsCard } from './HasSubcomponentsCard'; diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx new file mode 100644 index 0000000000..08923a59c8 --- /dev/null +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.test.tsx @@ -0,0 +1,115 @@ +/* + * 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, RELATION_HAS_PART } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, +} from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; +import React from 'react'; +import { HasSystemsCard } from './HasSystemsCard'; + +describe('', () => { + const catalogApi: jest.Mocked = { + getLocationById: jest.fn(), + getEntityByName: jest.fn(), + getEntities: jest.fn(), + addLocation: jest.fn(), + getLocationByEntity: jest.fn(), + removeEntityByUid: jest.fn(), + } as any; + let Wrapper: React.ComponentType; + + beforeEach(() => { + const apis = ApiRegistry.with(catalogApiRef, catalogApi); + + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + {children} + ); + }); + + afterEach(() => jest.resetAllMocks()); + + it('shows empty list if no relations', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'my-domain', + namespace: 'my-namespace', + }, + relations: [], + }; + + const { getByText } = await renderInTestApp( + + + + + , + ); + + expect(getByText('Systems')).toBeInTheDocument(); + expect(getByText(/No system is part of this domain/i)).toBeInTheDocument(); + }); + + it('shows related systems', async () => { + const entity: Entity = { + apiVersion: 'v1', + kind: 'Domain', + metadata: { + name: 'my-domain', + namespace: 'my-namespace', + }, + relations: [ + { + target: { + kind: 'System', + namespace: 'my-namespace', + name: 'target-name', + }, + type: RELATION_HAS_PART, + }, + ], + }; + catalogApi.getEntityByName.mockResolvedValue({ + apiVersion: 'v1', + kind: 'System', + metadata: { + name: 'target-name', + namespace: 'my-namespace', + }, + spec: {}, + }); + + const { getByText } = await renderInTestApp( + + + + + , + ); + + await waitFor(() => { + expect(getByText('Systems')).toBeInTheDocument(); + expect(getByText(/target-name/i)).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx new file mode 100644 index 0000000000..9faa640060 --- /dev/null +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx @@ -0,0 +1,78 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RELATION_HAS_PART, SystemEntity } from '@backstage/catalog-model'; +import { + CodeSnippet, + InfoCard, + Link, + Progress, + WarningPanel, +} from '@backstage/core'; +import { + EntityTable, + useEntity, + useRelatedEntities, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; + +type Props = { + variant?: 'gridItem'; +}; + +export const HasSystemsCard = ({ variant = 'gridItem' }: Props) => { + const { entity } = useEntity(); + const { entities, loading, error } = useRelatedEntities(entity, { + type: RELATION_HAS_PART, + }); + + if (loading) { + return ( + + + + ); + } + + if (error || !entities) { + return ( + + } + /> + + ); + } + + return ( + + No system is part of this domain.{' '} + + Learn how to add systems. + + + } + columns={EntityTable.systemEntityColumns} + entities={entities as SystemEntity[]} + /> + ); +}; diff --git a/plugins/catalog/src/components/HasSystemsCard/index.ts b/plugins/catalog/src/components/HasSystemsCard/index.ts new file mode 100644 index 0000000000..a29d257e7e --- /dev/null +++ b/plugins/catalog/src/components/HasSystemsCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { HasSystemsCard } from './HasSystemsCard'; diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index 1bc11f6880..800c5c4693 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -20,9 +20,13 @@ export { EntityPageLayout } from './components/EntityPageLayout'; export * from './components/EntitySwitch'; export { Router } from './components/Router'; export { + CatalogEntityPage, + CatalogIndexPage, catalogPlugin, catalogPlugin as plugin, - CatalogIndexPage, - CatalogEntityPage, + EntityAboutCard, + EntityHasComponentsCard, + EntityHasSubcomponentsCard, + EntityHasSystemsCard, EntityLinksCard, } from './plugin'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index c6944a318a..a0932a8dba 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -17,10 +17,10 @@ import { CatalogClient } from '@backstage/catalog-client'; import { createApiFactory, - createPlugin, - discoveryApiRef, createComponentExtension, + createPlugin, createRoutableExtension, + discoveryApiRef, identityApiRef, } from '@backstage/core'; import { @@ -60,13 +60,19 @@ export const CatalogIndexPage = catalogPlugin.provide( export const CatalogEntityPage = catalogPlugin.provide( createRoutableExtension({ component: () => - import('./components/CatalogEntityPage/CatalogEntityPage').then( - m => m.CatalogEntityPage, - ), + import('./components/CatalogEntityPage').then(m => m.CatalogEntityPage), mountPoint: entityRouteRef, }), ); +export const EntityAboutCard = catalogPlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./components/AboutCard').then(m => m.AboutCard), + }, + }), +); + export const EntityLinksCard = catalogPlugin.provide( createComponentExtension({ component: { @@ -75,3 +81,32 @@ export const EntityLinksCard = catalogPlugin.provide( }, }), ); + +export const EntityHasSystemsCard = catalogPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/HasSystemsCard').then(m => m.HasSystemsCard), + }, + }), +); + +export const EntityHasComponentsCard = catalogPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/HasComponentsCard').then(m => m.HasComponentsCard), + }, + }), +); + +export const EntityHasSubcomponentsCard = catalogPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/HasSubcomponentsCard').then( + m => m.HasSubcomponentsCard, + ), + }, + }), +); diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index dae68a5ce2..86adb8673f 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -11,6 +11,8 @@ At Spotify, we find that cloud costs are optimized organically when: Cost Insights shows trends over time, at the granularity of Backstage catalog entities - rather than the cloud provider's concepts. It can be used to troubleshoot cost anomalies, and promote cost-saving infrastructure migrations. +Learn more with the Backstage blog post [New Cost Insights plugin: The engineer's solution to taming cloud costs](https://backstage.io/blog/2020/10/22/cost-insights-plugin). + ## Install ```bash diff --git a/plugins/org/package.json b/plugins/org/package.json index 4e12ad8837..4a278a22f5 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -22,6 +22,7 @@ "dependencies": { "@backstage/catalog-model": "^0.7.1", "@backstage/core": "^0.6.1", + "@backstage/core-api": "^0.2.9", "@backstage/plugin-catalog-react": "^0.0.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -29,6 +30,7 @@ "@material-ui/lab": "4.0.0-alpha.45", "react": "^16.13.1", "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3" }, diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx new file mode 100644 index 0000000000..39d30d8165 --- /dev/null +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx @@ -0,0 +1,67 @@ +/* + * 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 { Grid } from '@material-ui/core'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { GroupEntity } from '@backstage/catalog-model'; +import { EntityContext } from '@backstage/plugin-catalog-react'; +import { GroupProfileCard } from '.'; + +export default { + title: 'Plugins/Org/Group Profile Card', + component: GroupProfileCard, +}; + +const dummyDepartment = { + type: 'childOf', + target: { + namespace: 'default', + kind: 'group', + name: 'department-a', + }, +}; + +const defaultEntity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'team-a', + description: 'Team A', + }, + spec: { + profile: { + displayName: 'Team A', + email: 'team-a@example.com', + picture: + 'https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25', + }, + type: 'group', + children: [], + }, + relations: [dummyDepartment], +}; + +export const Default = () => ( + + + + + + + + + +); diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 9a72606c16..1682192880 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -117,7 +117,7 @@ export const GroupProfileCard = ({ - + {profile.email} diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx new file mode 100644 index 0000000000..20fb3a10ec --- /dev/null +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.stories.tsx @@ -0,0 +1,131 @@ +/* + * 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 { Grid } from '@material-ui/core'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { Entity, GroupEntity } from '@backstage/catalog-model'; +import { EntityContext, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { ApiProvider, ApiRegistry } from '@backstage/core-api'; + +import { MembersListCard } from '.'; + +export default { + title: 'Plugins/Org/Group Members List Card', + component: MembersListCard, +}; + +const makeUser = ({ + name, + uid, + displayName, + email, +}: { + name: string; + uid: string; + displayName: string; + email: string; +}) => ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name, + uid, + }, + spec: { + profile: { + displayName, + email, + picture: `https://avatars.dicebear.com/api/avataaars/${email}.svg?background=%23fff`, + }, + }, + relations: [ + { + type: 'memberOf', + target: { + namespace: 'default', + kind: 'group', + name: 'team-a', + }, + }, + ], +}); + +const defaultEntity: GroupEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Group', + metadata: { + name: 'team-a', + description: 'Team A', + }, + spec: { + profile: { + displayName: 'Team A', + email: 'team-a@example.com', + picture: + 'https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25', + }, + type: 'group', + children: [], + }, +}; + +const alice = makeUser({ + name: 'alice', + uid: '123', + displayName: 'Alice Doe', + email: 'alice@example.com', +}); +const bob = makeUser({ + name: 'bob', + uid: '456', + displayName: 'Bob Jones', + email: 'bob@example.com', +}); + +const catalogApi = (items: Entity[]) => ({ + getEntities: () => Promise.resolve({ items }), +}); + +const apiRegistry = (items: Entity[]) => + ApiRegistry.from([[catalogApiRef, catalogApi(items)]]); + +export const Default = () => ( + + + + + + + + + + + +); + +export const Empty = () => ( + + + + + + + + + + + +); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 7f8ede2eb7..e60e22a8f2 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -47,7 +47,7 @@ const useStyles = makeStyles((theme: Theme) => borderRadius: '4px', overflow: 'visible', position: 'relative', - margin: theme.spacing(3, 0, 1), + margin: theme.spacing(4, 1, 1), flex: '1', minWidth: '0px', }, @@ -69,7 +69,7 @@ const MemberComponent = ({ const displayName = profile?.displayName ?? metaName; return ( - + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name, + }, + spec: { + type, + }, + relations: [ + { + type: 'ownedBy', + target: { + namespace: 'default', + kind: 'Group', + name: 'team-a', + }, + }, + ], +}); + +const serviceA = makeComponent({ type: 'service', name: 'service-a' }); +const serviceB = makeComponent({ type: 'service', name: 'service-a' }); +const websiteA = makeComponent({ type: 'website', name: 'website-a' }); + +const catalogApi: Partial = { + getEntities: () => Promise.resolve({ items: [serviceA, serviceB, websiteA] }), +}; + +const apiRegistry = ApiRegistry.from([[catalogApiRef, catalogApi]]); + +export const Default = () => ( + + + + + + + + + + + +); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx new file mode 100644 index 0000000000..b30427e923 --- /dev/null +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -0,0 +1,93 @@ +/* + * 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 { Grid } from '@material-ui/core'; +import React from 'react'; +import { MemoryRouter } from 'react-router'; +import { UserEntity } from '@backstage/catalog-model'; +import { EntityContext } from '@backstage/plugin-catalog-react'; +import { UserProfileCard } from '.'; + +export default { + title: 'Plugins/Org/User Profile Card', + component: UserProfileCard, +}; + +const dummyGroup = { + type: 'memberOf', + target: { + namespace: 'default', + kind: 'group', + name: 'team-a', + }, +}; + +const defaultEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'guest', + }, + spec: { + profile: { + displayName: 'Guest User', + email: 'guest@example.com', + picture: + 'https://avatars.dicebear.com/api/avataaars/guest@example.com.svg?background=%23fff', + }, + memberOf: ['team-a'], + }, + relations: [dummyGroup], +}; + +export const Default = () => ( + + + + + + + + + +); + +const noImageEntity: UserEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { + name: 'guest', + }, + spec: { + profile: { + displayName: 'Guest User', + email: 'guest@example.com', + }, + memberOf: ['team-a'], + }, + relations: [dummyGroup], +}; + +export const NoImage = () => ( + + + + + + + + + +); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index a4969f615b..4dfa543d95 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -95,11 +95,11 @@ export const UserProfileCard = ({ return ( } variant={variant}> - + - + {profile?.email && ( diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts index d473656863..02865f8529 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.ts @@ -43,7 +43,7 @@ export class BitbucketPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); const git = Git.fromAuth({ logger, ...this.getAuth() }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts index 0c94edce42..f6e5600a83 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.test.ts @@ -28,7 +28,7 @@ describe('File preparer', () => { const preparer = new FilePreparer(); const root = os.platform() === 'win32' ? 'C:\\' : '/'; const workspacePath = path.join(root, 'tmp'); - const checkoutPath = path.resolve(workspacePath, 'checkout'); + const targetPath = path.resolve(workspacePath, 'template'); await preparer.prepare({ url: `file:///${root}path/to/template`, @@ -37,12 +37,12 @@ describe('File preparer', () => { }); expect(fs.copy).toHaveBeenCalledWith( path.join(root, 'path', 'to', 'template'), - checkoutPath, + targetPath, { recursive: true, }, ); - expect(fs.ensureDir).toHaveBeenCalledWith(checkoutPath); + expect(fs.ensureDir).toHaveBeenCalledWith(targetPath); await expect( preparer.prepare({ diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts index 4d49ba222e..687cc39452 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/file.ts @@ -25,12 +25,12 @@ export class FilePreparer implements PreparerBase { throw new InputError(`Wrong location protocol, should be 'file', ${url}`); } - const checkoutDir = path.join(workspacePath, 'checkout'); - await fs.ensureDir(checkoutDir); + const targetDir = path.join(workspacePath, 'template'); + await fs.ensureDir(targetDir); const templatePath = fileURLToPath(url); - await fs.copy(templatePath, checkoutDir, { + await fs.copy(templatePath, targetDir, { recursive: true, }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts index 301f015c1e..976370bd21 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.test.ts @@ -90,8 +90,8 @@ describe('GitHubPreparer', () => { expect(Git.fromAuth).toHaveBeenCalledWith({ logger, - username: 'fake-token', - password: 'x-oauth-basic', + username: 'x-access-token', + password: 'fake-token', }); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 533aa8e005..5b74ee81ca 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -33,13 +33,13 @@ export class GithubPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); const git = this.config.token ? Git.fromAuth({ - username: this.config.token, - password: 'x-oauth-basic', + username: 'x-access-token', + password: this.config.token, logger, }) : Git.fromAuth({ logger }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts index fa5c8c1325..e15de33ac6 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/gitlab.ts @@ -33,7 +33,7 @@ export class GitlabPreparer implements PreparerBase { const targetPath = path.join(workspacePath, 'template'); const fullPathToTemplate = path.resolve( checkoutPath, - parsedGitUrl.filepath, + parsedGitUrl.filepath ?? '', ); parsedGitUrl.git_suffix = true; diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts index 30fe8fd65a..1dc53e5a5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.test.ts @@ -97,7 +97,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); @@ -148,7 +148,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); @@ -206,7 +206,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); @@ -257,7 +257,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); @@ -307,7 +307,7 @@ describe('GitHub Publisher', () => { expect(initRepoAndPush).toHaveBeenCalledWith({ dir: resultPath, remoteUrl: 'https://github.com/backstage/backstage.git', - auth: { username: 'fake-token', password: 'x-oauth-basic' }, + auth: { username: 'x-access-token', password: 'fake-token' }, logger, }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 13aab7ff34..407a1633c2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -72,8 +72,8 @@ export class GithubPublisher implements PublisherBase { dir: path.join(workspacePath, 'result'), remoteUrl, auth: { - username: this.config.token, - password: 'x-oauth-basic', + username: 'x-access-token', + password: this.config.token, }, logger, }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 51425f75e8..e8aa9e7c43 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -17,7 +17,7 @@ import { Config } from '@backstage/config'; import Docker from 'dockerode'; import express from 'express'; -import { resolve as resolvePath } from 'path'; +import { resolve as resolvePath, dirname } from 'path'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { @@ -164,7 +164,7 @@ export async function createRouter( const preparer = new FilePreparer(); const path = resolvePath( - templateEntityLocation, + dirname(templateEntityLocation), template.spec.path || '.', ); diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index 732b5f3e44..ef17335b26 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -17,6 +17,8 @@ import { Button } from '@backstage/core'; import { BackstageTheme, pageTheme } from '@backstage/theme'; import { Card, + CardActions, + CardContent, Chip, makeStyles, Typography, @@ -34,18 +36,11 @@ const useStyles = makeStyles(theme => ({ props.backgroundImage, backgroundPosition: 0, }, - content: { - padding: theme.spacing(2), - }, description: { height: 175, overflow: 'hidden', textOverflow: 'ellipsis', }, - footer: { - display: 'flex', - flexDirection: 'row-reverse', - }, })); export type TemplateCardProps = { @@ -76,19 +71,19 @@ export const TemplateCard = ({ {type} {title} -
+ {tags?.map(tag => ( ))} {description} -
- -
-
+ + + + ); }; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index f3f97be877..09df6dba1a 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -99,7 +99,7 @@ describe('TemplatePage', () => { , ); - expect(rendered.queryByText('Create a new component')).toBeInTheDocument(); + expect(rendered.queryByText('Create a New Component')).toBeInTheDocument(); expect(rendered.queryByText('React SSR Template')).toBeInTheDocument(); // await act(async () => await mutate('templates/test')); }); @@ -116,7 +116,7 @@ describe('TemplatePage', () => { , ); - expect(rendered.queryByText('Create a new component')).toBeInTheDocument(); + expect(rendered.queryByText('Create a New Component')).toBeInTheDocument(); expect(rendered.queryByTestId('loading-progress')).toBeInTheDocument(); // Need to cleanup the promise or will timeout act(() => { @@ -141,7 +141,7 @@ describe('TemplatePage', () => { ); expect( - rendered.queryByText('Create a new component'), + rendered.queryByText('Create a New Component'), ).not.toBeInTheDocument(); expect(rendered.queryByText('This is root')).toBeInTheDocument(); }); diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index e1422d4ec1..d779f8b42c 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -149,10 +149,10 @@ export const TemplatePage = () => { return (
- Create a new component + Create a New Component } subtitle="Create new software components using standard templates" diff --git a/plugins/splunk-on-call/.eslintrc.js b/plugins/splunk-on-call/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/splunk-on-call/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/splunk-on-call/README.md b/plugins/splunk-on-call/README.md new file mode 100644 index 0000000000..73ee05c181 --- /dev/null +++ b/plugins/splunk-on-call/README.md @@ -0,0 +1,120 @@ +# Splunk On-Call + +## Overview + +This plugin displays Splunk On-Call, formerly VictorOps, information about an entity. + +There is a way to trigger an new incident directly to specific users or/and specific teams. + +This plugin requires that entities are annotated with a team name. See more further down in this document. + +This plugin provides: + +- A list of incidents +- A way to trigger a new incident to specific users or/and teams +- A way to acknowledge/resolve an incident +- Information details about the persons on-call + +## Setup instructions + +Install the plugin: + +```bash +yarn add @backstage/plugin-splunk-on-call +``` + +Add it to the app in `plugins.ts`: + +```ts +export { plugin as SplunkOnCall } from '@backstage/plugin-splunk-on-call'; +``` + +Add it to the `EntityPage.tsx`: + +```ts +import { + isPluginApplicableToEntity as isSplunkOnCallAvailable, + SplunkOnCallCard, +} from '@backstage/plugin-splunk-on-call'; +// ... +{ + isSplunkOnCallAvailable(entity) && ( + + + + ); +} +``` + +## Client configuration + +In order to be able to perform certain action (create-acknowledge-resolve an action), you need to provide the username of the user making the action. +The user supplied must be a valid Splunk On-Call user and a member of your organization. + +In `app-config.yaml`: + +```yaml +splunkOnCall: + username: +``` + +The user supplied must be a valid Splunk On-Call user and a member of your organization. + +In order to make the API calls, you need to provide a new proxy config which will redirect to the Splunk On-Call API endpoint and add authentication information in the headers: + +```yaml +# app-config.yaml +proxy: + # ... + '/splunk-on-call': + target: https://api.victorops.com/api-public + headers: + X-VO-Api-Id: + $env: SPLUNK_ON_CALL_API_ID + X-VO-Api-Key: + $env: SPLUNK_ON_CALL_API_KEY +``` + +In addition, to make certain API calls (trigger-resolve-acknowledge an incident) you need to add the `PATCH` method to the backend `cors` methods list: `[GET, POST, PUT, DELETE, PATCH]`. + +### Adding your team name to the entity annotation + +The information displayed for each entity is based on the team name. +If you want to use this plugin for an entity, you need to label it with the below annotation: + +```yaml +annotations: + splunk.com/on-call-team': +``` + +## Providing the API key and API id + +In order for the client to make requests to the [Splunk On-Call API](https://portal.victorops.com/public/api-docs.html#/) it needs an [API ID and an API Key](https://help.victorops.com/knowledge-base/api/). + +Then start the backend passing the values as an environment variable: + +```bash +$ SPLUNK_ON_CALL_API_KEY='' SPLUNK_ON_CALL_API_ID='' yarn start +``` + +This will proxy the request by adding `X-VO-Api-Id` and `X-VO-Api-Key` headers with the provided values. + +You can also add the values in your helm template: + +```yaml +# backend-secret.yaml +stringData: + # ... + SPLUNK_ON_CALL_API_ID: { { .Values.auth.splunkOnCallApiId } } + SPLUNK_ON_CALL_API_KEY: { { .Values.auth.splunkOnCallApiKey } } +``` + +To enable it you need to provide them in the chart's values: + +```yaml +# values.yaml +auth: + # ... + splunkOnCallApiId: h + splunkOnCallApiKey: h +``` diff --git a/plugins/splunk-on-call/dev/index.tsx b/plugins/splunk-on-call/dev/index.tsx new file mode 100644 index 0000000000..346c92c37e --- /dev/null +++ b/plugins/splunk-on-call/dev/index.tsx @@ -0,0 +1,26 @@ +/* + * 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 React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { splunkOnCallPlugin, SplunkOnCallPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(splunkOnCallPlugin) + .addPage({ + title: 'Splunk On-Call', + element: , + }) + .render(); diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json new file mode 100644 index 0000000000..c0542bd64c --- /dev/null +++ b/plugins/splunk-on-call/package.json @@ -0,0 +1,64 @@ +{ + "name": "@backstage/plugin-splunk-on-call", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/splunk-on-call" + }, + "keywords": [ + "backstage", + "splunk-on-call" + ], + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.1", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "classnames": "^2.2.6", + "luxon": "^1.25.0", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router-dom": "6.0.0-beta.0", + "react-use": "^15.3.3" + }, + "devDependencies": { + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.10", + "@backstage/test-utils": "^0.1.7", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/luxon": "^1.25.0", + "@types/node": "^12.0.0", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2", + "node-fetch": "^2.6.1" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts new file mode 100644 index 0000000000..501dad80a2 --- /dev/null +++ b/plugins/splunk-on-call/src/api/client.ts @@ -0,0 +1,204 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef, DiscoveryApi, ConfigApi } from '@backstage/core'; +import { + Incident, + OnCall, + User, + EscalationPolicyInfo, + Team, +} from '../components/types'; +import { + SplunkOnCallApi, + TriggerAlarmRequest, + IncidentsResponse, + OnCallsResponse, + ClientApiConfig, + RequestOptions, + ListUserResponse, + EscalationPolicyResponse, + PatchIncidentRequest, +} from './types'; + +export class UnauthorizedError extends Error {} + +export const splunkOnCallApiRef = createApiRef({ + id: 'plugin.splunk-on-call.api', + description: 'Used to fetch data from Splunk On-Call API', +}); + +export class SplunkOnCallClient implements SplunkOnCallApi { + static fromConfig(configApi: ConfigApi, discoveryApi: DiscoveryApi) { + const usernameFromConfig: string | null = + configApi.getOptionalString('splunkOnCall.username') || null; + return new SplunkOnCallClient({ + username: usernameFromConfig, + discoveryApi, + }); + } + constructor(private readonly config: ClientApiConfig) {} + + async getIncidents(): Promise { + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/incidents`; + + const { incidents } = await this.getByUrl(url); + + return incidents; + } + + async getOnCallUsers(): Promise { + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/oncall/current`; + const { teamsOnCall } = await this.getByUrl(url); + + return teamsOnCall; + } + + async getTeams(): Promise { + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/team`; + const teams = await this.getByUrl(url); + + return teams; + } + + async acknowledgeIncident({ + incidentNames, + }: PatchIncidentRequest): Promise { + const options = { + method: 'PATCH', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userName: this.config.username, + incidentNames, + }), + }; + + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/incidents/ack`; + + return this.request(url, options); + } + + async resolveIncident({ + incidentNames, + }: PatchIncidentRequest): Promise { + const options = { + method: 'PATCH', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ + userName: this.config.username, + incidentNames, + }), + }; + + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/incidents/resolve`; + + return this.request(url, options); + } + + async getUsers(): Promise { + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v2/user`; + const { users } = await this.getByUrl(url); + + return users; + } + + async getEscalationPolicies(): Promise { + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/policies`; + const { policies } = await this.getByUrl(url); + + return policies; + } + + async triggerAlarm({ + summary, + details, + userName, + targets, + isMultiResponder, + }: TriggerAlarmRequest): Promise { + const body = JSON.stringify({ + summary, + details, + userName: this.config.username || userName, + targets, + isMultiResponder, + }); + + const options = { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body, + }; + + const url = `${await this.config.discoveryApi.getBaseUrl( + 'proxy', + )}/splunk-on-call/v1/incidents`; + + return this.request(url, options); + } + + private async getByUrl(url: string): Promise { + const options = { + method: 'GET', + headers: { + 'Content-Type': 'application/json', + }, + }; + const response = await this.request(url, options); + + return response.json(); + } + + private async request( + url: string, + options: RequestOptions, + ): Promise { + const response = await fetch(url, options); + if (response.status === 403) { + throw new UnauthorizedError(); + } + if (!response.ok) { + const payload = await response.json(); + const errors = payload.errors.map((error: string) => error).join(' '); + const message = `Request failed with ${response.status}, ${errors}`; + throw new Error(message); + } + return response; + } +} diff --git a/plugins/splunk-on-call/src/api/index.ts b/plugins/splunk-on-call/src/api/index.ts new file mode 100644 index 0000000000..1e4056fbc0 --- /dev/null +++ b/plugins/splunk-on-call/src/api/index.ts @@ -0,0 +1,22 @@ +/* + * 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 { + SplunkOnCallClient, + splunkOnCallApiRef, + UnauthorizedError, +} from './client'; +export type { SplunkOnCallApi } from './types'; diff --git a/plugins/splunk-on-call/src/api/mocks.ts b/plugins/splunk-on-call/src/api/mocks.ts new file mode 100644 index 0000000000..86df04275d --- /dev/null +++ b/plugins/splunk-on-call/src/api/mocks.ts @@ -0,0 +1,100 @@ +/* + * 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 { + EscalationPolicyInfo, + Incident, + Team, + User, +} from '../components/types'; + +export const MOCKED_USER: User = { + createdAt: '2021-02-01T23:38:38Z', + displayName: 'Test User', + email: 'test@example.com', + firstName: 'FirstNameTest', + lastName: 'LastNameTest', + passwordLastUpdated: '2021-02-01T23:38:38Z', + username: 'test_user', + verified: true, + _selfUrl: '/api-public/v1/user/test_user', +}; + +export const MOCKED_ON_CALL = [ + { + team: { name: 'team_example', slug: 'team-zEalMCgwYSA0Lt40' }, + oncallNow: [ + { + escalationPolicy: { name: 'Example', slug: 'team-zEalMCgwYSA0Lt40' }, + users: [{ onCalluser: { username: 'test_user' } }], + }, + ], + }, +]; + +export const MOCK_INCIDENT: Incident = { + alertCount: 1, + currentPhase: 'ACKED', + entityDisplayName: 'test-incident', + entityId: 'entityId', + entityState: 'CRITICAL', + entityType: 'SERVICE', + incidentNumber: '1', + lastAlertId: 'lastAlertId', + lastAlertTime: '2021-02-03T00:13:11Z', + routingKey: 'routingdefault', + service: 'test', + startTime: '2021-02-03T00:13:11Z', + pagedTeams: ['team-O9SqT13fsnCstjMi'], + pagedUsers: [], + pagedPolicies: [ + { + policy: { + name: 'Generated Direct User Policy for test_user', + slug: 'directUserPolicySlug-test', + _selfUrl: '/test', + }, + }, + ], + transitions: [{ name: 'ACKED', at: '2021-02-03T01:20:00Z', by: 'test' }], + monitorName: 'vouser-user', + monitorType: 'Manual', + firstAlertUuid: 'firstAlertUuid', + incidentLink: 'https://portal.victorops.com/example', +}; + +export const MOCK_TEAM: Team = { + _selfUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi', + _membersUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/members', + _policiesUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/policies', + _adminsUrl: '/api-public/v1/team/team-O9SqT13fsnCstjMi/admins', + name: 'test', + slug: 'team-O9SqT13fsnCstjMi', + memberCount: 1, + version: 1, + isDefaultTeam: false, +}; + +export const ESCALATION_POLICIES: EscalationPolicyInfo[] = [ + { + policy: { + name: 'Example', + slug: 'team-zEalMCgwYSA0Lt40', + _selfUrl: '/api-public/v1/policies/team-zEalMCgwYSA0Lt40', + }, + team: { name: 'Example', slug: 'team-zEalMCgwYSA0Lt40' }, + }, +]; diff --git a/plugins/splunk-on-call/src/api/types.ts b/plugins/splunk-on-call/src/api/types.ts new file mode 100644 index 0000000000..500f2a6291 --- /dev/null +++ b/plugins/splunk-on-call/src/api/types.ts @@ -0,0 +1,117 @@ +/* + * 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 { + EscalationPolicyInfo, + Incident, + OnCall, + Team, + User, +} from '../components/types'; +import { DiscoveryApi } from '@backstage/core'; + +export enum TargetType { + UserValue = 'User', + EscalationPolicyValue = 'EscalationPolicy', +} + +export type IncidentTarget = { + type: TargetType; + slug: string; +}; + +export type TriggerAlarmRequest = { + targets: IncidentTarget[]; + details: string; + summary: string; + userName: string; + isMultiResponder?: boolean; +}; + +export interface SplunkOnCallApi { + /** + * Fetches a list of incidents + */ + getIncidents(): Promise; + + /** + * Fetches the list of users in an escalation policy. + */ + getOnCallUsers(): Promise; + + /** + * Triggers an incident to specific users and/or specific teams. + */ + triggerAlarm(request: TriggerAlarmRequest): Promise; + + /** + * Resolves an incident. + */ + resolveIncident(request: PatchIncidentRequest): Promise; + + /** + * Acknowledge an incident. + */ + acknowledgeIncident(request: PatchIncidentRequest): Promise; + + /** + * Get a list of users for your organization. + */ + getUsers(): Promise; + + /** + * Get a list of teams for your organization. + */ + getTeams(): Promise; + + /** + * Get a list of escalation policies for your organization. + */ + getEscalationPolicies(): Promise; +} + +export type PatchIncidentRequest = { + incidentNames: string[]; + message?: string; +}; + +export type EscalationPolicyResponse = { + policies: EscalationPolicyInfo[]; +}; + +export type ListUserResponse = { + users: User[]; + _selfUrl?: string; +}; + +export type IncidentsResponse = { + incidents: Incident[]; +}; + +export type OnCallsResponse = { + teamsOnCall: OnCall[]; +}; + +export type ClientApiConfig = { + username: string | null; + discoveryApi: DiscoveryApi; +}; + +export type RequestOptions = { + method: string; + headers: HeadersInit; + body?: BodyInit; +}; diff --git a/plugins/splunk-on-call/src/assets/emptystate.svg b/plugins/splunk-on-call/src/assets/emptystate.svg new file mode 100644 index 0000000000..fa7f19123e --- /dev/null +++ b/plugins/splunk-on-call/src/assets/emptystate.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/plugins/splunk-on-call/src/components/Errors/MissingApiKeyOrApiIdError.tsx b/plugins/splunk-on-call/src/components/Errors/MissingApiKeyOrApiIdError.tsx new file mode 100644 index 0000000000..ea72b6ca51 --- /dev/null +++ b/plugins/splunk-on-call/src/components/Errors/MissingApiKeyOrApiIdError.tsx @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EmptyState } from '@backstage/core'; +import { Button } from '@material-ui/core'; + +export const MissingApiKeyOrApiIdError = () => ( + + Read More + + } + /> +); diff --git a/plugins/splunk-on-call/src/components/Errors/index.ts b/plugins/splunk-on-call/src/components/Errors/index.ts new file mode 100644 index 0000000000..9698b0bd14 --- /dev/null +++ b/plugins/splunk-on-call/src/components/Errors/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { MissingApiKeyOrApiIdError } from './MissingApiKeyOrApiIdError'; diff --git a/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx new file mode 100644 index 0000000000..41157a2b5e --- /dev/null +++ b/plugins/splunk-on-call/src/components/Escalation/Escalation.test.tsx @@ -0,0 +1,100 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { EscalationPolicy } from './EscalationPolicy'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { splunkOnCallApiRef } from '../../api'; +import { MOCKED_ON_CALL, MOCKED_USER } from '../../api/mocks'; + +const mockSplunkOnCallApi = { + getOnCallUsers: () => [], +}; +const apis = ApiRegistry.from([[splunkOnCallApiRef, mockSplunkOnCallApi]]); + +describe('Escalation', () => { + it('Handles an empty response', async () => { + mockSplunkOnCallApi.getOnCallUsers = jest + .fn() + .mockImplementationOnce(async () => []); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + expect(mockSplunkOnCallApi.getOnCallUsers).toHaveBeenCalled(); + }); + + it('Render a list of users', async () => { + mockSplunkOnCallApi.getOnCallUsers = jest + .fn() + .mockImplementationOnce(async () => MOCKED_ON_CALL); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect(getByText('FirstNameTest LastNameTest')).toBeInTheDocument(); + expect(getByText('test@example.com')).toBeInTheDocument(); + expect(mockSplunkOnCallApi.getOnCallUsers).toHaveBeenCalled(); + }); + + it('Handles errors', async () => { + mockSplunkOnCallApi.getOnCallUsers = jest + .fn() + .mockRejectedValueOnce(new Error('Error message')); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect( + getByText('Error encountered while fetching information. Error message'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx b/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx new file mode 100644 index 0000000000..9538a0e73b --- /dev/null +++ b/plugins/splunk-on-call/src/components/Escalation/EscalationPolicy.tsx @@ -0,0 +1,77 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { List, ListSubheader } from '@material-ui/core'; +import { EscalationUsersEmptyState } from './EscalationUsersEmptyState'; +import { EscalationUser } from './EscalationUser'; +import { useAsync } from 'react-use'; +import { splunkOnCallApiRef } from '../../api'; +import { useApi, Progress } from '@backstage/core'; +import { Alert } from '@material-ui/lab'; +import { User } from '../types'; + +type Props = { + users: { [key: string]: User }; + team: string; +}; + +export const EscalationPolicy = ({ users, team }: Props) => { + const api = useApi(splunkOnCallApiRef); + + const { value: userNames, loading, error } = useAsync(async () => { + const oncalls = await api.getOnCallUsers(); + const teamUsernames = oncalls + .filter(oncall => oncall.team?.name === team) + .flatMap(oncall => { + return oncall.oncallNow?.flatMap(oncallNow => { + return oncallNow.users?.flatMap(user => { + return user?.onCalluser?.username; + }); + }); + }); + return teamUsernames; + }); + + if (error) { + return ( + + Error encountered while fetching information. {error.message} + + ); + } + + if (loading) { + return ; + } + + if (!userNames?.length) { + return ; + } + + return ( + ON CALL}> + {userNames && + userNames.map( + (userName, index) => + userName && + userName in users && ( + + ), + )} + + ); +}; diff --git a/plugins/splunk-on-call/src/components/Escalation/EscalationUser.tsx b/plugins/splunk-on-call/src/components/Escalation/EscalationUser.tsx new file mode 100644 index 0000000000..4df0719696 --- /dev/null +++ b/plugins/splunk-on-call/src/components/Escalation/EscalationUser.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + ListItem, + ListItemIcon, + ListItemSecondaryAction, + Tooltip, + ListItemText, + makeStyles, + IconButton, + Typography, +} from '@material-ui/core'; +import Avatar from '@material-ui/core/Avatar'; +import EmailIcon from '@material-ui/icons/Email'; +import { User } from '../types'; + +const useStyles = makeStyles({ + listItemPrimary: { + fontWeight: 'bold', + }, +}); + +type Props = { + user: User; +}; + +export const EscalationUser = ({ user }: Props) => { + const classes = useStyles(); + + return ( + + + + + + {user.firstName} {user.lastName} + + } + secondary={user.email} + /> + + + + + + + + + ); +}; diff --git a/plugins/splunk-on-call/src/components/Escalation/EscalationUsersEmptyState.tsx b/plugins/splunk-on-call/src/components/Escalation/EscalationUsersEmptyState.tsx new file mode 100644 index 0000000000..d587011601 --- /dev/null +++ b/plugins/splunk-on-call/src/components/Escalation/EscalationUsersEmptyState.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + ListItem, + ListItemIcon, + ListItemText, + makeStyles, +} from '@material-ui/core'; +import { StatusWarning } from '@backstage/core'; + +const useStyles = makeStyles({ + denseListIcon: { + marginRight: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, +}); + +export const EscalationUsersEmptyState = () => { + const classes = useStyles(); + return ( + + +
+ +
+
+ +
+ ); +}; diff --git a/plugins/splunk-on-call/src/components/Escalation/index.ts b/plugins/splunk-on-call/src/components/Escalation/index.ts new file mode 100644 index 0000000000..ac2db62cd9 --- /dev/null +++ b/plugins/splunk-on-call/src/components/Escalation/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { EscalationPolicy } from './EscalationPolicy'; diff --git a/plugins/splunk-on-call/src/components/Incident/IncidentEmptyState.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentEmptyState.tsx new file mode 100644 index 0000000000..f7a0398c55 --- /dev/null +++ b/plugins/splunk-on-call/src/components/Incident/IncidentEmptyState.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Grid, Typography } from '@material-ui/core'; +import EmptyStateImage from '../../assets/emptystate.svg'; + +export const IncidentsEmptyState = () => { + return ( + + + Nice! No incidents found! + + + EmptyState + + + ); +}; diff --git a/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx new file mode 100644 index 0000000000..1bf233de86 --- /dev/null +++ b/plugins/splunk-on-call/src/components/Incident/IncidentListItem.tsx @@ -0,0 +1,241 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect } from 'react'; +import { + ListItem, + ListItemIcon, + ListItemSecondaryAction, + Tooltip, + ListItemText, + makeStyles, + IconButton, + Typography, +} from '@material-ui/core'; +import DoneIcon from '@material-ui/icons/Done'; +import DoneAllIcon from '@material-ui/icons/DoneAll'; +import { + StatusError, + StatusWarning, + StatusOK, + useApi, + alertApiRef, +} from '@backstage/core'; +import { DateTime, Duration } from 'luxon'; +import { Incident, IncidentPhase } from '../types'; +import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; +import { splunkOnCallApiRef } from '../../api/client'; +import { useAsyncFn } from 'react-use'; +import { PatchIncidentRequest } from '../../api/types'; + +const useStyles = makeStyles({ + denseListIcon: { + marginRight: 0, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + justifyContent: 'center', + }, + listItemPrimary: { + fontWeight: 'bold', + }, + listItemIcon: { + minWidth: '1em', + }, + secondaryAction: { + paddingRight: 48, + }, +}); + +type Props = { + incident: Incident; + onIncidentAction: () => void; +}; + +const IncidentPhaseStatus = ({ + currentPhase, +}: { + currentPhase: IncidentPhase; +}) => { + switch (currentPhase) { + case 'UNACKED': + return ; + case 'ACKED': + return ; + default: + return ; + } +}; + +const incidentPhaseTooltip = (currentPhase: IncidentPhase) => { + switch (currentPhase) { + case 'UNACKED': + return 'Triggered'; + case 'ACKED': + return 'Acknowledged'; + default: + return 'Resolved'; + } +}; + +const IncidentAction = ({ + currentPhase, + incidentNames, + resolveAction, + acknowledgeAction, +}: { + currentPhase: string; + incidentNames: string[]; + resolveAction: (args: PatchIncidentRequest) => void; + acknowledgeAction: (args: PatchIncidentRequest) => void; +}) => { + switch (currentPhase) { + case 'UNACKED': + return ( + + acknowledgeAction({ incidentNames })}> + + + + ); + case 'ACKED': + return ( + + resolveAction({ incidentNames })}> + + + + ); + default: + return <>; + } +}; + +export const IncidentListItem = ({ incident, onIncidentAction }: Props) => { + const classes = useStyles(); + const duration = + new Date().getTime() - new Date(incident.startTime!).getTime(); + const createdAt = DateTime.local() + .minus(Duration.fromMillis(duration)) + .toRelative({ locale: 'en' }); + const alertApi = useApi(alertApiRef); + const api = useApi(splunkOnCallApiRef); + + const hasBeenManuallyTriggered = incident.monitorName?.includes('vouser-'); + + const user = hasBeenManuallyTriggered + ? incident.monitorName?.replace('vouser-', '') + : incident.monitorName; + + const [ + { value: resolveValue, error: resolveError }, + handleResolveIncident, + ] = useAsyncFn( + async ({ incidentNames }: PatchIncidentRequest) => + await api.resolveIncident({ + incidentNames, + }), + ); + + const [ + { value: acknowledgeValue, error: acknowledgeError }, + handleAcknowledgeIncident, + ] = useAsyncFn( + async ({ incidentNames }: PatchIncidentRequest) => + await api.acknowledgeIncident({ + incidentNames, + }), + ); + + useEffect(() => { + if (acknowledgeValue) { + alertApi.post({ + message: `Incident successfully acknowledged`, + }); + } + + if (resolveValue) { + alertApi.post({ + message: `Incident successfully resolved`, + }); + } + if (resolveValue || acknowledgeValue) { + onIncidentAction(); + } + }, [acknowledgeValue, resolveValue, alertApi, onIncidentAction]); + + if (acknowledgeError) { + alertApi.post({ + message: `Failed to acknowledge incident. ${acknowledgeError.message}`, + severity: 'error', + }); + } + + if (resolveError) { + alertApi.post({ + message: `Failed to resolve incident. ${resolveError.message}`, + severity: 'error', + }); + } + + return ( + + + +
+ +
+
+
+ + Created {createdAt} {user && `by ${user}`} + + } + /> + + {incident.incidentLink && incident.incidentNumber && ( + + + + + + + + + )} +
+ ); +}; diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx new file mode 100644 index 0000000000..d1f973735c --- /dev/null +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.test.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render, waitFor } from '@testing-library/react'; +import { Incidents } from './Incidents'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { + alertApiRef, + ApiProvider, + ApiRegistry, + createApiRef, + IdentityApi, + identityApiRef, +} from '@backstage/core'; +import { splunkOnCallApiRef } from '../../api'; +import { MOCK_TEAM, MOCK_INCIDENT } from '../../api/mocks'; + +const mockIdentityApi: Partial = { + getUserId: () => 'test', +}; + +const mockSplunkOnCallApi = { + getIncidents: () => [], + getTeams: () => [], +}; +const apis = ApiRegistry.from([ + [ + alertApiRef, + createApiRef({ + id: 'core.alert', + description: 'Used to report alerts and forward them to the app', + }), + ], + [identityApiRef, mockIdentityApi], + [splunkOnCallApiRef, mockSplunkOnCallApi], +]); + +describe('Incidents', () => { + it('Renders an empty state when there are no incidents', async () => { + mockSplunkOnCallApi.getTeams = jest + .fn() + .mockImplementationOnce(async () => [MOCK_TEAM]); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + }); + + it('Renders all incidents', async () => { + mockSplunkOnCallApi.getIncidents = jest + .fn() + .mockImplementationOnce(async () => [MOCK_INCIDENT]); + + mockSplunkOnCallApi.getTeams = jest + .fn() + .mockImplementationOnce(async () => [MOCK_TEAM]); + const { + getByText, + getByTitle, + getAllByTitle, + getByLabelText, + queryByTestId, + } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByText('user', { + exact: false, + }), + ).toBeInTheDocument(); + expect(getByText('test-incident')).toBeInTheDocument(); + expect(getByTitle('Acknowledged')).toBeInTheDocument(); + expect(getByLabelText('Status warning')).toBeInTheDocument(); + + // assert links, mailto and hrefs, date calculation + expect(getAllByTitle('View in Splunk On-Call').length).toEqual(1); + }); + + it('Handle errors', async () => { + mockSplunkOnCallApi.getIncidents = jest + .fn() + .mockRejectedValueOnce(new Error('Error occurred')); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByText('Error encountered while fetching information. Error occurred'), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/splunk-on-call/src/components/Incident/Incidents.tsx b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx new file mode 100644 index 0000000000..e1a79314b9 --- /dev/null +++ b/plugins/splunk-on-call/src/components/Incident/Incidents.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useEffect } from 'react'; +import { List, ListSubheader } from '@material-ui/core'; +import { IncidentListItem } from './IncidentListItem'; +import { IncidentsEmptyState } from './IncidentEmptyState'; +import { useAsyncFn } from 'react-use'; +import { splunkOnCallApiRef } from '../../api'; +import { useApi, Progress } from '@backstage/core'; +import { Alert } from '@material-ui/lab'; + +type Props = { + refreshIncidents: boolean; + team: string; +}; + +export const Incidents = ({ refreshIncidents, team }: Props) => { + const api = useApi(splunkOnCallApiRef); + + const [{ value: incidents, loading, error }, getIncidents] = useAsyncFn( + async () => { + const allIncidents = await api.getIncidents(); + const teams = await api.getTeams(); + const teamSlug = teams.find(teamValue => teamValue.name === team)?.slug; + const filteredIncidents = teamSlug + ? allIncidents.filter(incident => + incident.pagedTeams?.includes(teamSlug), + ) + : []; + return filteredIncidents; + }, + ); + + useEffect(() => { + getIncidents(); + }, [refreshIncidents, getIncidents]); + + if (error) { + return ( + + Error encountered while fetching information. {error.message} + + ); + } + + if (loading) { + return ; + } + + if (!incidents?.length) { + return ; + } + + return ( + INCIDENTS}> + {incidents!.map((incident, index) => ( + getIncidents()} + key={index} + incident={incident} + /> + ))} + + ); +}; diff --git a/plugins/splunk-on-call/src/components/Incident/index.ts b/plugins/splunk-on-call/src/components/Incident/index.ts new file mode 100644 index 0000000000..fb2702602b --- /dev/null +++ b/plugins/splunk-on-call/src/components/Incident/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Incidents } from './Incidents'; diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx new file mode 100644 index 0000000000..b1280ab21c --- /dev/null +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.test.tsx @@ -0,0 +1,155 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render, waitFor, fireEvent, act } from '@testing-library/react'; +import { SplunkOnCallCard } from './SplunkOnCallCard'; +import { Entity } from '@backstage/catalog-model'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { + alertApiRef, + ApiProvider, + ApiRegistry, + ConfigApi, + configApiRef, + ConfigReader, + createApiRef, +} from '@backstage/core'; +import { + splunkOnCallApiRef, + UnauthorizedError, + SplunkOnCallClient, +} from '../api'; +import { + ESCALATION_POLICIES, + MOCKED_ON_CALL, + MOCKED_USER, + MOCK_INCIDENT, + MOCK_TEAM, +} from '../api/mocks'; + +const mockSplunkOnCallApi: Partial = { + getUsers: async () => [], + getIncidents: async () => [MOCK_INCIDENT], + getOnCallUsers: async () => MOCKED_ON_CALL, + getTeams: async () => [MOCK_TEAM], + getEscalationPolicies: async () => ESCALATION_POLICIES, +}; + +const configApi: ConfigApi = new ConfigReader({ + splunkOnCall: { + username: MOCKED_USER.username, + }, +}); + +const apis = ApiRegistry.from([ + [splunkOnCallApiRef, mockSplunkOnCallApi], + [configApiRef, configApi], + [ + alertApiRef, + createApiRef({ + id: 'core.alert', + description: 'Used to report alerts and forward them to the app', + }), + ], +]); +const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'splunkoncall-test', + annotations: { + 'splunk.com/on-call-team': 'Example', + }, + }, +}; + +describe('SplunkOnCallCard', () => { + it('Render splunkoncall', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockImplementationOnce(async () => [MOCKED_USER]); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Create Incident')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + }); + + it('Handles custom error for missing token', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockRejectedValueOnce(new UnauthorizedError()); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect( + getByText('Missing or invalid Splunk On-Call API key and/or API id'), + ).toBeInTheDocument(); + }); + + it('handles general error', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockRejectedValueOnce(new Error('An error occurred')); + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + + expect( + getByText( + 'Error encountered while fetching information. An error occurred', + ), + ).toBeInTheDocument(); + }); + it('opens the dialog when trigger button is clicked', async () => { + mockSplunkOnCallApi.getUsers = jest + .fn() + .mockImplementationOnce(async () => [MOCKED_USER]); + + const { getByText, queryByTestId, getByTestId, getByRole } = render( + wrapInTestApp( + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Create Incident')).toBeInTheDocument(); + const triggerButton = getByTestId('trigger-button'); + await act(async () => { + fireEvent.click(triggerButton); + }); + expect(getByRole('dialog')).toBeInTheDocument(); + }); +}); diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx new file mode 100644 index 0000000000..84d94d60d3 --- /dev/null +++ b/plugins/splunk-on-call/src/components/SplunkOnCallCard.tsx @@ -0,0 +1,194 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState, useCallback } from 'react'; +import { + useApi, + Progress, + HeaderIconLinkRow, + MissingAnnotationEmptyState, + configApiRef, + EmptyState, +} from '@backstage/core'; +import { Entity } from '@backstage/catalog-model'; +import { + Button, + makeStyles, + Card, + CardHeader, + Divider, + CardContent, + Typography, +} from '@material-ui/core'; +import { Incidents } from './Incident'; +import { EscalationPolicy } from './Escalation'; +import { useAsync } from 'react-use'; +import { Alert } from '@material-ui/lab'; +import { splunkOnCallApiRef, UnauthorizedError } from '../api'; +import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; +import { TriggerDialog } from './TriggerDialog'; +import { MissingApiKeyOrApiIdError } from './Errors/MissingApiKeyOrApiIdError'; +import { User } from './types'; + +const useStyles = makeStyles({ + triggerAlarm: { + paddingTop: 0, + paddingBottom: 0, + fontSize: '0.7rem', + textTransform: 'uppercase', + fontWeight: 600, + letterSpacing: 1.2, + lineHeight: 1.5, + '&:hover, &:focus, &.focus': { + backgroundColor: 'transparent', + textDecoration: 'none', + }, + }, +}); + +export const SPLUNK_ON_CALL_TEAM = 'splunk.com/on-call-team'; + +export const MissingTeamAnnotation = () => ( + +); + +export const MissingUsername = () => ( + + + +); + +export const isPluginApplicableToEntity = (entity: Entity) => + Boolean(entity.metadata.annotations?.[SPLUNK_ON_CALL_TEAM]); + +type Props = { + entity: Entity; +}; + +export const SplunkOnCallCard = ({ entity }: Props) => { + const classes = useStyles(); + const config = useApi(configApiRef); + const api = useApi(splunkOnCallApiRef); + const [showDialog, setShowDialog] = useState(false); + const [refreshIncidents, setRefreshIncidents] = useState(false); + const team = entity.metadata.annotations![SPLUNK_ON_CALL_TEAM]; + + const username = config.getOptionalString('splunkOnCall.username'); + + const handleRefresh = useCallback(() => { + setRefreshIncidents(x => !x); + }, []); + + const handleDialog = useCallback(() => { + setShowDialog(x => !x); + }, []); + + const { value: users, loading, error } = useAsync(async () => { + const allUsers = await api.getUsers(); + const usersHashMap = allUsers.reduce( + (map: Record, obj: User) => { + if (obj.username) { + map[obj.username] = obj; + } + return map; + }, + {}, + ); + return { usersHashMap, userList: allUsers }; + }); + + const incidentCreator = + username && users?.userList.find(user => user.username === username); + + if (error instanceof UnauthorizedError) { + return ; + } + + if (error) { + return ( + + Error encountered while fetching information. {error.message} + + ); + } + + if (loading) { + return ; + } + + const Content = () => { + if (!team) { + return ; + } + if (!username || !incidentCreator) { + return ; + } + + return ( + <> + + {users?.usersHashMap && team && ( + + )} + {users && incidentCreator && ( + + )} + + ); + }; + + const triggerLink = { + label: 'Create Incident', + action: ( + + ), + icon: , + }; + + return ( + + Team: {team}, + username && ( + + ), + ]} + /> + + + + + + ); +}; diff --git a/plugins/splunk-on-call/src/components/SplunkOnCallPage.tsx b/plugins/splunk-on-call/src/components/SplunkOnCallPage.tsx new file mode 100644 index 0000000000..4222cbfbeb --- /dev/null +++ b/plugins/splunk-on-call/src/components/SplunkOnCallPage.tsx @@ -0,0 +1,72 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Grid, makeStyles } from '@material-ui/core'; +import { + Content, + ContentHeader, + Page, + Header, + SupportButton, +} from '@backstage/core'; +import { SplunkOnCallCard } from './SplunkOnCallCard'; +import { useEntity } from '@backstage/plugin-catalog-react'; + +const useStyles = makeStyles(() => ({ + overflowXScroll: { + overflowX: 'scroll', + }, +})); + +export type SplunkOnCallPageProps = { + title?: string; + subtitle?: string; + pageTitle?: string; +}; + +export const SplunkOnCallPage = ({ + title, + subtitle, + pageTitle, +}: SplunkOnCallPageProps): JSX.Element => { + const classes = useStyles(); + const { entity } = useEntity(); + + return ( + +
+ + + + This is used to help you automate incident management. + + + + + + + + + + ); +}; + +SplunkOnCallPage.defaultProps = { + title: 'Splunk On-Call', + subtitle: 'Automate incident management', + pageTitle: 'Dashboard', +}; diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx new file mode 100644 index 0000000000..741a15fb41 --- /dev/null +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render, fireEvent, act } from '@testing-library/react'; +import { wrapInTestApp } from '@backstage/test-utils'; +import { + ApiRegistry, + alertApiRef, + createApiRef, + ApiProvider, +} from '@backstage/core'; +import { splunkOnCallApiRef } from '../../api'; +import { TriggerDialog } from './TriggerDialog'; +import { ESCALATION_POLICIES, MOCKED_USER } from '../../api/mocks'; + +describe('TriggerDialog', () => { + const mockTriggerAlarmFn = jest.fn(); + const mockSplunkOnCallApi = { + triggerAlarm: mockTriggerAlarmFn, + getEscalationPolicies: async () => ESCALATION_POLICIES, + }; + + const apis = ApiRegistry.from([ + [ + alertApiRef, + createApiRef({ + id: 'core.alert', + description: 'Used to report alerts and forward them to the app', + }), + ], + [splunkOnCallApiRef, mockSplunkOnCallApi], + ]); + + it('open the dialog and trigger an alarm', async () => { + const { getByText, getByRole, getAllByRole, getByTestId } = render( + wrapInTestApp( + + {}} + users={[MOCKED_USER]} + onIncidentCreated={() => {}} + /> + , + ), + ); + + expect(getByRole('dialog')).toBeInTheDocument(); + expect( + getByText('This action will trigger an incident', { + exact: false, + }), + ).toBeInTheDocument(); + const summary = getByTestId('trigger-summary-input'); + const body = getByTestId('trigger-body-input'); + const behavior = getByTestId('trigger-select-behavior'); + const description = 'Test Trigger Alarm'; + await act(async () => { + fireEvent.change(summary, { target: { value: description } }); + fireEvent.change(body, { target: { value: description } }); + fireEvent.change(behavior, { target: { value: '0' } }); + fireEvent.mouseDown(getAllByRole('button')[0]); + }); + + // Trigger user targets select + const options = getAllByRole('option'); + await act(async () => { + fireEvent.click(options[0]); + fireEvent.keyDown(options[0], { + key: 'Escape', + code: 'Escape', + keyCode: 27, + charCode: 27, + }); + }); + + // Trigger policy targets select + await act(async () => { + fireEvent.mouseDown(getAllByRole('button')[1]); + }); + const policiesOptions = getAllByRole('option'); + await act(async () => { + fireEvent.click(policiesOptions[0]); + }); + + // Trigger incident creation button + const triggerButton = getByTestId('trigger-button'); + await act(async () => { + fireEvent.click(triggerButton); + }); + expect(mockTriggerAlarmFn).toHaveBeenCalled(); + expect(mockTriggerAlarmFn).toHaveBeenCalledWith({ + summary: description, + details: description, + userName: 'test_user', + targets: [ + { slug: 'test_user', type: 'User' }, + { slug: 'team-zEalMCgwYSA0Lt40', type: 'EscalationPolicy' }, + ], + isMultiResponder: false, + }); + }); +}); diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx new file mode 100644 index 0000000000..8132db985b --- /dev/null +++ b/plugins/splunk-on-call/src/components/TriggerDialog/TriggerDialog.tsx @@ -0,0 +1,365 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { useState, useEffect } from 'react'; +import { + Dialog, + DialogTitle, + TextField, + DialogActions, + Button, + DialogContent, + Typography, + CircularProgress, + Select, + MenuItem, + Input, + Chip, + createStyles, + makeStyles, + Theme, + FormControl, + InputLabel, +} from '@material-ui/core'; +import { useApi, alertApiRef } from '@backstage/core'; +import { useAsync, useAsyncFn } from 'react-use'; +import { splunkOnCallApiRef } from '../../api'; +import { Alert } from '@material-ui/lab'; +import { User } from '../types'; +import { IncidentTarget, TargetType } from '../../api/types'; + +const MenuProps = { + PaperProps: { + style: { + width: 250, + }, + }, +}; + +type Props = { + users: User[]; + incidentCreator: User; + showDialog: boolean; + handleDialog: () => void; + onIncidentCreated: () => void; +}; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + chips: { + display: 'flex', + flexWrap: 'wrap', + }, + chip: { + margin: 2, + }, + formControl: { + margin: theme.spacing(1), + minWidth: `calc(100% - ${theme.spacing(2)}px)`, + }, + targets: { + display: 'flex', + flexDirection: 'column', + width: '100%', + }, + }), +); + +export const TriggerDialog = ({ + users, + incidentCreator, + showDialog, + handleDialog, + onIncidentCreated: onIncidentCreated, +}: Props) => { + const alertApi = useApi(alertApiRef); + const api = useApi(splunkOnCallApiRef); + const classes = useStyles(); + + const [userTargets, setUserTargets] = useState([]); + const [policyTargets, setPolicyTargets] = useState([]); + const [detailsValue, setDetails] = useState(''); + const [summaryValue, setSummary] = useState(''); + const [isMultiResponderValue, setIsMultiResponder] = useState('1'); + + const [ + { value, loading: triggerLoading, error: triggerError }, + handleTriggerAlarm, + ] = useAsyncFn( + async ( + summary: string, + details: string, + userName: string, + targets: IncidentTarget[], + isMultiResponder: boolean, + ) => + await api.triggerAlarm({ + summary, + details, + userName, + targets, + isMultiResponder, + }), + ); + + const { + value: policies, + loading: policiesLoaading, + error: policiesError, + } = useAsync(async () => { + const allPolicies = await api.getEscalationPolicies(); + return allPolicies; + }); + + const handleUserTargets = (event: React.ChangeEvent<{ value: unknown }>) => { + setUserTargets(event.target.value as string[]); + }; + + const handlePolicyTargets = ( + event: React.ChangeEvent<{ value: unknown }>, + ) => { + setPolicyTargets(event.target.value as string[]); + }; + + const detailsChanged = (event: React.ChangeEvent) => { + setDetails(event.target.value); + }; + + const summaryChanged = (event: React.ChangeEvent) => { + setSummary(event.target.value); + }; + + const isMultiResponderChanged = ( + event: React.ChangeEvent<{ value: unknown }>, + ) => { + setIsMultiResponder(event.target.value as string); + }; + + const targets = (): IncidentTarget[] => [ + ...userTargets.map(user => ({ slug: user, type: TargetType.UserValue })), + ...policyTargets.map(user => ({ + slug: user, + type: TargetType.EscalationPolicyValue, + })), + ]; + + useEffect(() => { + if (value) { + alertApi.post({ + message: `Alarm successfully triggered`, + }); + onIncidentCreated(); + handleDialog(); + } + }, [value, alertApi, handleDialog, onIncidentCreated]); + + if (triggerError) { + alertApi.post({ + message: `Failed to trigger alarm. ${triggerError.message}`, + severity: 'error', + }); + } + + return ( + + This action will trigger an incident + + + Created by:{' '} + + {incidentCreator?.firstName} {incidentCreator?.lastName} + + + + + If the issue you are seeing does not need urgent attention, please + get in touch with the responsible team using their preferred + communications channel. You can find information about the owner of + this entity in the "About" card. If the issue is urgent, please + don't hesitate to trigger the alert. + + + + Please describe the problem you want to report. Be as descriptive as + possible. Your signed in user and a reference to the current page will + automatically be amended to the alarm so that the receiver can reach + out to you if necessary. + +
+ + Select the targets + +
+ + Select Users + + + + Select Teams / Policies + + +
+
+ + Acknowledge Behavior + + + + + + +
+ + + + +
+ ); +}; diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/index.ts b/plugins/splunk-on-call/src/components/TriggerDialog/index.ts new file mode 100644 index 0000000000..655cef8504 --- /dev/null +++ b/plugins/splunk-on-call/src/components/TriggerDialog/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { TriggerDialog } from './TriggerDialog'; diff --git a/plugins/splunk-on-call/src/components/types.ts b/plugins/splunk-on-call/src/components/types.ts new file mode 100644 index 0000000000..de5718cb28 --- /dev/null +++ b/plugins/splunk-on-call/src/components/types.ts @@ -0,0 +1,129 @@ +/* + * 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 { IncidentTarget } from '../api/types'; + +export type Team = { + name?: string; + slug?: string; + memberCount?: number; + version?: number; + isDefaultTeam?: boolean; + _selfUrl?: string; + _policiesUrl?: string; + _membersUrl?: string; + _adminsUrl?: string; +}; + +export type OnCall = { + team?: OnCallTeamResource; + oncallNow?: OnCallNowResource[]; +}; + +export type OnCallTeamResource = { + name?: string; + slug?: string; +}; + +export type OnCallNowResource = { + escalationPolicy?: OnCallEscalationPolicyResource; + users?: OnCallUsersResource[]; +}; + +export type OnCallEscalationPolicyResource = { + name?: string; + slug?: string; +}; + +export type OnCallUsersResource = { + onCalluser?: OnCallUser; +}; + +export type OnCallUser = { + username?: string; +}; + +export type User = { + firstName?: string; + lastName?: string; + displayName?: string; + username?: string; + email?: string; + createdAt?: string; + passwordLastUpdated?: string; + verified?: boolean; + _selfUrl?: string; +}; + +export type CreateIncidentRequest = { + summary: string; + details: string; + userName: string; + targets: IncidentTarget; + isMultiResponder: boolean; +}; + +export type IncidentPhase = 'UNACKED' | 'ACKED' | 'RESOLVED'; + +export type Incident = { + incidentNumber?: string; + startTime?: string; + currentPhase: IncidentPhase; + entityState?: string; + entityType?: string; + routingKey?: string; + alertCount?: number; + lastAlertTime?: string; + lastAlertId?: string; + entityId?: string; + host?: string; + service?: string; + pagedUsers?: string[]; + pagedTeams?: string[]; + entityDisplayName?: string; + pagedPolicies?: EscalationPolicyInfo[]; + transitions?: IncidentTransition[]; + firstAlertUuid?: string; + monitorName?: string; + monitorType?: string; + incidentLink?: string; +}; + +export type EscalationPolicyInfo = { + policy: EscalationPolicySummary; + team?: EscalationPolicyTeam; +}; + +export type IncidentTransition = { + name?: string; + at?: string; + by?: string; + message?: string; + manually?: boolean; + alertId?: string; + alertUrl?: string; +}; + +export type EscalationPolicySummary = { + name: string; + slug: string; + _selfUrl: string; +}; + +export type EscalationPolicyTeam = { + name: string; + slug: string; +}; diff --git a/plugins/splunk-on-call/src/index.ts b/plugins/splunk-on-call/src/index.ts new file mode 100644 index 0000000000..c8de96f8ea --- /dev/null +++ b/plugins/splunk-on-call/src/index.ts @@ -0,0 +1,29 @@ +/* + * 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 { + splunkOnCallPlugin, + splunkOnCallPlugin as plugin, + SplunkOnCallPage, +} from './plugin'; +export { + isPluginApplicableToEntity, + SplunkOnCallCard, +} from './components/SplunkOnCallCard'; +export { + SplunkOnCallClient, + splunkOnCallApiRef, + UnauthorizedError, +} from './api/client'; diff --git a/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx b/plugins/splunk-on-call/src/plugin.test.ts similarity index 58% rename from plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx rename to plugins/splunk-on-call/src/plugin.test.ts index b539753a95..b846eba706 100644 --- a/plugins/api-docs/src/components/EmptyState/MissingProvidesApisEmptyState.test.tsx +++ b/plugins/splunk-on-call/src/plugin.test.ts @@ -13,16 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { splunkOnCallPlugin } from './plugin'; -import { renderInTestApp } from '@backstage/test-utils'; -import React from 'react'; -import { MissingProvidesApisEmptyState } from './MissingProvidesApisEmptyState'; - -describe('', () => { - it('renders without exploding', async () => { - const { getByText } = await renderInTestApp( - , - ); - expect(getByText(/providesApis:/i)).toBeInTheDocument(); +describe('splunk-on-call', () => { + it('should export plugin', () => { + expect(splunkOnCallPlugin).toBeDefined(); }); }); diff --git a/plugins/splunk-on-call/src/plugin.ts b/plugins/splunk-on-call/src/plugin.ts new file mode 100644 index 0000000000..835cda48df --- /dev/null +++ b/plugins/splunk-on-call/src/plugin.ts @@ -0,0 +1,51 @@ +/* + * 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 { + createApiFactory, + createPlugin, + createRouteRef, + discoveryApiRef, + configApiRef, + createRoutableExtension, +} from '@backstage/core'; +import { splunkOnCallApiRef, SplunkOnCallClient } from './api'; + +export const rootRouteRef = createRouteRef({ + title: 'splunk-on-call', +}); + +export const splunkOnCallPlugin = createPlugin({ + id: 'splunk-on-call', + apis: [ + createApiFactory({ + api: splunkOnCallApiRef, + deps: { discoveryApi: discoveryApiRef, configApi: configApiRef }, + factory: ({ configApi, discoveryApi }) => + SplunkOnCallClient.fromConfig(configApi, discoveryApi), + }), + ], + routes: { + root: rootRouteRef, + }, +}); + +export const SplunkOnCallPage = splunkOnCallPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/SplunkOnCallPage').then(m => m.SplunkOnCallPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/splunk-on-call/src/setupTests.ts b/plugins/splunk-on-call/src/setupTests.ts new file mode 100644 index 0000000000..0bfa67b49a --- /dev/null +++ b/plugins/splunk-on-call/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 8c54a2b1e4..51f4d212ba 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -130,6 +130,7 @@ export class DocsBuilder { outputDir, dockerClient: this.dockerClient, parsedLocationAnnotation, + etag, }); this.logger.debug(`Generated files temporarily stored at ${outputDir}`); diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index afd829d94d..32983270ad 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -47,7 +47,7 @@ "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", - "sanitize-html": "^1.27.0" + "sanitize-html": "^2.3.2" }, "devDependencies": { "@backstage/cli": "^0.6.0", diff --git a/scripts/check-type-dependencies.js b/scripts/check-type-dependencies.js index 6ee4626a21..f3862214a1 100755 --- a/scripts/check-type-dependencies.js +++ b/scripts/check-type-dependencies.js @@ -24,8 +24,8 @@ const chalk = require('chalk'); async function main() { // This is from lerna, and cba polluting root package.json // eslint-disable-next-line import/no-extraneous-dependencies - const LernaProject = require('@lerna/project'); - const project = new LernaProject(resolvePath('.')); + const { Project } = require('@lerna/project'); + const project = new Project(resolvePath('.')); const packages = await project.getPackages(); let hadErrors = false; diff --git a/scripts/isolated-release.js b/scripts/isolated-release.js index 03c4e632c7..27dfd35b07 100755 --- a/scripts/isolated-release.js +++ b/scripts/isolated-release.js @@ -18,7 +18,7 @@ const path = require('path'); const childProcess = require('child_process'); // eslint-disable-next-line import/no-extraneous-dependencies -const LernaProject = require('@lerna/project'); +const { Project } = require('@lerna/project'); // Prepare a release of the provided packages, e.g. @backstage/core async function main(args) { @@ -28,7 +28,7 @@ async function main(args) { process.exit(1); } - const project = new LernaProject(__dirname); + const project = new Project(__dirname); const packages = await project.getPackages(); const ignoreArgs = packages .filter(p => !args.includes(p.name)) diff --git a/yarn.lock b/yarn.lock index 475ac65b6b..fb8fcdcad1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1865,6 +1865,21 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/plugin-catalog-react@^0.0.2": + version "0.0.2" + resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.2.tgz#e50da2dac9fab3a0d5973f8d1083ee2c368e5e52" + integrity sha512-O6aujFPRaEFTk4XlwOoswbnoHIOqMtj6ycUj6R1mNKOM4plUgGDKKhO3be69FHMJEMbiSvVe6AW+1kXaK+1LqA== + dependencies: + "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.0" + "@material-ui/core" "^4.11.0" + "@types/react" "^16.9" + react "^16.13.1" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + "@backstage/plugin-catalog@^0.2.0": version "0.3.1" dependencies: @@ -2299,80 +2314,6 @@ minimatch "^3.0.4" strip-json-comments "^3.1.1" -"@evocateur/libnpmaccess@^3.1.2": - version "3.1.2" - resolved "https://registry.npmjs.org/@evocateur/libnpmaccess/-/libnpmaccess-3.1.2.tgz#ecf7f6ce6b004e9f942b098d92200be4a4b1c845" - integrity sha512-KSCAHwNWro0CF2ukxufCitT9K5LjL/KuMmNzSu8wuwN2rjyKHD8+cmOsiybK+W5hdnwc5M1SmRlVCaMHQo+3rg== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - aproba "^2.0.0" - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - npm-package-arg "^6.1.0" - -"@evocateur/libnpmpublish@^1.2.2": - version "1.2.2" - resolved "https://registry.npmjs.org/@evocateur/libnpmpublish/-/libnpmpublish-1.2.2.tgz#55df09d2dca136afba9c88c759ca272198db9f1a" - integrity sha512-MJrrk9ct1FeY9zRlyeoyMieBjGDG9ihyyD9/Ft6MMrTxql9NyoEx2hw9casTIP4CdqEVu+3nQ2nXxoJ8RCXyFg== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - aproba "^2.0.0" - figgy-pudding "^3.5.1" - get-stream "^4.0.0" - lodash.clonedeep "^4.5.0" - normalize-package-data "^2.4.0" - npm-package-arg "^6.1.0" - semver "^5.5.1" - ssri "^6.0.1" - -"@evocateur/npm-registry-fetch@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@evocateur/npm-registry-fetch/-/npm-registry-fetch-4.0.0.tgz#8c4c38766d8d32d3200fcb0a83f064b57365ed66" - integrity sha512-k1WGfKRQyhJpIr+P17O5vLIo2ko1PFLKwoetatdduUSt/aQ4J2sJrJwwatdI5Z3SiYk/mRH9S3JpdmMFd/IK4g== - dependencies: - JSONStream "^1.3.4" - bluebird "^3.5.1" - figgy-pudding "^3.4.1" - lru-cache "^5.1.1" - make-fetch-happen "^5.0.0" - npm-package-arg "^6.1.0" - safe-buffer "^5.1.2" - -"@evocateur/pacote@^9.6.3": - version "9.6.5" - resolved "https://registry.npmjs.org/@evocateur/pacote/-/pacote-9.6.5.tgz#33de32ba210b6f17c20ebab4d497efc6755f4ae5" - integrity sha512-EI552lf0aG2nOV8NnZpTxNo2PcXKPmDbF9K8eCBFQdIZwHNGN/mi815fxtmUMa2wTa1yndotICIDt/V0vpEx2w== - dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - bluebird "^3.5.3" - cacache "^12.0.3" - chownr "^1.1.2" - figgy-pudding "^3.5.1" - get-stream "^4.1.0" - glob "^7.1.4" - infer-owner "^1.0.4" - lru-cache "^5.1.1" - make-fetch-happen "^5.0.0" - minimatch "^3.0.4" - minipass "^2.3.5" - mississippi "^3.0.0" - mkdirp "^0.5.1" - normalize-package-data "^2.5.0" - npm-package-arg "^6.1.0" - npm-packlist "^1.4.4" - npm-pick-manifest "^3.0.0" - osenv "^0.1.5" - promise-inflight "^1.0.1" - promise-retry "^1.1.1" - protoduck "^5.0.1" - rimraf "^2.6.3" - safe-buffer "^5.2.0" - semver "^5.7.0" - ssri "^6.0.1" - tar "^4.4.10" - unique-filename "^1.1.1" - which "^1.3.1" - "@fmvilas/pseudo-yaml-ast@^0.3.1": version "0.3.1" resolved "https://registry.npmjs.org/@fmvilas/pseudo-yaml-ast/-/pseudo-yaml-ast-0.3.1.tgz#66c5df2c2d76ba8571dc5bd98fda4d7dce6121de" @@ -3225,690 +3166,676 @@ dependencies: stream "^0.0.2" -"@lerna/add@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/add/-/add-3.21.0.tgz#27007bde71cc7b0a2969ab3c2f0ae41578b4577b" - integrity sha512-vhUXXF6SpufBE1EkNEXwz1VLW03f177G9uMOFMQkp6OJ30/PWg4Ekifuz9/3YfgB2/GH8Tu4Lk3O51P2Hskg/A== +"@lerna/add@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/add/-/add-4.0.0.tgz#c36f57d132502a57b9e7058d1548b7a565ef183f" + integrity sha512-cpmAH1iS3k8JBxNvnMqrGTTjbY/ZAiKa1ChJzFevMYY3eeqbvhsBKnBcxjRXtdrJ6bd3dCQM+ZtK+0i682Fhng== dependencies: - "@evocateur/pacote" "^9.6.3" - "@lerna/bootstrap" "3.21.0" - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/validation-error" "3.13.0" + "@lerna/bootstrap" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" dedent "^0.7.0" - npm-package-arg "^6.1.0" - p-map "^2.1.0" - semver "^6.2.0" + npm-package-arg "^8.1.0" + p-map "^4.0.0" + pacote "^11.2.6" + semver "^7.3.4" -"@lerna/bootstrap@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-3.21.0.tgz#bcd1b651be5b0970b20d8fae04c864548123aed6" - integrity sha512-mtNHlXpmvJn6JTu0KcuTTPl2jLsDNud0QacV/h++qsaKbhAaJr/FElNZ5s7MwZFUM3XaDmvWzHKaszeBMHIbBw== +"@lerna/bootstrap@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/bootstrap/-/bootstrap-4.0.0.tgz#5f5c5e2c6cfc8fcec50cb2fbe569a8c607101891" + integrity sha512-RkS7UbeM2vu+kJnHzxNRCLvoOP9yGNgkzRdy4UV2hNalD7EP41bLvRVOwRYQ7fhc2QcbhnKNdOBihYRL0LcKtw== dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/has-npm-version" "3.16.5" - "@lerna/npm-install" "3.16.5" - "@lerna/package-graph" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/rimraf-dir" "3.16.5" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/symlink-binary" "3.17.0" - "@lerna/symlink-dependencies" "3.17.0" - "@lerna/validation-error" "3.13.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/has-npm-version" "4.0.0" + "@lerna/npm-install" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + "@lerna/validation-error" "4.0.0" dedent "^0.7.0" - get-port "^4.2.0" - multimatch "^3.0.0" - npm-package-arg "^6.1.0" + get-port "^5.1.1" + multimatch "^5.0.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - p-finally "^1.0.0" - p-map "^2.1.0" - p-map-series "^1.0.0" - p-waterfall "^1.0.0" - read-package-tree "^5.1.6" - semver "^6.2.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" + read-package-tree "^5.3.1" + semver "^7.3.4" -"@lerna/changed@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/changed/-/changed-3.21.0.tgz#108e15f679bfe077af500f58248c634f1044ea0b" - integrity sha512-hzqoyf8MSHVjZp0gfJ7G8jaz+++mgXYiNs9iViQGA8JlN/dnWLI5sWDptEH3/B30Izo+fdVz0S0s7ydVE3pWIw== +"@lerna/changed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/changed/-/changed-4.0.0.tgz#b9fc76cea39b9292a6cd263f03eb57af85c9270b" + integrity sha512-cD+KuPRp6qiPOD+BO6S6SN5cARspIaWSOqGBpGnYzLb4uWT8Vk4JzKyYtc8ym1DIwyoFXHosXt8+GDAgR8QrgQ== dependencies: - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/listable" "3.18.5" - "@lerna/output" "3.13.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" -"@lerna/check-working-tree@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-3.16.5.tgz#b4f8ae61bb4523561dfb9f8f8d874dd46bb44baa" - integrity sha512-xWjVBcuhvB8+UmCSb5tKVLB5OuzSpw96WEhS2uz6hkWVa/Euh1A0/HJwn2cemyK47wUrCQXtczBUiqnq9yX5VQ== +"@lerna/check-working-tree@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/check-working-tree/-/check-working-tree-4.0.0.tgz#257e36a602c00142e76082a19358e3e1ae8dbd58" + integrity sha512-/++bxM43jYJCshBiKP5cRlCTwSJdRSxVmcDAXM+1oUewlZJVSVlnks5eO0uLxokVFvLhHlC5kHMc7gbVFPHv6Q== dependencies: - "@lerna/collect-uncommitted" "3.16.5" - "@lerna/describe-ref" "3.16.5" - "@lerna/validation-error" "3.13.0" + "@lerna/collect-uncommitted" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/validation-error" "4.0.0" -"@lerna/child-process@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-3.16.5.tgz#38fa3c18064aa4ac0754ad80114776a7b36a69b2" - integrity sha512-vdcI7mzei9ERRV4oO8Y1LHBZ3A5+ampRKg1wq5nutLsUA4mEBN6H7JqjWOMY9xZemv6+kATm2ofjJ3lW5TszQg== +"@lerna/child-process@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/child-process/-/child-process-4.0.0.tgz#341b96a57dffbd9705646d316e231df6fa4df6e1" + integrity sha512-XtCnmCT9eyVsUUHx6y/CTBYdV9g2Cr/VxyseTWBgfIur92/YKClfEtJTbOh94jRT62hlKLqSvux/UhxXVh613Q== dependencies: - chalk "^2.3.1" - execa "^1.0.0" - strong-log-transformer "^2.0.0" + chalk "^4.1.0" + execa "^5.0.0" + strong-log-transformer "^2.1.0" -"@lerna/clean@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/clean/-/clean-3.21.0.tgz#c0b46b5300cc3dae2cda3bec14b803082da3856d" - integrity sha512-b/L9l+MDgE/7oGbrav6rG8RTQvRiZLO1zTcG17zgJAAuhlsPxJExMlh2DFwJEVi2les70vMhHfST3Ue1IMMjpg== +"@lerna/clean@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/clean/-/clean-4.0.0.tgz#8f778b6f2617aa2a936a6b5e085ae62498e57dc5" + integrity sha512-uugG2iN9k45ITx2jtd8nEOoAtca8hNlDCUM0N3lFgU/b1mEQYAPRkqr1qs4FLRl/Y50ZJ41wUz1eazS+d/0osA== dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/rimraf-dir" "3.16.5" - p-map "^2.1.0" - p-map-series "^1.0.0" - p-waterfall "^1.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/rimraf-dir" "4.0.0" + p-map "^4.0.0" + p-map-series "^2.1.0" + p-waterfall "^2.1.1" -"@lerna/cli@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/cli/-/cli-3.18.5.tgz#c90c461542fcd35b6d5b015a290fb0dbfb41d242" - integrity sha512-erkbxkj9jfc89vVs/jBLY/fM0I80oLmJkFUV3Q3wk9J3miYhP14zgVEBsPZY68IZlEjT6T3Xlq2xO1AVaatHsA== +"@lerna/cli@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/cli/-/cli-4.0.0.tgz#8eabd334558836c1664df23f19acb95e98b5bbf3" + integrity sha512-Neaw3GzFrwZiRZv2g7g6NwFjs3er1vhraIniEs0jjVLPMNC4eata0na3GfE5yibkM/9d3gZdmihhZdZ3EBdvYA== dependencies: - "@lerna/global-options" "3.13.0" + "@lerna/global-options" "4.0.0" dedent "^0.7.0" npmlog "^4.1.2" - yargs "^14.2.2" + yargs "^16.2.0" -"@lerna/collect-uncommitted@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-3.16.5.tgz#a494d61aac31cdc7aec4bbe52c96550274132e63" - integrity sha512-ZgqnGwpDZiWyzIQVZtQaj9tRizsL4dUOhuOStWgTAw1EMe47cvAY2kL709DzxFhjr6JpJSjXV5rZEAeU3VE0Hg== +"@lerna/collect-uncommitted@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/collect-uncommitted/-/collect-uncommitted-4.0.0.tgz#855cd64612969371cfc2453b90593053ff1ba779" + integrity sha512-ufSTfHZzbx69YNj7KXQ3o66V4RC76ffOjwLX0q/ab//61bObJ41n03SiQEhSlmpP+gmFbTJ3/7pTe04AHX9m/g== dependencies: - "@lerna/child-process" "3.16.5" - chalk "^2.3.1" - figgy-pudding "^3.5.1" + "@lerna/child-process" "4.0.0" + chalk "^4.1.0" npmlog "^4.1.2" -"@lerna/collect-updates@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-3.20.0.tgz#62f9d76ba21a25b7d9fbf31c02de88744a564bd1" - integrity sha512-qBTVT5g4fupVhBFuY4nI/3FSJtQVcDh7/gEPOpRxoXB/yCSnT38MFHXWl+y4einLciCjt/+0x6/4AG80fjay2Q== +"@lerna/collect-updates@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/collect-updates/-/collect-updates-4.0.0.tgz#8e208b1bafd98a372ff1177f7a5e288f6bea8041" + integrity sha512-bnNGpaj4zuxsEkyaCZLka9s7nMs58uZoxrRIPJ+nrmrZYp1V5rrd+7/NYTuunOhY2ug1sTBvTAxj3NZQ+JKnOw== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/describe-ref" "3.16.5" + "@lerna/child-process" "4.0.0" + "@lerna/describe-ref" "4.0.0" minimatch "^3.0.4" npmlog "^4.1.2" - slash "^2.0.0" + slash "^3.0.0" -"@lerna/command@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/command/-/command-3.21.0.tgz#9a2383759dc7b700dacfa8a22b2f3a6e190121f7" - integrity sha512-T2bu6R8R3KkH5YoCKdutKv123iUgUbW8efVjdGCDnCMthAQzoentOJfDeodBwn0P2OqCl3ohsiNVtSn9h78fyQ== +"@lerna/command@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/command/-/command-4.0.0.tgz#991c7971df8f5bf6ae6e42c808869a55361c1b98" + integrity sha512-LM9g3rt5FsPNFqIHUeRwWXLNHJ5NKzOwmVKZ8anSp4e1SPrv2HNc1V02/9QyDDZK/w+5POXH5lxZUI1CHaOK/A== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/package-graph" "3.18.5" - "@lerna/project" "3.21.0" - "@lerna/validation-error" "3.13.0" - "@lerna/write-log-file" "3.13.0" + "@lerna/child-process" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/project" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/write-log-file" "4.0.0" clone-deep "^4.0.1" dedent "^0.7.0" - execa "^1.0.0" + execa "^5.0.0" is-ci "^2.0.0" npmlog "^4.1.2" -"@lerna/conventional-commits@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-3.22.0.tgz#2798f4881ee2ef457bdae027ab7d0bf0af6f1e09" - integrity sha512-z4ZZk1e8Mhz7+IS8NxHr64wyklHctCJyWpJKEZZPJiLFJ8yKto/x38O80R10pIzC0rr8Sy/OsjSH4bl0TbbgqA== +"@lerna/conventional-commits@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/conventional-commits/-/conventional-commits-4.0.0.tgz#660fb2c7b718cb942ead70110df61f18c6f99750" + integrity sha512-CSUQRjJHFrH8eBn7+wegZLV3OrNc0Y1FehYfYGhjLE2SIfpCL4bmfu/ViYuHh9YjwHaA+4SX6d3hR+xkeseKmw== dependencies: - "@lerna/validation-error" "3.13.0" - conventional-changelog-angular "^5.0.3" - conventional-changelog-core "^3.1.6" - conventional-recommended-bump "^5.0.0" - fs-extra "^8.1.0" - get-stream "^4.0.0" + "@lerna/validation-error" "4.0.0" + conventional-changelog-angular "^5.0.12" + conventional-changelog-core "^4.2.2" + conventional-recommended-bump "^6.1.0" + fs-extra "^9.1.0" + get-stream "^6.0.0" lodash.template "^4.5.0" - npm-package-arg "^6.1.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - pify "^4.0.1" - semver "^6.2.0" + pify "^5.0.0" + semver "^7.3.4" -"@lerna/create-symlink@3.16.2": - version "3.16.2" - resolved "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-3.16.2.tgz#412cb8e59a72f5a7d9463e4e4721ad2070149967" - integrity sha512-pzXIJp6av15P325sgiIRpsPXLFmkisLhMBCy4764d+7yjf2bzrJ4gkWVMhsv4AdF0NN3OyZ5jjzzTtLNqfR+Jw== +"@lerna/create-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/create-symlink/-/create-symlink-4.0.0.tgz#8c5317ce5ae89f67825443bd7651bf4121786228" + integrity sha512-I0phtKJJdafUiDwm7BBlEUOtogmu8+taxq6PtIrxZbllV9hWg59qkpuIsiFp+no7nfRVuaasNYHwNUhDAVQBig== dependencies: - "@zkochan/cmd-shim" "^3.1.0" - fs-extra "^8.1.0" + cmd-shim "^4.1.0" + fs-extra "^9.1.0" npmlog "^4.1.2" -"@lerna/create@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/create/-/create-3.22.0.tgz#d6bbd037c3dc5b425fe5f6d1b817057c278f7619" - integrity sha512-MdiQQzCcB4E9fBF1TyMOaAEz9lUjIHp1Ju9H7f3lXze5JK6Fl5NYkouAvsLgY6YSIhXMY8AHW2zzXeBDY4yWkw== +"@lerna/create@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/create/-/create-4.0.0.tgz#b6947e9b5dfb6530321952998948c3e63d64d730" + integrity sha512-mVOB1niKByEUfxlbKTM1UNECWAjwUdiioIbRQZEeEabtjCL69r9rscIsjlGyhGWCfsdAG5wfq4t47nlDXdLLag== dependencies: - "@evocateur/pacote" "^9.6.3" - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/validation-error" "3.13.0" - camelcase "^5.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/validation-error" "4.0.0" dedent "^0.7.0" - fs-extra "^8.1.0" - globby "^9.2.0" - init-package-json "^1.10.3" - npm-package-arg "^6.1.0" - p-reduce "^1.0.0" - pify "^4.0.1" - semver "^6.2.0" - slash "^2.0.0" - validate-npm-package-license "^3.0.3" + fs-extra "^9.1.0" + globby "^11.0.2" + init-package-json "^2.0.2" + npm-package-arg "^8.1.0" + p-reduce "^2.1.0" + pacote "^11.2.6" + pify "^5.0.0" + semver "^7.3.4" + slash "^3.0.0" + validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" - whatwg-url "^7.0.0" + whatwg-url "^8.4.0" + yargs-parser "20.2.4" -"@lerna/describe-ref@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-3.16.5.tgz#a338c25aaed837d3dc70b8a72c447c5c66346ac0" - integrity sha512-c01+4gUF0saOOtDBzbLMFOTJDHTKbDFNErEY6q6i9QaXuzy9LNN62z+Hw4acAAZuJQhrVWncVathcmkkjvSVGw== +"@lerna/describe-ref@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/describe-ref/-/describe-ref-4.0.0.tgz#53c53b4ea65fdceffa072a62bfebe6772c45d9ec" + integrity sha512-eTU5+xC4C5Gcgz+Ey4Qiw9nV2B4JJbMulsYJMW8QjGcGh8zudib7Sduj6urgZXUYNyhYpRs+teci9M2J8u+UvQ== dependencies: - "@lerna/child-process" "3.16.5" + "@lerna/child-process" "4.0.0" npmlog "^4.1.2" -"@lerna/diff@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/diff/-/diff-3.21.0.tgz#e6df0d8b9916167ff5a49fcb02ac06424280a68d" - integrity sha512-5viTR33QV3S7O+bjruo1SaR40m7F2aUHJaDAC7fL9Ca6xji+aw1KFkpCtVlISS0G8vikUREGMJh+c/VMSc8Usw== +"@lerna/diff@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/diff/-/diff-4.0.0.tgz#6d3071817aaa4205a07bf77cfc6e932796d48b92" + integrity sha512-jYPKprQVg41+MUMxx6cwtqsNm0Yxx9GDEwdiPLwcUTFx+/qKCEwifKNJ1oGIPBxyEHX2PFCOjkK39lHoj2qiag== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/validation-error" "3.13.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/validation-error" "4.0.0" npmlog "^4.1.2" -"@lerna/exec@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/exec/-/exec-3.21.0.tgz#17f07533893cb918a17b41bcc566dc437016db26" - integrity sha512-iLvDBrIE6rpdd4GIKTY9mkXyhwsJ2RvQdB9ZU+/NhR3okXfqKc6py/24tV111jqpXTtZUW6HNydT4dMao2hi1Q== +"@lerna/exec@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/exec/-/exec-4.0.0.tgz#eb6cb95cb92d42590e9e2d628fcaf4719d4a8be6" + integrity sha512-VGXtL/b/JfY84NB98VWZpIExfhLOzy0ozm/0XaS4a2SmkAJc5CeUfrhvHxxkxiTBLkU+iVQUyYEoAT0ulQ8PCw== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/profiler" "3.20.0" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - p-map "^2.1.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" -"@lerna/filter-options@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-3.20.0.tgz#0f0f5d5a4783856eece4204708cc902cbc8af59b" - integrity sha512-bmcHtvxn7SIl/R9gpiNMVG7yjx7WyT0HSGw34YVZ9B+3xF/83N3r5Rgtjh4hheLZ+Q91Or0Jyu5O3Nr+AwZe2g== +"@lerna/filter-options@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/filter-options/-/filter-options-4.0.0.tgz#ac94cc515d7fa3b47e2f7d74deddeabb1de5e9e6" + integrity sha512-vV2ANOeZhOqM0rzXnYcFFCJ/kBWy/3OA58irXih9AMTAlQLymWAK0akWybl++sUJ4HB9Hx12TOqaXbYS2NM5uw== dependencies: - "@lerna/collect-updates" "3.20.0" - "@lerna/filter-packages" "3.18.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/filter-packages" "4.0.0" dedent "^0.7.0" - figgy-pudding "^3.5.1" npmlog "^4.1.2" -"@lerna/filter-packages@3.18.0": - version "3.18.0" - resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-3.18.0.tgz#6a7a376d285208db03a82958cfb8172e179b4e70" - integrity sha512-6/0pMM04bCHNATIOkouuYmPg6KH3VkPCIgTfQmdkPJTullERyEQfNUKikrefjxo1vHOoCACDpy65JYyKiAbdwQ== +"@lerna/filter-packages@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/filter-packages/-/filter-packages-4.0.0.tgz#b1f70d70e1de9cdd36a4e50caa0ac501f8d012f2" + integrity sha512-+4AJIkK7iIiOaqCiVTYJxh/I9qikk4XjNQLhE3kixaqgMuHl1NQ99qXRR0OZqAWB9mh8Z1HA9bM5K1HZLBTOqA== dependencies: - "@lerna/validation-error" "3.13.0" - multimatch "^3.0.0" + "@lerna/validation-error" "4.0.0" + multimatch "^5.0.0" npmlog "^4.1.2" -"@lerna/get-npm-exec-opts@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-3.13.0.tgz#d1b552cb0088199fc3e7e126f914e39a08df9ea5" - integrity sha512-Y0xWL0rg3boVyJk6An/vurKzubyJKtrxYv2sj4bB8Mc5zZ3tqtv0ccbOkmkXKqbzvNNF7VeUt1OJ3DRgtC/QZw== +"@lerna/get-npm-exec-opts@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/get-npm-exec-opts/-/get-npm-exec-opts-4.0.0.tgz#dc955be94a4ae75c374ef9bce91320887d34608f" + integrity sha512-yvmkerU31CTWS2c7DvmAWmZVeclPBqI7gPVr5VATUKNWJ/zmVcU4PqbYoLu92I9Qc4gY1TuUplMNdNuZTSL7IQ== dependencies: npmlog "^4.1.2" -"@lerna/get-packed@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-3.16.0.tgz#1b316b706dcee86c7baa55e50b087959447852ff" - integrity sha512-AjsFiaJzo1GCPnJUJZiTW6J1EihrPkc2y3nMu6m3uWFxoleklsSCyImumzVZJssxMi3CPpztj8LmADLedl9kXw== +"@lerna/get-packed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/get-packed/-/get-packed-4.0.0.tgz#0989d61624ac1f97e393bdad2137c49cd7a37823" + integrity sha512-rfWONRsEIGyPJTxFzC8ECb3ZbsDXJbfqWYyeeQQDrJRPnEJErlltRLPLgC2QWbxFgFPsoDLeQmFHJnf0iDfd8w== dependencies: - fs-extra "^8.1.0" - ssri "^6.0.1" - tar "^4.4.8" + fs-extra "^9.1.0" + ssri "^8.0.1" + tar "^6.1.0" -"@lerna/github-client@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-3.22.0.tgz#5d816aa4f76747ed736ae64ff962b8f15c354d95" - integrity sha512-O/GwPW+Gzr3Eb5bk+nTzTJ3uv+jh5jGho9BOqKlajXaOkMYGBELEAqV5+uARNGWZFvYAiF4PgqHb6aCUu7XdXg== +"@lerna/github-client@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/github-client/-/github-client-4.0.0.tgz#2ced67721363ef70f8e12ffafce4410918f4a8a4" + integrity sha512-2jhsldZtTKXYUBnOm23Lb0Fx8G4qfSXF9y7UpyUgWUj+YZYd+cFxSuorwQIgk5P4XXrtVhsUesIsli+BYSThiw== dependencies: - "@lerna/child-process" "3.16.5" + "@lerna/child-process" "4.0.0" "@octokit/plugin-enterprise-rest" "^6.0.1" - "@octokit/rest" "^16.28.4" - git-url-parse "^11.1.2" + "@octokit/rest" "^18.1.0" + git-url-parse "^11.4.4" npmlog "^4.1.2" -"@lerna/gitlab-client@3.15.0": - version "3.15.0" - resolved "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-3.15.0.tgz#91f4ec8c697b5ac57f7f25bd50fe659d24aa96a6" - integrity sha512-OsBvRSejHXUBMgwWQqNoioB8sgzL/Pf1pOUhHKtkiMl6aAWjklaaq5HPMvTIsZPfS6DJ9L5OK2GGZuooP/5c8Q== +"@lerna/gitlab-client@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/gitlab-client/-/gitlab-client-4.0.0.tgz#00dad73379c7b38951d4b4ded043504c14e2b67d" + integrity sha512-OMUpGSkeDWFf7BxGHlkbb35T7YHqVFCwBPSIR6wRsszY8PAzCYahtH3IaJzEJyUg6vmZsNl0FSr3pdA2skhxqA== dependencies: - node-fetch "^2.5.0" + node-fetch "^2.6.1" npmlog "^4.1.2" - whatwg-url "^7.0.0" + whatwg-url "^8.4.0" -"@lerna/global-options@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-3.13.0.tgz#217662290db06ad9cf2c49d8e3100ee28eaebae1" - integrity sha512-SlZvh1gVRRzYLVluz9fryY1nJpZ0FHDGB66U9tFfvnnxmueckRQxLopn3tXj3NU1kc3QANT2I5BsQkOqZ4TEFQ== +"@lerna/global-options@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/global-options/-/global-options-4.0.0.tgz#c7d8b0de6a01d8a845e2621ea89e7f60f18c6a5f" + integrity sha512-TRMR8afAHxuYBHK7F++Ogop2a82xQjoGna1dvPOY6ltj/pEx59pdgcJfYcynYqMkFIk8bhLJJN9/ndIfX29FTQ== -"@lerna/has-npm-version@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-3.16.5.tgz#ab83956f211d8923ea6afe9b979b38cc73b15326" - integrity sha512-WL7LycR9bkftyqbYop5rEGJ9sRFIV55tSGmbN1HLrF9idwOCD7CLrT64t235t3t4O5gehDnwKI5h2U3oxTrF8Q== +"@lerna/has-npm-version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/has-npm-version/-/has-npm-version-4.0.0.tgz#d3fc3292c545eb28bd493b36e6237cf0279f631c" + integrity sha512-LQ3U6XFH8ZmLCsvsgq1zNDqka0Xzjq5ibVN+igAI5ccRWNaUsE/OcmsyMr50xAtNQMYMzmpw5GVLAivT2/YzCg== dependencies: - "@lerna/child-process" "3.16.5" - semver "^6.2.0" + "@lerna/child-process" "4.0.0" + semver "^7.3.4" -"@lerna/import@3.22.0": - version "3.22.0" - resolved "https://registry.npmjs.org/@lerna/import/-/import-3.22.0.tgz#1a5f0394f38e23c4f642a123e5e1517e70d068d2" - integrity sha512-uWOlexasM5XR6tXi4YehODtH9Y3OZrFht3mGUFFT3OIl2s+V85xIGFfqFGMTipMPAGb2oF1UBLL48kR43hRsOg== +"@lerna/import@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/import/-/import-4.0.0.tgz#bde656c4a451fa87ae41733ff8a8da60547c5465" + integrity sha512-FaIhd+4aiBousKNqC7TX1Uhe97eNKf5/SC7c5WZANVWtC7aBWdmswwDt3usrzCNpj6/Wwr9EtEbYROzxKH8ffg== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/validation-error" "3.13.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/validation-error" "4.0.0" dedent "^0.7.0" - fs-extra "^8.1.0" - p-map-series "^1.0.0" + fs-extra "^9.1.0" + p-map-series "^2.1.0" -"@lerna/info@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/info/-/info-3.21.0.tgz#76696b676fdb0f35d48c83c63c1e32bb5e37814f" - integrity sha512-0XDqGYVBgWxUquFaIptW2bYSIu6jOs1BtkvRTWDDhw4zyEdp6q4eaMvqdSap1CG+7wM5jeLCi6z94wS0AuiuwA== +"@lerna/info@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/info/-/info-4.0.0.tgz#b9fb0e479d60efe1623603958a831a88b1d7f1fc" + integrity sha512-8Uboa12kaCSZEn4XRfPz5KU9XXoexSPS4oeYGj76s2UQb1O1GdnEyfjyNWoUl1KlJ2i/8nxUskpXIftoFYH0/Q== dependencies: - "@lerna/command" "3.21.0" - "@lerna/output" "3.13.0" - envinfo "^7.3.1" + "@lerna/command" "4.0.0" + "@lerna/output" "4.0.0" + envinfo "^7.7.4" -"@lerna/init@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/init/-/init-3.21.0.tgz#1e810934dc8bf4e5386c031041881d3b4096aa5c" - integrity sha512-6CM0z+EFUkFfurwdJCR+LQQF6MqHbYDCBPyhu/d086LRf58GtYZYj49J8mKG9ktayp/TOIxL/pKKjgLD8QBPOg== +"@lerna/init@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/init/-/init-4.0.0.tgz#dadff67e6dfb981e8ccbe0e6a310e837962f6c7a" + integrity sha512-wY6kygop0BCXupzWj5eLvTUqdR7vIAm0OgyV9WHpMYQGfs1V22jhztt8mtjCloD/O0nEe4tJhdG62XU5aYmPNQ== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/command" "3.21.0" - fs-extra "^8.1.0" - p-map "^2.1.0" - write-json-file "^3.2.0" + "@lerna/child-process" "4.0.0" + "@lerna/command" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + write-json-file "^4.3.0" -"@lerna/link@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/link/-/link-3.21.0.tgz#8be68ff0ccee104b174b5bbd606302c2f06e9d9b" - integrity sha512-tGu9GxrX7Ivs+Wl3w1+jrLi1nQ36kNI32dcOssij6bg0oZ2M2MDEFI9UF2gmoypTaN9uO5TSsjCFS7aR79HbdQ== +"@lerna/link@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/link/-/link-4.0.0.tgz#c3a38aabd44279d714e90f2451e31b63f0fb65ba" + integrity sha512-KlvPi7XTAcVOByfaLlOeYOfkkDcd+bejpHMCd1KcArcFTwijOwXOVi24DYomIeHvy6HsX/IUquJ4PPUJIeB4+w== dependencies: - "@lerna/command" "3.21.0" - "@lerna/package-graph" "3.18.5" - "@lerna/symlink-dependencies" "3.17.0" - p-map "^2.1.0" - slash "^2.0.0" + "@lerna/command" "4.0.0" + "@lerna/package-graph" "4.0.0" + "@lerna/symlink-dependencies" "4.0.0" + p-map "^4.0.0" + slash "^3.0.0" -"@lerna/list@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/list/-/list-3.21.0.tgz#42f76fafa56dea13b691ec8cab13832691d61da2" - integrity sha512-KehRjE83B1VaAbRRkRy6jLX1Cin8ltsrQ7FHf2bhwhRHK0S54YuA6LOoBnY/NtA8bHDX/Z+G5sMY78X30NS9tg== +"@lerna/list@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/list/-/list-4.0.0.tgz#24b4e6995bd73f81c556793fe502b847efd9d1d7" + integrity sha512-L2B5m3P+U4Bif5PultR4TI+KtW+SArwq1i75QZ78mRYxPc0U/piau1DbLOmwrdqr99wzM49t0Dlvl6twd7GHFg== dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/listable" "3.18.5" - "@lerna/output" "3.13.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/listable" "4.0.0" + "@lerna/output" "4.0.0" -"@lerna/listable@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/listable/-/listable-3.18.5.tgz#e82798405b5ed8fc51843c8ef1e7a0e497388a1a" - integrity sha512-Sdr3pVyaEv5A7ZkGGYR7zN+tTl2iDcinryBPvtuv20VJrXBE8wYcOks1edBTcOWsPjCE/rMP4bo1pseyk3UTsg== +"@lerna/listable@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/listable/-/listable-4.0.0.tgz#d00d6cb4809b403f2b0374fc521a78e318b01214" + integrity sha512-/rPOSDKsOHs5/PBLINZOkRIX1joOXUXEtyUs5DHLM8q6/RP668x/1lFhw6Dx7/U+L0+tbkpGtZ1Yt0LewCLgeQ== dependencies: - "@lerna/query-graph" "3.18.5" - chalk "^2.3.1" + "@lerna/query-graph" "4.0.0" + chalk "^4.1.0" columnify "^1.5.4" -"@lerna/log-packed@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-3.16.0.tgz#f83991041ee77b2495634e14470b42259fd2bc16" - integrity sha512-Fp+McSNBV/P2mnLUYTaSlG8GSmpXM7krKWcllqElGxvAqv6chk2K3c2k80MeVB4WvJ9tRjUUf+i7HUTiQ9/ckQ== +"@lerna/log-packed@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/log-packed/-/log-packed-4.0.0.tgz#95168fe2e26ac6a71e42f4be857519b77e57a09f" + integrity sha512-+dpCiWbdzgMAtpajLToy9PO713IHoE6GV/aizXycAyA07QlqnkpaBNZ8DW84gHdM1j79TWockGJo9PybVhrrZQ== dependencies: - byte-size "^5.0.1" + byte-size "^7.0.0" columnify "^1.5.4" has-unicode "^2.0.1" npmlog "^4.1.2" -"@lerna/npm-conf@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-3.16.0.tgz#1c10a89ae2f6c2ee96962557738685300d376827" - integrity sha512-HbO3DUrTkCAn2iQ9+FF/eisDpWY5POQAOF1m7q//CZjdC2HSW3UYbKEGsSisFxSfaF9Z4jtrV+F/wX6qWs3CuA== +"@lerna/npm-conf@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-conf/-/npm-conf-4.0.0.tgz#b259fd1e1cee2bf5402b236e770140ff9ade7fd2" + integrity sha512-uS7H02yQNq3oejgjxAxqq/jhwGEE0W0ntr8vM3EfpCW1F/wZruwQw+7bleJQ9vUBjmdXST//tk8mXzr5+JXCfw== dependencies: - config-chain "^1.1.11" - pify "^4.0.1" + config-chain "^1.1.12" + pify "^5.0.0" -"@lerna/npm-dist-tag@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-3.18.5.tgz#9ef9abb7c104077b31f6fab22cc73b314d54ac55" - integrity sha512-xw0HDoIG6HreVsJND9/dGls1c+lf6vhu7yJoo56Sz5bvncTloYGLUppIfDHQr4ZvmPCK8rsh0euCVh2giPxzKQ== +"@lerna/npm-dist-tag@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-dist-tag/-/npm-dist-tag-4.0.0.tgz#d1e99b4eccd3414142f0548ad331bf2d53f3257a" + integrity sha512-F20sg28FMYTgXqEQihgoqSfwmq+Id3zT23CnOwD+XQMPSy9IzyLf1fFVH319vXIw6NF6Pgs4JZN2Qty6/CQXGw== dependencies: - "@evocateur/npm-registry-fetch" "^4.0.0" - "@lerna/otplease" "3.18.5" - figgy-pudding "^3.5.1" - npm-package-arg "^6.1.0" + "@lerna/otplease" "4.0.0" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" npmlog "^4.1.2" -"@lerna/npm-install@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-3.16.5.tgz#d6bfdc16f81285da66515ae47924d6e278d637d3" - integrity sha512-hfiKk8Eku6rB9uApqsalHHTHY+mOrrHeWEs+gtg7+meQZMTS3kzv4oVp5cBZigndQr3knTLjwthT/FX4KvseFg== +"@lerna/npm-install@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-install/-/npm-install-4.0.0.tgz#31180be3ab3b7d1818a1a0c206aec156b7094c78" + integrity sha512-aKNxq2j3bCH3eXl3Fmu4D54s/YLL9WSwV8W7X2O25r98wzrO38AUN6AB9EtmAx+LV/SP15et7Yueg9vSaanRWg== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/get-npm-exec-opts" "3.13.0" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" + fs-extra "^9.1.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - signal-exit "^3.0.2" - write-pkg "^3.1.0" + signal-exit "^3.0.3" + write-pkg "^4.0.0" -"@lerna/npm-publish@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-3.18.5.tgz#240e4039959fd9816b49c5b07421e11b5cb000af" - integrity sha512-3etLT9+2L8JAx5F8uf7qp6iAtOLSMj+ZYWY6oUgozPi/uLqU0/gsMsEXh3F0+YVW33q0M61RpduBoAlOOZnaTg== +"@lerna/npm-publish@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-publish/-/npm-publish-4.0.0.tgz#84eb62e876fe949ae1fd62c60804423dbc2c4472" + integrity sha512-vQb7yAPRo5G5r77DRjHITc9piR9gvEKWrmfCH7wkfBnGWEqu7n8/4bFQ7lhnkujvc8RXOsYpvbMQkNfkYibD/w== dependencies: - "@evocateur/libnpmpublish" "^1.2.2" - "@lerna/otplease" "3.18.5" - "@lerna/run-lifecycle" "3.16.2" - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" + "@lerna/otplease" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + fs-extra "^9.1.0" + libnpmpublish "^4.0.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - pify "^4.0.1" - read-package-json "^2.0.13" + pify "^5.0.0" + read-package-json "^3.0.0" -"@lerna/npm-run-script@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-3.16.5.tgz#9c2ec82453a26c0b46edc0bb7c15816c821f5c15" - integrity sha512-1asRi+LjmVn3pMjEdpqKJZFT/3ZNpb+VVeJMwrJaV/3DivdNg7XlPK9LTrORuKU4PSvhdEZvJmSlxCKyDpiXsQ== +"@lerna/npm-run-script@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/npm-run-script/-/npm-run-script-4.0.0.tgz#dfebf4f4601442e7c0b5214f9fb0d96c9350743b" + integrity sha512-Jmyh9/IwXJjOXqKfIgtxi0bxi1pUeKe5bD3S81tkcy+kyng/GNj9WSqD5ZggoNP2NP//s4CLDAtUYLdP7CU9rA== dependencies: - "@lerna/child-process" "3.16.5" - "@lerna/get-npm-exec-opts" "3.13.0" + "@lerna/child-process" "4.0.0" + "@lerna/get-npm-exec-opts" "4.0.0" npmlog "^4.1.2" -"@lerna/otplease@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/otplease/-/otplease-3.18.5.tgz#b77b8e760b40abad9f7658d988f3ea77d4fd0231" - integrity sha512-S+SldXAbcXTEDhzdxYLU0ZBKuYyURP/ND2/dK6IpKgLxQYh/z4ScljPDMyKymmEvgiEJmBsPZAAPfmNPEzxjog== +"@lerna/otplease@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/otplease/-/otplease-4.0.0.tgz#84972eb43448f8a1077435ba1c5e59233b725850" + integrity sha512-Sgzbqdk1GH4psNiT6hk+BhjOfIr/5KhGBk86CEfHNJTk9BK4aZYyJD4lpDbDdMjIV4g03G7pYoqHzH765T4fxw== dependencies: - "@lerna/prompt" "3.18.5" - figgy-pudding "^3.5.1" + "@lerna/prompt" "4.0.0" -"@lerna/output@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/output/-/output-3.13.0.tgz#3ded7cc908b27a9872228a630d950aedae7a4989" - integrity sha512-7ZnQ9nvUDu/WD+bNsypmPG5MwZBwu86iRoiW6C1WBuXXDxM5cnIAC1m2WxHeFnjyMrYlRXM9PzOQ9VDD+C15Rg== +"@lerna/output@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/output/-/output-4.0.0.tgz#b1d72215c0e35483e4f3e9994debc82c621851f2" + integrity sha512-Un1sHtO1AD7buDQrpnaYTi2EG6sLF+KOPEAMxeUYG5qG3khTs2Zgzq5WE3dt2N/bKh7naESt20JjIW6tBELP0w== dependencies: npmlog "^4.1.2" -"@lerna/pack-directory@3.16.4": - version "3.16.4" - resolved "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-3.16.4.tgz#3eae5f91bdf5acfe0384510ed53faddc4c074693" - integrity sha512-uxSF0HZeGyKaaVHz5FroDY9A5NDDiCibrbYR6+khmrhZtY0Bgn6hWq8Gswl9iIlymA+VzCbshWIMX4o2O8C8ng== +"@lerna/pack-directory@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/pack-directory/-/pack-directory-4.0.0.tgz#8b617db95d20792f043aaaa13a9ccc0e04cb4c74" + integrity sha512-NJrmZNmBHS+5aM+T8N6FVbaKFScVqKlQFJNY2k7nsJ/uklNKsLLl6VhTQBPwMTbf6Tf7l6bcKzpy7aePuq9UiQ== dependencies: - "@lerna/get-packed" "3.16.0" - "@lerna/package" "3.16.0" - "@lerna/run-lifecycle" "3.16.2" - figgy-pudding "^3.5.1" - npm-packlist "^1.4.4" + "@lerna/get-packed" "4.0.0" + "@lerna/package" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + npm-packlist "^2.1.4" npmlog "^4.1.2" - tar "^4.4.10" - temp-write "^3.4.0" + tar "^6.1.0" + temp-write "^4.0.0" -"@lerna/package-graph@3.18.5", "@lerna/package-graph@^3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-3.18.5.tgz#c740e2ea3578d059e551633e950690831b941f6b" - integrity sha512-8QDrR9T+dBegjeLr+n9WZTVxUYUhIUjUgZ0gvNxUBN8S1WB9r6H5Yk56/MVaB64tA3oGAN9IIxX6w0WvTfFudA== +"@lerna/package-graph@4.0.0", "@lerna/package-graph@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/package-graph/-/package-graph-4.0.0.tgz#16a00253a8ac810f72041481cb46bcee8d8123dd" + integrity sha512-QED2ZCTkfXMKFoTGoccwUzjHtZMSf3UKX14A4/kYyBms9xfFsesCZ6SLI5YeySEgcul8iuIWfQFZqRw+Qrjraw== dependencies: - "@lerna/prerelease-id-from-version" "3.16.0" - "@lerna/validation-error" "3.13.0" - npm-package-arg "^6.1.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/validation-error" "4.0.0" + npm-package-arg "^8.1.0" npmlog "^4.1.2" - semver "^6.2.0" + semver "^7.3.4" -"@lerna/package@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/package/-/package-3.16.0.tgz#7e0a46e4697ed8b8a9c14d59c7f890e0d38ba13c" - integrity sha512-2lHBWpaxcBoiNVbtyLtPUuTYEaB/Z+eEqRS9duxpZs6D+mTTZMNy6/5vpEVSCBmzvdYpyqhqaYjjSLvjjr5Riw== +"@lerna/package@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/package/-/package-4.0.0.tgz#1b4c259c4bcff45c876ee1d591a043aacbc0d6b7" + integrity sha512-l0M/izok6FlyyitxiQKr+gZLVFnvxRQdNhzmQ6nRnN9dvBJWn+IxxpM+cLqGACatTnyo9LDzNTOj2Db3+s0s8Q== dependencies: - load-json-file "^5.3.0" - npm-package-arg "^6.1.0" - write-pkg "^3.1.0" + load-json-file "^6.2.0" + npm-package-arg "^8.1.0" + write-pkg "^4.0.0" -"@lerna/prerelease-id-from-version@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-3.16.0.tgz#b24bfa789f5e1baab914d7b08baae9b7bd7d83a1" - integrity sha512-qZyeUyrE59uOK8rKdGn7jQz+9uOpAaF/3hbslJVFL1NqF9ELDTqjCPXivuejMX/lN4OgD6BugTO4cR7UTq/sZA== +"@lerna/prerelease-id-from-version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/prerelease-id-from-version/-/prerelease-id-from-version-4.0.0.tgz#c7e0676fcee1950d85630e108eddecdd5b48c916" + integrity sha512-GQqguzETdsYRxOSmdFZ6zDBXDErIETWOqomLERRY54f4p+tk4aJjoVdd9xKwehC9TBfIFvlRbL1V9uQGHh1opg== dependencies: - semver "^6.2.0" + semver "^7.3.4" -"@lerna/profiler@3.20.0": - version "3.20.0" - resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-3.20.0.tgz#0f6dc236f4ea8f9ea5f358c6703305a4f32ad051" - integrity sha512-bh8hKxAlm6yu8WEOvbLENm42i2v9SsR4WbrCWSbsmOElx3foRnMlYk7NkGECa+U5c3K4C6GeBbwgqs54PP7Ljg== +"@lerna/profiler@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/profiler/-/profiler-4.0.0.tgz#8a53ab874522eae15d178402bff90a14071908e9" + integrity sha512-/BaEbqnVh1LgW/+qz8wCuI+obzi5/vRE8nlhjPzdEzdmWmZXuCKyWSEzAyHOJWw1ntwMiww5dZHhFQABuoFz9Q== dependencies: - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" + fs-extra "^9.1.0" npmlog "^4.1.2" - upath "^1.2.0" + upath "^2.0.1" -"@lerna/project@3.21.0", "@lerna/project@^3.18.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/project/-/project-3.21.0.tgz#5d784d2d10c561a00f20320bcdb040997c10502d" - integrity sha512-xT1mrpET2BF11CY32uypV2GPtPVm6Hgtha7D81GQP9iAitk9EccrdNjYGt5UBYASl4CIDXBRxwmTTVGfrCx82A== +"@lerna/project@4.0.0", "@lerna/project@^4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/project/-/project-4.0.0.tgz#ff84893935833533a74deff30c0e64ddb7f0ba6b" + integrity sha512-o0MlVbDkD5qRPkFKlBZsXZjoNTWPyuL58564nSfZJ6JYNmgAptnWPB2dQlAc7HWRZkmnC2fCkEdoU+jioPavbg== dependencies: - "@lerna/package" "3.16.0" - "@lerna/validation-error" "3.13.0" - cosmiconfig "^5.1.0" + "@lerna/package" "4.0.0" + "@lerna/validation-error" "4.0.0" + cosmiconfig "^7.0.0" dedent "^0.7.0" - dot-prop "^4.2.0" - glob-parent "^5.0.0" - globby "^9.2.0" - load-json-file "^5.3.0" + dot-prop "^6.0.1" + glob-parent "^5.1.1" + globby "^11.0.2" + load-json-file "^6.2.0" npmlog "^4.1.2" - p-map "^2.1.0" - resolve-from "^4.0.0" - write-json-file "^3.2.0" + p-map "^4.0.0" + resolve-from "^5.0.0" + write-json-file "^4.3.0" -"@lerna/prompt@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-3.18.5.tgz#628cd545f225887d060491ab95df899cfc5218a1" - integrity sha512-rkKj4nm1twSbBEb69+Em/2jAERK8htUuV8/xSjN0NPC+6UjzAwY52/x9n5cfmpa9lyKf/uItp7chCI7eDmNTKQ== +"@lerna/prompt@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/prompt/-/prompt-4.0.0.tgz#5ec69a803f3f0db0ad9f221dad64664d3daca41b" + integrity sha512-4Ig46oCH1TH5M7YyTt53fT6TuaKMgqUUaqdgxvp6HP6jtdak6+amcsqB8YGz2eQnw/sdxunx84DfI9XpoLj4bQ== dependencies: - inquirer "^6.2.0" + inquirer "^7.3.3" npmlog "^4.1.2" -"@lerna/publish@3.22.1": - version "3.22.1" - resolved "https://registry.npmjs.org/@lerna/publish/-/publish-3.22.1.tgz#b4f7ce3fba1e9afb28be4a1f3d88222269ba9519" - integrity sha512-PG9CM9HUYDreb1FbJwFg90TCBQooGjj+n/pb3gw/eH5mEDq0p8wKdLFe0qkiqUkm/Ub5C8DbVFertIo0Vd0zcw== +"@lerna/publish@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/publish/-/publish-4.0.0.tgz#f67011305adeba120066a3b6d984a5bb5fceef65" + integrity sha512-K8jpqjHrChH22qtkytA5GRKIVFEtqBF6JWj1I8dWZtHs4Jywn8yB1jQ3BAMLhqmDJjWJtRck0KXhQQKzDK2UPg== dependencies: - "@evocateur/libnpmaccess" "^3.1.2" - "@evocateur/npm-registry-fetch" "^4.0.0" - "@evocateur/pacote" "^9.6.3" - "@lerna/check-working-tree" "3.16.5" - "@lerna/child-process" "3.16.5" - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/describe-ref" "3.16.5" - "@lerna/log-packed" "3.16.0" - "@lerna/npm-conf" "3.16.0" - "@lerna/npm-dist-tag" "3.18.5" - "@lerna/npm-publish" "3.18.5" - "@lerna/otplease" "3.18.5" - "@lerna/output" "3.13.0" - "@lerna/pack-directory" "3.16.4" - "@lerna/prerelease-id-from-version" "3.16.0" - "@lerna/prompt" "3.18.5" - "@lerna/pulse-till-done" "3.13.0" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - "@lerna/version" "3.22.1" - figgy-pudding "^3.5.1" - fs-extra "^8.1.0" - npm-package-arg "^6.1.0" + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/describe-ref" "4.0.0" + "@lerna/log-packed" "4.0.0" + "@lerna/npm-conf" "4.0.0" + "@lerna/npm-dist-tag" "4.0.0" + "@lerna/npm-publish" "4.0.0" + "@lerna/otplease" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/pack-directory" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/pulse-till-done" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + "@lerna/version" "4.0.0" + fs-extra "^9.1.0" + libnpmaccess "^4.0.1" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.0.0" npmlog "^4.1.2" - p-finally "^1.0.0" - p-map "^2.1.0" - p-pipe "^1.2.0" - semver "^6.2.0" + p-map "^4.0.0" + p-pipe "^3.1.0" + pacote "^11.2.6" + semver "^7.3.4" -"@lerna/pulse-till-done@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-3.13.0.tgz#c8e9ce5bafaf10d930a67d7ed0ccb5d958fe0110" - integrity sha512-1SOHpy7ZNTPulzIbargrgaJX387csN7cF1cLOGZiJQA6VqnS5eWs2CIrG8i8wmaUavj2QlQ5oEbRMVVXSsGrzA== +"@lerna/pulse-till-done@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/pulse-till-done/-/pulse-till-done-4.0.0.tgz#04bace7d483a8205c187b806bcd8be23d7bb80a3" + integrity sha512-Frb4F7QGckaybRhbF7aosLsJ5e9WuH7h0KUkjlzSByVycxY91UZgaEIVjS2oN9wQLrheLMHl6SiFY0/Pvo0Cxg== dependencies: npmlog "^4.1.2" -"@lerna/query-graph@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-3.18.5.tgz#df4830bb5155273003bf35e8dda1c32d0927bd86" - integrity sha512-50Lf4uuMpMWvJ306be3oQDHrWV42nai9gbIVByPBYJuVW8dT8O8pA3EzitNYBUdLL9/qEVbrR0ry1HD7EXwtRA== +"@lerna/query-graph@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/query-graph/-/query-graph-4.0.0.tgz#09dd1c819ac5ee3f38db23931143701f8a6eef63" + integrity sha512-YlP6yI3tM4WbBmL9GCmNDoeQyzcyg1e4W96y/PKMZa5GbyUvkS2+Jc2kwPD+5KcXou3wQZxSPzR3Te5OenaDdg== dependencies: - "@lerna/package-graph" "3.18.5" - figgy-pudding "^3.5.1" + "@lerna/package-graph" "4.0.0" -"@lerna/resolve-symlink@3.16.0": - version "3.16.0" - resolved "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-3.16.0.tgz#37fc7095fabdbcf317c26eb74e0d0bde8efd2386" - integrity sha512-Ibj5e7njVHNJ/NOqT4HlEgPFPtPLWsO7iu59AM5bJDcAJcR96mLZ7KGVIsS2tvaO7akMEJvt2P+ErwCdloG3jQ== +"@lerna/resolve-symlink@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/resolve-symlink/-/resolve-symlink-4.0.0.tgz#6d006628a210c9b821964657a9e20a8c9a115e14" + integrity sha512-RtX8VEUzqT+uLSCohx8zgmjc6zjyRlh6i/helxtZTMmc4+6O4FS9q5LJas2uGO2wKvBlhcD6siibGt7dIC3xZA== dependencies: - fs-extra "^8.1.0" + fs-extra "^9.1.0" npmlog "^4.1.2" - read-cmd-shim "^1.0.1" + read-cmd-shim "^2.0.0" -"@lerna/rimraf-dir@3.16.5": - version "3.16.5" - resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-3.16.5.tgz#04316ab5ffd2909657aaf388ea502cb8c2f20a09" - integrity sha512-bQlKmO0pXUsXoF8lOLknhyQjOZsCc0bosQDoX4lujBXSWxHVTg1VxURtWf2lUjz/ACsJVDfvHZbDm8kyBk5okA== +"@lerna/rimraf-dir@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/rimraf-dir/-/rimraf-dir-4.0.0.tgz#2edf3b62d4eb0ef4e44e430f5844667d551ec25a" + integrity sha512-QNH9ABWk9mcMJh2/muD9iYWBk1oQd40y6oH+f3wwmVGKYU5YJD//+zMiBI13jxZRtwBx0vmBZzkBkK1dR11cBg== dependencies: - "@lerna/child-process" "3.16.5" + "@lerna/child-process" "4.0.0" npmlog "^4.1.2" - path-exists "^3.0.0" - rimraf "^2.6.2" + path-exists "^4.0.0" + rimraf "^3.0.2" -"@lerna/run-lifecycle@3.16.2": - version "3.16.2" - resolved "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-3.16.2.tgz#67b288f8ea964db9ea4fb1fbc7715d5bbb0bce00" - integrity sha512-RqFoznE8rDpyyF0rOJy3+KjZCeTkO8y/OB9orPauR7G2xQ7PTdCpgo7EO6ZNdz3Al+k1BydClZz/j78gNCmL2A== +"@lerna/run-lifecycle@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run-lifecycle/-/run-lifecycle-4.0.0.tgz#e648a46f9210a9bcd7c391df6844498cb5079334" + integrity sha512-IwxxsajjCQQEJAeAaxF8QdEixfI7eLKNm4GHhXHrgBu185JcwScFZrj9Bs+PFKxwb+gNLR4iI5rpUdY8Y0UdGQ== dependencies: - "@lerna/npm-conf" "3.16.0" - figgy-pudding "^3.5.1" - npm-lifecycle "^3.1.2" + "@lerna/npm-conf" "4.0.0" + npm-lifecycle "^3.1.5" npmlog "^4.1.2" -"@lerna/run-topologically@3.18.5": - version "3.18.5" - resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-3.18.5.tgz#3cd639da20e967d7672cb88db0f756b92f2fdfc3" - integrity sha512-6N1I+6wf4hLOnPW+XDZqwufyIQ6gqoPfHZFkfWlvTQ+Ue7CuF8qIVQ1Eddw5HKQMkxqN10thKOFfq/9NQZ4NUg== +"@lerna/run-topologically@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run-topologically/-/run-topologically-4.0.0.tgz#af846eeee1a09b0c2be0d1bfb5ef0f7b04bb1827" + integrity sha512-EVZw9hGwo+5yp+VL94+NXRYisqgAlj0jWKWtAIynDCpghRxCE5GMO3xrQLmQgqkpUl9ZxQFpICgYv5DW4DksQA== dependencies: - "@lerna/query-graph" "3.18.5" - figgy-pudding "^3.5.1" - p-queue "^4.0.0" + "@lerna/query-graph" "4.0.0" + p-queue "^6.6.2" -"@lerna/run@3.21.0": - version "3.21.0" - resolved "https://registry.npmjs.org/@lerna/run/-/run-3.21.0.tgz#2a35ec84979e4d6e42474fe148d32e5de1cac891" - integrity sha512-fJF68rT3veh+hkToFsBmUJ9MHc9yGXA7LSDvhziAojzOb0AI/jBDp6cEcDQyJ7dbnplba2Lj02IH61QUf9oW0Q== +"@lerna/run@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/run/-/run-4.0.0.tgz#4bc7fda055a729487897c23579694f6183c91262" + integrity sha512-9giulCOzlMPzcZS/6Eov6pxE9gNTyaXk0Man+iCIdGJNMrCnW7Dme0Z229WWP/UoxDKg71F2tMsVVGDiRd8fFQ== dependencies: - "@lerna/command" "3.21.0" - "@lerna/filter-options" "3.20.0" - "@lerna/npm-run-script" "3.16.5" - "@lerna/output" "3.13.0" - "@lerna/profiler" "3.20.0" - "@lerna/run-topologically" "3.18.5" - "@lerna/timer" "3.13.0" - "@lerna/validation-error" "3.13.0" - p-map "^2.1.0" + "@lerna/command" "4.0.0" + "@lerna/filter-options" "4.0.0" + "@lerna/npm-run-script" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/profiler" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/timer" "4.0.0" + "@lerna/validation-error" "4.0.0" + p-map "^4.0.0" -"@lerna/symlink-binary@3.17.0": - version "3.17.0" - resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-3.17.0.tgz#8f8031b309863814883d3f009877f82e38aef45a" - integrity sha512-RLpy9UY6+3nT5J+5jkM5MZyMmjNHxZIZvXLV+Q3MXrf7Eaa1hNqyynyj4RO95fxbS+EZc4XVSk25DGFQbcRNSQ== +"@lerna/symlink-binary@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/symlink-binary/-/symlink-binary-4.0.0.tgz#21009f62d53a425f136cb4c1a32c6b2a0cc02d47" + integrity sha512-zualodWC4q1QQc1pkz969hcFeWXOsVYZC5AWVtAPTDfLl+TwM7eG/O6oP+Rr3fFowspxo6b1TQ6sYfDV6HXNWA== dependencies: - "@lerna/create-symlink" "3.16.2" - "@lerna/package" "3.16.0" - fs-extra "^8.1.0" - p-map "^2.1.0" + "@lerna/create-symlink" "4.0.0" + "@lerna/package" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" -"@lerna/symlink-dependencies@3.17.0": - version "3.17.0" - resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-3.17.0.tgz#48d6360e985865a0e56cd8b51b308a526308784a" - integrity sha512-KmjU5YT1bpt6coOmdFueTJ7DFJL4H1w5eF8yAQ2zsGNTtZ+i5SGFBWpb9AQaw168dydc3s4eu0W0Sirda+F59Q== +"@lerna/symlink-dependencies@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/symlink-dependencies/-/symlink-dependencies-4.0.0.tgz#8910eca084ae062642d0490d8972cf2d98e9ebbd" + integrity sha512-BABo0MjeUHNAe2FNGty1eantWp8u83BHSeIMPDxNq0MuW2K3CiQRaeWT3EGPAzXpGt0+hVzBrA6+OT0GPn7Yuw== dependencies: - "@lerna/create-symlink" "3.16.2" - "@lerna/resolve-symlink" "3.16.0" - "@lerna/symlink-binary" "3.17.0" - fs-extra "^8.1.0" - p-finally "^1.0.0" - p-map "^2.1.0" - p-map-series "^1.0.0" + "@lerna/create-symlink" "4.0.0" + "@lerna/resolve-symlink" "4.0.0" + "@lerna/symlink-binary" "4.0.0" + fs-extra "^9.1.0" + p-map "^4.0.0" + p-map-series "^2.1.0" -"@lerna/timer@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/timer/-/timer-3.13.0.tgz#bcd0904551db16e08364d6c18e5e2160fc870781" - integrity sha512-RHWrDl8U4XNPqY5MQHkToWS9jHPnkLZEt5VD+uunCKTfzlxGnRCr3/zVr8VGy/uENMYpVP3wJa4RKGY6M0vkRw== +"@lerna/timer@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/timer/-/timer-4.0.0.tgz#a52e51bfcd39bfd768988049ace7b15c1fd7a6da" + integrity sha512-WFsnlaE7SdOvjuyd05oKt8Leg3ENHICnvX3uYKKdByA+S3g+TCz38JsNs7OUZVt+ba63nC2nbXDlUnuT2Xbsfg== -"@lerna/validation-error@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-3.13.0.tgz#c86b8f07c5ab9539f775bd8a54976e926f3759c3" - integrity sha512-SiJP75nwB8GhgwLKQfdkSnDufAaCbkZWJqEDlKOUPUvVOplRGnfL+BPQZH5nvq2BYSRXsksXWZ4UHVnQZI/HYA== +"@lerna/validation-error@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/validation-error/-/validation-error-4.0.0.tgz#af9d62fe8304eaa2eb9a6ba1394f9aa807026d35" + integrity sha512-1rBOM5/koiVWlRi3V6dB863E1YzJS8v41UtsHgMr6gB2ncJ2LsQtMKlJpi3voqcgh41H8UsPXR58RrrpPpufyw== dependencies: npmlog "^4.1.2" -"@lerna/version@3.22.1": - version "3.22.1" - resolved "https://registry.npmjs.org/@lerna/version/-/version-3.22.1.tgz#9805a9247a47ee62d6b81bd9fa5fb728b24b59e2" - integrity sha512-PSGt/K1hVqreAFoi3zjD0VEDupQ2WZVlVIwesrE5GbrL2BjXowjCsTDPqblahDUPy0hp6h7E2kG855yLTp62+g== +"@lerna/version@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/version/-/version-4.0.0.tgz#532659ec6154d8a8789c5ab53878663e244e3228" + integrity sha512-otUgiqs5W9zGWJZSCCMRV/2Zm2A9q9JwSDS7s/tlKq4mWCYriWo7+wsHEA/nPTMDyYyBO5oyZDj+3X50KDUzeA== dependencies: - "@lerna/check-working-tree" "3.16.5" - "@lerna/child-process" "3.16.5" - "@lerna/collect-updates" "3.20.0" - "@lerna/command" "3.21.0" - "@lerna/conventional-commits" "3.22.0" - "@lerna/github-client" "3.22.0" - "@lerna/gitlab-client" "3.15.0" - "@lerna/output" "3.13.0" - "@lerna/prerelease-id-from-version" "3.16.0" - "@lerna/prompt" "3.18.5" - "@lerna/run-lifecycle" "3.16.2" - "@lerna/run-topologically" "3.18.5" - "@lerna/validation-error" "3.13.0" - chalk "^2.3.1" + "@lerna/check-working-tree" "4.0.0" + "@lerna/child-process" "4.0.0" + "@lerna/collect-updates" "4.0.0" + "@lerna/command" "4.0.0" + "@lerna/conventional-commits" "4.0.0" + "@lerna/github-client" "4.0.0" + "@lerna/gitlab-client" "4.0.0" + "@lerna/output" "4.0.0" + "@lerna/prerelease-id-from-version" "4.0.0" + "@lerna/prompt" "4.0.0" + "@lerna/run-lifecycle" "4.0.0" + "@lerna/run-topologically" "4.0.0" + "@lerna/validation-error" "4.0.0" + chalk "^4.1.0" dedent "^0.7.0" - load-json-file "^5.3.0" + load-json-file "^6.2.0" minimatch "^3.0.4" npmlog "^4.1.2" - p-map "^2.1.0" - p-pipe "^1.2.0" - p-reduce "^1.0.0" - p-waterfall "^1.0.0" - semver "^6.2.0" - slash "^2.0.0" - temp-write "^3.4.0" - write-json-file "^3.2.0" + p-map "^4.0.0" + p-pipe "^3.1.0" + p-reduce "^2.1.0" + p-waterfall "^2.1.1" + semver "^7.3.4" + slash "^3.0.0" + temp-write "^4.0.0" + write-json-file "^4.3.0" -"@lerna/write-log-file@3.13.0": - version "3.13.0" - resolved "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-3.13.0.tgz#b78d9e4cfc1349a8be64d91324c4c8199e822a26" - integrity sha512-RibeMnDPvlL8bFYW5C8cs4mbI3AHfQef73tnJCQ/SgrXZHehmHnsyWUiE7qDQCAo+B1RfTapvSyFF69iPj326A== +"@lerna/write-log-file@4.0.0": + version "4.0.0" + resolved "https://registry.npmjs.org/@lerna/write-log-file/-/write-log-file-4.0.0.tgz#18221a38a6a307d6b0a5844dd592ad53fa27091e" + integrity sha512-XRG5BloiArpXRakcnPHmEHJp+4AtnhRtpDIHSghmXD5EichI1uD73J7FgPp30mm2pDRq3FdqB0NbwSEsJ9xFQg== dependencies: npmlog "^4.1.2" - write-file-atomic "^2.3.0" + write-file-atomic "^3.0.3" "@manypkg/find-root@^1.1.0": version "1.1.0" @@ -4092,6 +4019,34 @@ "@nodelib/fs.scandir" "2.1.3" fastq "^1.6.0" +"@npmcli/ci-detect@^1.0.0": + version "1.3.0" + resolved "https://registry.npmjs.org/@npmcli/ci-detect/-/ci-detect-1.3.0.tgz#6c1d2c625fb6ef1b9dea85ad0a5afcbef85ef22a" + integrity sha512-oN3y7FAROHhrAt7Rr7PnTSwrHrZVRTS2ZbyxeQwSSYD0ifwM3YNgQqbaRmjcWoPyq77MjchusjJDspbzMmip1Q== + +"@npmcli/git@^2.0.1": + version "2.0.6" + resolved "https://registry.npmjs.org/@npmcli/git/-/git-2.0.6.tgz#47b97e96b2eede3f38379262fa3bdfa6eae57bf2" + integrity sha512-a1MnTfeRPBaKbFY07fd+6HugY1WAkKJzdiJvlRub/9o5xz2F/JtPacZZapx5zRJUQFIzSL677vmTSxEcDMrDbg== + dependencies: + "@npmcli/promise-spawn" "^1.1.0" + lru-cache "^6.0.0" + mkdirp "^1.0.3" + npm-pick-manifest "^6.0.0" + promise-inflight "^1.0.1" + promise-retry "^2.0.1" + semver "^7.3.2" + unique-filename "^1.1.1" + which "^2.0.2" + +"@npmcli/installed-package-contents@^1.0.6": + version "1.0.7" + resolved "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-1.0.7.tgz#ab7408c6147911b970a8abe261ce512232a3f4fa" + integrity sha512-9rufe0wnJusCQoLpV9ZPKIVP55itrM5BxOXs10DmdbRfgWtHy1LDyskbwRnBghuB0PrF7pNPOqREVtpz4HqzKw== + dependencies: + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + "@npmcli/move-file@^1.0.1": version "1.0.1" resolved "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.0.1.tgz#de103070dac0f48ce49cf6693c23af59c0f70464" @@ -4099,6 +4054,30 @@ dependencies: mkdirp "^1.0.4" +"@npmcli/node-gyp@^1.0.2": + version "1.0.2" + resolved "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-1.0.2.tgz#3cdc1f30e9736dbc417373ed803b42b1a0a29ede" + integrity sha512-yrJUe6reVMpktcvagumoqD9r08fH1iRo01gn1u0zoCApa9lnZGEigVKUd2hzsCId4gdtkZZIVscLhNxMECKgRg== + +"@npmcli/promise-spawn@^1.1.0", "@npmcli/promise-spawn@^1.2.0", "@npmcli/promise-spawn@^1.3.2": + version "1.3.2" + resolved "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-1.3.2.tgz#42d4e56a8e9274fba180dabc0aea6e38f29274f5" + integrity sha512-QyAGYo/Fbj4MXeGdJcFzZ+FkDkomfRBrPM+9QYJSg+PxgAUL+LU3FneQk37rKR2/zjqkCV1BLHccX98wRXG3Sg== + dependencies: + infer-owner "^1.0.4" + +"@npmcli/run-script@^1.8.2": + version "1.8.3" + resolved "https://registry.npmjs.org/@npmcli/run-script/-/run-script-1.8.3.tgz#07f440ed492400bb1114369bc37315eeaaae2bb3" + integrity sha512-ELPGWAVU/xyU+A+H3pEPj0QOvYwLTX71RArXcClFzeiyJ/b/McsZ+d0QxpznvfFtZzxGN/gz/1cvlqICR4/suQ== + dependencies: + "@npmcli/node-gyp" "^1.0.2" + "@npmcli/promise-spawn" "^1.3.2" + infer-owner "^1.0.4" + node-gyp "^7.1.0" + puka "^1.0.1" + read-package-json-fast "^2.0.1" + "@octokit/auth-app@^2.10.5": version "2.10.5" resolved "https://registry.npmjs.org/@octokit/auth-app/-/auth-app-2.10.5.tgz#85d69cb96818f5da34bf0b81bb637d3675ad4e9a" @@ -4113,7 +4092,7 @@ universal-github-app-jwt "^1.0.1" universal-user-agent "^6.0.0" -"@octokit/auth-token@^2.4.0", "@octokit/auth-token@^2.4.4": +"@octokit/auth-token@^2.4.4": version "2.4.4" resolved "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-2.4.4.tgz#ee31c69b01d0378c12fd3ffe406030f3d94d3b56" integrity sha512-LNfGu3Ro9uFAYh10MUZVaT7X2CnNm2C8IDQmabx+3DygYIQjs9FwzFAHN/0t6mu5HEPhxcb1XOuxdpY82vCg2Q== @@ -4155,18 +4134,16 @@ resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-2.2.0.tgz#123e0438a0bc718ccdac3b5a2e69b3dd00daa85b" integrity sha512-274lNUDonw10kT8wHg8fCcUc1ZjZHbWv0/TbAwb0ojhBQqZYc1cQ/4yqTVTtPMDeZ//g7xVEYe/s3vURkRghPg== +"@octokit/openapi-types@^4.0.3": + version "4.0.4" + resolved "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-4.0.4.tgz#96fcce11e929802898646205ac567e5df592f82b" + integrity sha512-31zY8JIuz3h6RAFOnyA8FbOwhILILiBu1qD81RyZZWY7oMBhIdBn6MaAmnnptLhB4jk0g50nkQkUVP4kUzppcA== + "@octokit/plugin-enterprise-rest@^6.0.1": version "6.0.1" resolved "https://registry.npmjs.org/@octokit/plugin-enterprise-rest/-/plugin-enterprise-rest-6.0.1.tgz#e07896739618dab8da7d4077c658003775f95437" integrity sha512-93uGjlhUD+iNg1iWhUENAtJata6w5nE+V4urXOAlIXdco6xNZtUSfYY8dzp3Udy74aqO/B5UZL80x/YMa5PKRw== -"@octokit/plugin-paginate-rest@^1.1.1": - version "1.1.2" - resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-1.1.2.tgz#004170acf8c2be535aba26727867d692f7b488fc" - integrity sha512-jbsSoi5Q1pj63sC16XIUboklNw+8tL9VOnJsWycWYR78TKss5PVpIPb1TUUcMQ+bBh7cY579cVAWmf5qG+dw+Q== - dependencies: - "@octokit/types" "^2.0.1" - "@octokit/plugin-paginate-rest@^2.6.2": version "2.7.0" resolved "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-2.7.0.tgz#6bb7b043c246e0654119a6ec4e72a172c9e2c7f3" @@ -4174,17 +4151,17 @@ dependencies: "@octokit/types" "^6.0.1" -"@octokit/plugin-request-log@^1.0.0", "@octokit/plugin-request-log@^1.0.2": +"@octokit/plugin-request-log@^1.0.2": version "1.0.2" resolved "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-1.0.2.tgz#394d59ec734cd2f122431fbaf05099861ece3c44" integrity sha512-oTJSNAmBqyDR41uSMunLQKMX0jmEXbwD1fpz8FG27lScV3RhtGfBa1/BBLym+PxcC16IBlF7KH9vP1BUYxA+Eg== -"@octokit/plugin-rest-endpoint-methods@2.4.0": - version "2.4.0" - resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-2.4.0.tgz#3288ecf5481f68c494dd0602fc15407a59faf61e" - integrity sha512-EZi/AWhtkdfAYi01obpX0DF7U6b1VRr30QNQ5xSFPITMdLSfhcBqjamE3F+sKcxPbD7eZuMHu3Qkk2V+JGxBDQ== +"@octokit/plugin-rest-endpoint-methods@4.10.3": + version "4.10.3" + resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.10.3.tgz#d78ddf926bca3b81a4d9b79a463d32b3750a4485" + integrity sha512-CsNQeVY34Vs9iea2Z9/TCPlebxv6KpjO9f1BUPz+14qundTSYT9kgf8j5wA1k37VstfBQ4xnuURYdnbGzJBJXw== dependencies: - "@octokit/types" "^2.0.1" + "@octokit/types" "^6.8.3" deprecation "^2.3.1" "@octokit/plugin-rest-endpoint-methods@4.4.1": @@ -4195,15 +4172,6 @@ "@octokit/types" "^6.1.0" deprecation "^2.3.1" -"@octokit/request-error@^1.0.2": - version "1.2.1" - resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-1.2.1.tgz#ede0714c773f32347576c25649dc013ae6b31801" - integrity sha512-+6yDyk1EES6WK+l3viRDElw96MvwfJxCt45GvmjDUKWjYIb3PJZQkq3i46TwGwoPD4h8NmTrENmtyA1FwbmhRA== - dependencies: - "@octokit/types" "^2.0.0" - deprecation "^2.0.0" - once "^1.4.0" - "@octokit/request-error@^2.0.0": version "2.0.2" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0" @@ -4213,7 +4181,7 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.2.0", "@octokit/request@^5.3.0", "@octokit/request@^5.4.11", "@octokit/request@^5.4.12": +"@octokit/request@^5.3.0", "@octokit/request@^5.4.11", "@octokit/request@^5.4.12": version "5.4.13" resolved "https://registry.npmjs.org/@octokit/request/-/request-5.4.13.tgz#eec5987b3e96f984fc5f41967e001170c6d23a18" integrity sha512-WcNRH5XPPtg7i1g9Da5U9dvZ6YbTffw9BN2rVezYiE7couoSyaRsw0e+Tl8uk1fArHE7Dn14U7YqUDy59WaqEw== @@ -4227,28 +4195,6 @@ once "^1.4.0" universal-user-agent "^6.0.0" -"@octokit/rest@^16.28.4": - version "16.43.1" - resolved "https://registry.npmjs.org/@octokit/rest/-/rest-16.43.1.tgz#3b11e7d1b1ac2bbeeb23b08a17df0b20947eda6b" - integrity sha512-gfFKwRT/wFxq5qlNjnW2dh+qh74XgTQ2B179UX5K1HYCluioWj8Ndbgqw2PVqa1NnVJkGHp2ovMpVn/DImlmkw== - dependencies: - "@octokit/auth-token" "^2.4.0" - "@octokit/plugin-paginate-rest" "^1.1.1" - "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "2.4.0" - "@octokit/request" "^5.2.0" - "@octokit/request-error" "^1.0.2" - atob-lite "^2.0.0" - before-after-hook "^2.0.0" - btoa-lite "^1.0.0" - deprecation "^2.0.0" - lodash.get "^4.4.2" - lodash.set "^4.3.2" - lodash.uniq "^4.5.0" - octokit-pagination-methods "^1.1.0" - once "^1.4.0" - universal-user-agent "^4.0.0" - "@octokit/rest@^18.0.0", "@octokit/rest@^18.0.12": version "18.0.12" resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.12.tgz#278bd41358c56d87c201e787e8adc0cac132503a" @@ -4259,12 +4205,15 @@ "@octokit/plugin-request-log" "^1.0.2" "@octokit/plugin-rest-endpoint-methods" "4.4.1" -"@octokit/types@^2.0.0", "@octokit/types@^2.0.1": - version "2.5.0" - resolved "https://registry.npmjs.org/@octokit/types/-/types-2.5.0.tgz#f1bbd147e662ae2c79717d518aac686e58257773" - integrity sha512-KEnLwOfdXzxPNL34fj508bhi9Z9cStyN7qY1kOfVahmqtAfrWw6Oq3P4R+dtsg0lYtZdWBpUrS/Ixmd5YILSww== +"@octokit/rest@^18.1.0": + version "18.1.1" + resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.1.1.tgz#bd7053c28db3577c936029e9da6bfbd046474a2f" + integrity sha512-ZcCHMyfGT1qtJD72usigAfUQ6jU89ZUPFb2AOubR6WZ7/RRFVZUENVm1I2yvJBUicqTujezPW9cY1+o3Mb4rNA== dependencies: - "@types/node" ">= 8" + "@octokit/core" "^3.2.3" + "@octokit/plugin-paginate-rest" "^2.6.2" + "@octokit/plugin-request-log" "^1.0.2" + "@octokit/plugin-rest-endpoint-methods" "4.10.3" "@octokit/types@^5.0.0", "@octokit/types@^5.0.1": version "5.5.0" @@ -4281,6 +4230,13 @@ "@octokit/openapi-types" "^2.2.0" "@types/node" ">= 8" +"@octokit/types@^6.8.3": + version "6.8.5" + resolved "https://registry.npmjs.org/@octokit/types/-/types-6.8.5.tgz#797dfdad8c75718e97dc687d4c9fc49200ca8d17" + integrity sha512-ZsQawftZoi0kSF2pCsdgLURbOjtVcHnBOXiSxBKSNF56CRjARt5rb/g8WJgqB8vv4lgUEHrv06EdDKYQ22vA9Q== + dependencies: + "@octokit/openapi-types" "^4.0.3" + "@open-draft/until@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@open-draft/until/-/until-1.0.3.tgz#db9cc719191a62e7d9200f6e7bab21c5b848adca" @@ -4623,16 +4579,16 @@ integrity sha512-In1q0tIiqTYKAGe3KOHDcFDdZRFISyQeSeipeTHGfki23ebHRZcjxvqj5SSdBkw65D4VpSREMi0s9i5iJiMcTw== "@storybook/addon-actions@^6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.1.11.tgz#73e91cc95c45ea477cfd4f3603f6b95f5829eab6" - integrity sha512-J44XLx2G732OG7Az79Cpk5UlI5SyXHeQqdykwT/4IEQXSBXAYWSTIJJjpJdcjR/D+zpklab1QDSiWxCrKbe81A== + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/addon-actions/-/addon-actions-6.1.17.tgz#9d32336284738cefa69b99acafa4b132d5533600" + integrity sha512-4hyAvmjnI4C1ZQ7/t21jKKXE0jO1zAk310BkYin0NJf77Qi0tUE1DNOwirJY/xzRih36wWi1V79c/ZOJNsLv9Q== dependencies: - "@storybook/addons" "6.1.11" - "@storybook/api" "6.1.11" - "@storybook/client-api" "6.1.11" - "@storybook/components" "6.1.11" - "@storybook/core-events" "6.1.11" - "@storybook/theming" "6.1.11" + "@storybook/addons" "6.1.17" + "@storybook/api" "6.1.17" + "@storybook/client-api" "6.1.17" + "@storybook/components" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/theming" "6.1.17" core-js "^3.0.1" fast-deep-equal "^3.1.1" global "^4.3.2" @@ -4698,7 +4654,7 @@ global "^4.3.2" regenerator-runtime "^0.13.7" -"@storybook/addons@6.1.15", "@storybook/addons@^6.1.11": +"@storybook/addons@6.1.15": version "6.1.15" resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.15.tgz#09eb8d962f58bd20b4ac2f83b515831c83226352" integrity sha512-ENyHapLFOG93VaoQXPX8O3IWjLRyVBox9C9P20LMruKX/SfXAXx20qsoAWKKPGssopyOin17aoQX9pj+lFmCZQ== @@ -4713,6 +4669,21 @@ global "^4.3.2" regenerator-runtime "^0.13.7" +"@storybook/addons@6.1.17", "@storybook/addons@^6.1.11": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/addons/-/addons-6.1.17.tgz#ab0666446acb9fc19c94d7204dc9aafdefb6c7c2" + integrity sha512-3upDPJPzUkls2V3Fozzg+JOcv138bF90pbdRe9YSNu37QvRIL+iQODY7oFygMl+kqjG2F1FGw5EvxAV1mnlwCw== + dependencies: + "@storybook/api" "6.1.17" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/router" "6.1.17" + "@storybook/theming" "6.1.17" + 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" @@ -4763,18 +4734,30 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/channel-postmessage@6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.11.tgz#62c1079f04870dd27925bd538a2020e7380daa2e" - integrity sha512-voW4Z2SUacDOxwN2q1NEBL//8OpgvL2C5CeoG1VQyEllKM8Vg9t1Nxo2FFTJBzv5LeEX7VIJKeBoB25DYvKyng== +"@storybook/api@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/api/-/api-6.1.17.tgz#50393ce9b718063b67680212df895eceacc0c11d" + integrity sha512-sthcfuk2EQ3F5R620PBqpI4Pno3g7KQm6YPZA0DXB+LD/z61xH9ToE1gTLF4nzlE6HwghwkXOZyRwDowRdG+7A== dependencies: - "@storybook/channels" "6.1.11" - "@storybook/client-logger" "6.1.11" - "@storybook/core-events" "6.1.11" + "@reach/router" "^1.3.3" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/csf" "0.0.1" + "@storybook/router" "6.1.17" + "@storybook/semver" "^7.3.2" + "@storybook/theming" "6.1.17" + "@types/reach__router" "^1.3.7" core-js "^3.0.1" + fast-deep-equal "^3.1.1" global "^4.3.2" - qs "^6.6.0" + 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" @@ -4789,6 +4772,19 @@ qs "^6.6.0" telejson "^5.0.2" +"@storybook/channel-postmessage@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/channel-postmessage/-/channel-postmessage-6.1.17.tgz#309ce67c94637ec13319d4ce360a8f3742ddbaf4" + integrity sha512-2nVqxq4oZdSITqhFOnkh1rmDMjCwHuobnK5Fp3l7ftCkbmiZHMheKK9Tz4Rb803dhXvcGYs0zRS8NjKyxlOLsA== + dependencies: + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + core-js "^3.0.1" + global "^4.3.2" + qs "^6.6.0" + telejson "^5.0.2" + "@storybook/channels@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.11.tgz#a93a83746ad78dd40e1c056029f6d93b17bb66bc" @@ -4807,27 +4803,12 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" -"@storybook/client-api@6.1.11": - version "6.1.11" - resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.11.tgz#d25aac484ca84a1acb01d450e756a62408f00c1a" - integrity sha512-DodJQzGCR+PYs26klvbquTjfBgkw5nvCZd3jpgWQtOrYaY/cMY1LLkVkKqrm2ENW8f7vf7tiw78RtxaXy7xeIQ== +"@storybook/channels@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/channels/-/channels-6.1.17.tgz#2cc89a6b9727d19c24b15fa3cb15569b469db864" + integrity sha512-MUdj0eKr/AbxevHTSXX7AsgxAz6e5O4ZxoYX5G8ggoqSXrWzws6zRFmUmmTdjpIvVmP2M1Kh4SYFAKcS/AGw9w== dependencies: - "@storybook/addons" "6.1.11" - "@storybook/channel-postmessage" "6.1.11" - "@storybook/channels" "6.1.11" - "@storybook/client-logger" "6.1.11" - "@storybook/core-events" "6.1.11" - "@storybook/csf" "0.0.1" - "@types/qs" "^6.9.0" - "@types/webpack-env" "^1.15.3" core-js "^3.0.1" - global "^4.3.2" - lodash "^4.17.15" - memoizerific "^1.11.3" - qs "^6.6.0" - regenerator-runtime "^0.13.7" - stable "^0.1.8" - store2 "^2.7.1" ts-dedent "^2.0.0" util-deprecate "^1.0.2" @@ -4855,6 +4836,30 @@ ts-dedent "^2.0.0" util-deprecate "^1.0.2" +"@storybook/client-api@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/client-api/-/client-api-6.1.17.tgz#3ced22f08a47af70ccf8929111bc44b79e9e8ec0" + integrity sha512-Loz/wdh0axgq0PS19tx0tGEFEkFWlYc6YauJGHjygYa1xX7mJ54hDoaTolySCXN1HtfZn08D847yjGSN2oIqVg== + dependencies: + "@storybook/addons" "6.1.17" + "@storybook/channel-postmessage" "6.1.17" + "@storybook/channels" "6.1.17" + "@storybook/client-logger" "6.1.17" + "@storybook/core-events" "6.1.17" + "@storybook/csf" "0.0.1" + "@types/qs" "^6.9.0" + "@types/webpack-env" "^1.15.3" + core-js "^3.0.1" + global "^4.3.2" + lodash "^4.17.15" + memoizerific "^1.11.3" + qs "^6.6.0" + regenerator-runtime "^0.13.7" + stable "^0.1.8" + store2 "^2.7.1" + ts-dedent "^2.0.0" + util-deprecate "^1.0.2" + "@storybook/client-logger@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.11.tgz#5dd092e4293e5f58f7e89ddbc6eb2511b7d60954" @@ -4871,6 +4876,14 @@ core-js "^3.0.1" global "^4.3.2" +"@storybook/client-logger@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/client-logger/-/client-logger-6.1.17.tgz#0d89aaf824457f19bf9aa585bbcada57595e7d01" + integrity sha512-oqExrxhmws0ihB47sjdynZHd3OpUP4KWkx4udG+74lYIvBH+EZmQ9xF+UofeY3j5p1I9k8ugEcVKy0sqh1yR3w== + dependencies: + core-js "^3.0.1" + global "^4.3.2" + "@storybook/components@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.11.tgz#edd5db7fe43f47b5a7ab515840795a89d931512e" @@ -4923,6 +4936,32 @@ react-textarea-autosize "^8.1.1" ts-dedent "^2.0.0" +"@storybook/components@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/components/-/components-6.1.17.tgz#f92d36e370ec6039d8c7cee9ef13dda866eed3da" + integrity sha512-rIEll0UTxEKmG4IsSS5K+6DjRLVtX8J+9cg79GSAC7N1ZHUR1UQmjjJaehJa5q/NQ5H8C39acxpT4Py/BcsL2g== + dependencies: + "@popperjs/core" "^2.5.4" + "@storybook/client-logger" "6.1.17" + "@storybook/csf" "0.0.1" + "@storybook/theming" "6.1.17" + "@types/overlayscrollbars" "^1.9.0" + "@types/react-color" "^3.0.1" + "@types/react-syntax-highlighter" "11.0.4" + core-js "^3.0.1" + fast-deep-equal "^3.1.1" + global "^4.3.2" + lodash "^4.17.15" + markdown-to-jsx "^6.11.4" + memoizerific "^1.11.3" + overlayscrollbars "^1.10.2" + polished "^3.4.4" + react-color "^2.17.0" + react-popper-tooltip "^3.1.1" + react-syntax-highlighter "^13.5.0" + react-textarea-autosize "^8.1.1" + ts-dedent "^2.0.0" + "@storybook/core-events@6.1.11": version "6.1.11" resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.11.tgz#d50e8ec90490f9a7180a8c8a83afb6dcfe47ed66" @@ -4937,6 +4976,13 @@ dependencies: core-js "^3.0.1" +"@storybook/core-events@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/core-events/-/core-events-6.1.17.tgz#697ed916fcb2a411bc9f8bdbfacd0eb9d394eb58" + integrity sha512-xBI7kmyROcqhYNmFv4QBjD77CzV+k/0F051YFS5WicEI4qDWPPvzaShhm96ZrGobUX3+di4pC11gqdsrFeNCEg== + 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" @@ -5113,6 +5159,18 @@ memoizerific "^1.11.3" qs "^6.6.0" +"@storybook/router@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/router/-/router-6.1.17.tgz#96746878c50c6c97c7de5a1b23a9503c5d648775" + integrity sha512-wLqSOB5yLXgNyDGy008RUvjVRtVMq7lhmMRicSIxgJpkakPrMRN8n/nK7pxgQc/xDTphnS0u1nT01i97WszhCg== + 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" @@ -5174,6 +5232,24 @@ resolve-from "^5.0.0" ts-dedent "^2.0.0" +"@storybook/theming@6.1.17": + version "6.1.17" + resolved "https://registry.npmjs.org/@storybook/theming/-/theming-6.1.17.tgz#99cc120a230c30458d833b40c806b9b4dff7b34a" + integrity sha512-LpRuY2aIh2td+qZi7E8cp2oM88LudNMmTsBT6N2/Id69u/a9qQd2cYCA9k9fAsg7rjor+wR/N695jk3SGtoFTw== + dependencies: + "@emotion/core" "^10.1.1" + "@emotion/is-prop-valid" "^0.8.6" + "@emotion/styled" "^10.0.23" + "@storybook/client-logger" "6.1.17" + 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" @@ -6259,7 +6335,7 @@ dependencies: "@types/webpack" "*" -"@types/minimatch@*": +"@types/minimatch@*", "@types/minimatch@^3.0.3": version "3.0.3" resolved "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== @@ -7162,16 +7238,7 @@ resolved "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz#e77a97fbd345b76d83245edcd17d393b1b41fb31" integrity sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ== -"@zkochan/cmd-shim@^3.1.0": - version "3.1.0" - resolved "https://registry.npmjs.org/@zkochan/cmd-shim/-/cmd-shim-3.1.0.tgz#2ab8ed81f5bb5452a85f25758eb9b8681982fd2e" - integrity sha512-o8l0+x7C7sMZU3v9GuJIAU10qQLtwR1dtRQIOmlNMtyaqhmpXOzx1HWiYoWfmmf9HHZoAkXpc9TM9PQYF9d4Jg== - dependencies: - is-windows "^1.0.0" - mkdirp-promise "^5.0.1" - mz "^2.5.0" - -JSONStream@^1.0.4, JSONStream@^1.3.4: +JSONStream@^1.0.4: version "1.3.5" resolved "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz#3208c1f08d3a4d99261ab64f92302bc15e111ca0" integrity sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ== @@ -7260,18 +7327,16 @@ acorn@^7.4.0: resolved "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== +add-stream@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/add-stream/-/add-stream-1.0.0.tgz#6a7990437ca736d5e1288db92bd3266d5f5cb2aa" + integrity sha1-anmQQ3ynNtXhKI25K9MmbV9csqo= + address@1.1.2, address@^1.0.1: version "1.1.2" resolved "https://registry.npmjs.org/address/-/address-1.1.2.tgz#bf1116c9c758c51b7a933d296b72c221ed9428b6" integrity sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA== -agent-base@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" - agent-base@6: version "6.0.1" resolved "https://registry.npmjs.org/agent-base/-/agent-base-6.0.1.tgz#808007e4e5867decb0ab6ab2f928fbdb5a596db4" @@ -7279,18 +7344,13 @@ agent-base@6: dependencies: debug "4" -agent-base@~4.2.1: - version "4.2.1" - resolved "https://registry.npmjs.org/agent-base/-/agent-base-4.2.1.tgz#d89e5999f797875674c07d87f260fc41e83e8ca9" - integrity sha512-JVwXMr9nHYTUXsBFKUqhJwvlcYU/blreOEUkhNR2eXZIvwd+c+o5V4MgDPKWnMS/56awN3TRzIP+KoPn+roQtg== - dependencies: - es6-promisify "^5.0.0" - -agentkeepalive@^3.4.1: - version "3.5.2" - resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-3.5.2.tgz#a113924dd3fa24a0bc3b78108c450c2abee00f67" - integrity sha512-e0L/HNe6qkQ7H19kTlRRqUibEAwDK5AFk6y3PtMsuut2VAH6+Q4xZml1tNDJD7kSAyqmbG/K08K5WEJYtUrSlQ== +agentkeepalive@^4.1.3: + version "4.1.4" + resolved "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.1.4.tgz#d928028a4862cb11718e55227872e842a44c945b" + integrity sha512-+V/rGa3EuU74H6wR04plBb7Ks10FbtUQgRj/FQOG7uUIEuaINI+AiqJR1k6t3SVNs7o7ZjIdus6706qqzVq8jQ== dependencies: + debug "^4.1.0" + depd "^1.1.2" humanize-ms "^1.2.1" aggregate-error@3.0.1, aggregate-error@^3.0.0: @@ -7383,7 +7443,7 @@ ansi-colors@^4.1.1: resolved "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348" integrity sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA== -ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: +ansi-escapes@^3.0.0: version "3.2.0" resolved "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== @@ -7785,10 +7845,10 @@ arr-union@^3.1.0: resolved "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz#e39b09aea9def866a8f206e288af63919bae39c4" integrity sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ= -array-differ@^2.0.3: - version "2.1.0" - resolved "https://registry.npmjs.org/array-differ/-/array-differ-2.1.0.tgz#4b9c1c3f14b906757082925769e8ab904f4801b1" - integrity sha512-KbUpJgx909ZscOc/7CLATBFam7P1Z1QRQInvgT0UztM9Q72aGKCunKASAl7WNW0tnPmPyEMeMhdsfWhfmW037w== +array-differ@^3.0.0: + version "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" @@ -8018,11 +8078,6 @@ at-least-node@^1.0.0: resolved "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== -atob-lite@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/atob-lite/-/atob-lite-2.0.0.tgz#0fef5ad46f1bd7a8502c65727f0367d5ee43d696" - integrity sha1-D+9a1G8b16hQLGVyfwNn1e5D1pY= - atob@^2.1.2: version "2.1.2" resolved "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz#6d9517eb9e030d2436666651e86bd9f6f13533c9" @@ -8579,7 +8634,7 @@ bcrypt-pbkdf@^1.0.0, bcrypt-pbkdf@^1.0.2: dependencies: tweetnacl "^0.14.3" -before-after-hook@^2.0.0, before-after-hook@^2.1.0: +before-after-hook@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/before-after-hook/-/before-after-hook-2.1.0.tgz#b6c03487f44e24200dd30ca5e6a1979c5d2fb635" integrity sha512-IWIbu7pMqyw3EAJHzzHbWa85b6oud/yfKYg5rqB5hNE8CeMi3nX+2C2sj0HswfblST86hpVEOAb9x34NZd6P7A== @@ -8664,7 +8719,7 @@ block-stream@*: dependencies: inherits "~2.0.0" -bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.3, bluebird@^3.5.5, bluebird@^3.7.2: +bluebird@3.7.2, bluebird@^3.3.5, bluebird@^3.5.1, bluebird@^3.5.5, bluebird@^3.7.2: version "3.7.2" resolved "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz#9f229c15be272454ffa973ace0dbee79a1b0c36f" integrity sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg== @@ -8896,11 +8951,6 @@ bser@2.1.1: dependencies: node-int64 "^0.4.0" -btoa-lite@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/btoa-lite/-/btoa-lite-1.0.0.tgz#337766da15801210fdd956c22e9c6891ab9d0337" - integrity sha1-M3dm2hWAEhD92VbCLpxokaudAzc= - btoa@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/btoa/-/btoa-1.2.1.tgz#01a9909f8b2c93f6bf680ba26131eb30f7fa3d73" @@ -9020,10 +9070,10 @@ byline@^5.0.0: resolved "https://registry.npmjs.org/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1" integrity sha1-dBxSFkaOrcRXsDQQEYrXfejB3bE= -byte-size@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/byte-size/-/byte-size-5.0.1.tgz#4b651039a5ecd96767e71a3d7ed380e48bed4191" - integrity sha512-/XuKeqWocKsYa/cBY1YbSJSWWqTi4cFgr9S6OyM7PBaPbr9zvNGwWP33vt0uqGhwDdN+y3yhbXVILEUpnwEWGw== +byte-size@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/byte-size/-/byte-size-7.0.0.tgz#36528cd1ca87d39bd9abd51f5715dc93b6ceb032" + integrity sha512-NNiBxKgxybMBtWdmvx7ZITJi4ZG+CYUgwOSZTfqB1qogkRHrhbQE/R2r5Fh94X+InN5MCYz6SvB/ejHMj/HbsQ== bytes@3.0.0: version "3.0.0" @@ -9035,7 +9085,7 @@ bytes@3.1.0: resolved "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz#f6cf7933a360e0588fa9fde85651cdc7f805d1f6" integrity sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg== -cacache@^12.0.0, cacache@^12.0.2, cacache@^12.0.3: +cacache@^12.0.2: version "12.0.3" resolved "https://registry.npmjs.org/cacache/-/cacache-12.0.3.tgz#be99abba4e1bf5df461cd5a2c1071fc432573390" integrity sha512-kqdmfXEGFepesTuROHMs3MpFLWrPkSSpRqOw80RCflZXy/khxaArvFrQ7uJxSUduzAufc6G0g1VUCOZXxWavPw== @@ -9183,15 +9233,6 @@ camelcase-keys@^2.0.0: camelcase "^2.0.0" map-obj "^1.0.0" -camelcase-keys@^4.0.0: - version "4.2.0" - resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-4.2.0.tgz#a2aa5fb1af688758259c32c141426d78923b9b77" - integrity sha1-oqpfsa9oh1glnDLBQUJteJI7m3c= - dependencies: - camelcase "^4.1.0" - map-obj "^2.0.0" - quick-lru "^1.0.0" - camelcase-keys@^6.2.2: version "6.2.2" resolved "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz#5e755d6ba51aa223ec7d3d52f25778210f9dc3c0" @@ -9211,7 +9252,7 @@ camelcase@^3.0.0: resolved "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz#32fc4b9fcdaf845fcdf7e73bb97cac2261f0ab0a" integrity sha1-MvxLn82vhF/N9+c7uXysImHwqwo= -camelcase@^4.0.0, camelcase@^4.1.0: +camelcase@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/camelcase/-/camelcase-4.1.0.tgz#d545635be1e33c542649c69173e5de6acfae34dd" integrity sha1-1UVjW+HjPFQmScaRc+Xeas+uNN0= @@ -9296,7 +9337,7 @@ chainsaw@~0.1.0: dependencies: traverse ">=0.3.0 <0.4" -chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.4.1, chalk@^2.4.2: +chalk@2.4.2, chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -9401,7 +9442,7 @@ chokidar@^3.2.2, chokidar@^3.3.0, chokidar@^3.3.1, chokidar@^3.4.1, chokidar@^3. optionalDependencies: fsevents "~2.1.2" -chownr@^1.1.1, chownr@^1.1.2: +chownr@^1.1.1: version "1.1.4" resolved "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz#6fc9d7b42d32a583596337666e7d08084da2cc6b" integrity sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg== @@ -9644,6 +9685,13 @@ clsx@^1.0.1, clsx@^1.0.2, clsx@^1.0.4, clsx@^1.1.0: resolved "https://registry.npmjs.org/clsx/-/clsx-1.1.1.tgz#98b3134f9abbdf23b2663491ace13c5c03a73188" integrity sha512-6/bPho624p3S2pMyvP5kKBPXnI3ufHLObBFCfgx+LkeR5lg2XYy2hqZqUf45ypD8COn2bhgGJSUE+l5dhNBieA== +cmd-shim@^4.1.0: + version "4.1.0" + resolved "https://registry.npmjs.org/cmd-shim/-/cmd-shim-4.1.0.tgz#b3a904a6743e9fede4148c6f3800bf2a08135bdd" + integrity sha512-lb9L7EM4I/ZRVuljLPEtUJOP+xiQVknZ4ZMpMgEp4JzNldPb27HU03hi6K1/6CoIuit/Zm/LQXySErFeXxDprw== + dependencies: + mkdirp-infer-owner "^2.0.0" + co@^4.6.0: version "4.6.0" resolved "https://registry.npmjs.org/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184" @@ -9835,13 +9883,13 @@ commondir@^1.0.1: resolved "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz#ddd800da0c66127393cca5950ea968a3aaf1253b" integrity sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs= -compare-func@^1.3.1: - version "1.3.2" - resolved "https://registry.npmjs.org/compare-func/-/compare-func-1.3.2.tgz#99dd0ba457e1f9bc722b12c08ec33eeab31fa648" - integrity sha1-md0LpFfh+bxyKxLAjsM+6rMfpkg= +compare-func@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz#fb65e75edbddfd2e568554e8b5b05fff7a51fcb3" + integrity sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA== dependencies: array-ify "^1.0.0" - dot-prop "^3.0.0" + dot-prop "^5.1.0" compare-versions@^3.6.0: version "3.6.0" @@ -9954,7 +10002,7 @@ concurrently@^5.2.0: tree-kill "^1.2.2" yargs "^13.3.0" -config-chain@^1.1.11: +config-chain@^1.1.12: version "1.1.12" resolved "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz#0fde8d091200eb5e808caf25fe618c02f48e4efa" integrity sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA== @@ -10025,87 +10073,89 @@ content-type@~1.0.4: resolved "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" integrity sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA== -conventional-changelog-angular@^5.0.3: - version "5.0.6" - resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.6.tgz#269540c624553aded809c29a3508fdc2b544c059" - integrity sha512-QDEmLa+7qdhVIv8sFZfVxU1VSyVvnXPsxq8Vam49mKUcO1Z8VTLEJk9uI21uiJUsnmm0I4Hrsdc9TgkOQo9WSA== +conventional-changelog-angular@^5.0.12: + version "5.0.12" + resolved "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz#c979b8b921cbfe26402eb3da5bbfda02d865a2b9" + integrity sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw== dependencies: - compare-func "^1.3.1" + compare-func "^2.0.0" q "^1.5.1" -conventional-changelog-core@^3.1.6: - version "3.2.3" - resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz#b31410856f431c847086a7dcb4d2ca184a7d88fb" - integrity sha512-LMMX1JlxPIq/Ez5aYAYS5CpuwbOk6QFp8O4HLAcZxe3vxoCtABkhfjetk8IYdRB9CDQGwJFLR3Dr55Za6XKgUQ== +conventional-changelog-core@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/conventional-changelog-core/-/conventional-changelog-core-4.2.2.tgz#f0897df6d53b5d63dec36b9442bd45354f8b3ce5" + integrity sha512-7pDpRUiobQDNkwHyJG7k9f6maPo9tfPzkSWbRq97GGiZqisElhnvUZSvyQH20ogfOjntB5aadvv6NNcKL1sReg== dependencies: - conventional-changelog-writer "^4.0.6" - conventional-commits-parser "^3.0.3" + add-stream "^1.0.0" + conventional-changelog-writer "^4.0.18" + conventional-commits-parser "^3.2.0" dateformat "^3.0.0" get-pkg-repo "^1.0.0" - git-raw-commits "2.0.0" + git-raw-commits "^2.0.8" git-remote-origin-url "^2.0.0" - git-semver-tags "^2.0.3" - lodash "^4.2.1" - normalize-package-data "^2.3.5" + git-semver-tags "^4.1.1" + lodash "^4.17.15" + normalize-package-data "^3.0.0" q "^1.5.1" read-pkg "^3.0.0" read-pkg-up "^3.0.0" - through2 "^3.0.0" + shelljs "^0.8.3" + through2 "^4.0.0" -conventional-changelog-preset-loader@^2.1.1: - version "2.3.0" - resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.0.tgz#580fa8ab02cef22c24294d25e52d7ccd247a9a6a" - integrity sha512-/rHb32J2EJnEXeK4NpDgMaAVTFZS3o1ExmjKMtYVgIC4MQn0vkNSbYpdGRotkfGGRWiqk3Ri3FBkiZGbAfIfOQ== +conventional-changelog-preset-loader@^2.3.4: + version "2.3.4" + resolved "https://registry.npmjs.org/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" + integrity sha512-GEKRWkrSAZeTq5+YjUZOYxdHq+ci4dNwHvpaBC3+ENalzFWuCWa9EZXSuZBpkr72sMdKB+1fyDV4takK1Lf58g== -conventional-changelog-writer@^4.0.6: - version "4.0.11" - resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.0.11.tgz#9f56d2122d20c96eb48baae0bf1deffaed1edba4" - integrity sha512-g81GQOR392I+57Cw3IyP1f+f42ME6aEkbR+L7v1FBBWolB0xkjKTeCWVguzRrp6UiT1O6gBpJbEy2eq7AnV1rw== +conventional-changelog-writer@^4.0.18: + version "4.1.0" + resolved "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-4.1.0.tgz#1ca7880b75aa28695ad33312a1f2366f4b12659f" + integrity sha512-WwKcUp7WyXYGQmkLsX4QmU42AZ1lqlvRW9mqoyiQzdD+rJWbTepdWoKJuwXTS+yq79XKnQNa93/roViPQrAQgw== dependencies: - compare-func "^1.3.1" - conventional-commits-filter "^2.0.2" + compare-func "^2.0.0" + conventional-commits-filter "^2.0.7" dateformat "^3.0.0" - handlebars "^4.4.0" + handlebars "^4.7.6" json-stringify-safe "^5.0.1" lodash "^4.17.15" - meow "^5.0.0" + meow "^8.0.0" semver "^6.0.0" split "^1.0.0" - through2 "^3.0.0" + through2 "^4.0.0" -conventional-commits-filter@^2.0.2: - version "2.0.2" - resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.2.tgz#f122f89fbcd5bb81e2af2fcac0254d062d1039c1" - integrity sha512-WpGKsMeXfs21m1zIw4s9H5sys2+9JccTzpN6toXtxhpw2VNF2JUXwIakthKBy+LN4DvJm+TzWhxOMWOs1OFCFQ== +conventional-commits-filter@^2.0.7: + version "2.0.7" + resolved "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" + integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== dependencies: lodash.ismatch "^4.4.0" modify-values "^1.0.0" -conventional-commits-parser@^3.0.3: - version "3.0.8" - resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.0.8.tgz#23310a9bda6c93c874224375e72b09fb275fe710" - integrity sha512-YcBSGkZbYp7d+Cr3NWUeXbPDFUN6g3SaSIzOybi8bjHL5IJ5225OSCxJJ4LgziyEJ7AaJtE9L2/EU6H7Nt/DDQ== +conventional-commits-parser@^3.2.0: + version "3.2.1" + resolved "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-3.2.1.tgz#ba44f0b3b6588da2ee9fd8da508ebff50d116ce2" + integrity sha512-OG9kQtmMZBJD/32NEw5IhN5+HnBqVjy03eC+I71I0oQRFA5rOgA4OtPOYG7mz1GkCfCNxn3gKIX8EiHJYuf1cA== dependencies: JSONStream "^1.0.4" is-text-path "^1.0.1" lodash "^4.17.15" - meow "^5.0.0" - split2 "^2.0.0" - through2 "^3.0.0" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" trim-off-newlines "^1.0.0" -conventional-recommended-bump@^5.0.0: - version "5.0.1" - resolved "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-5.0.1.tgz#5af63903947b6e089e77767601cb592cabb106ba" - integrity sha512-RVdt0elRcCxL90IrNP0fYCpq1uGt2MALko0eyeQ+zQuDVWtMGAy9ng6yYn3kax42lCj9+XBxQ8ZN6S9bdKxDhQ== +conventional-recommended-bump@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/conventional-recommended-bump/-/conventional-recommended-bump-6.1.0.tgz#cfa623285d1de554012f2ffde70d9c8a22231f55" + integrity sha512-uiApbSiNGM/kkdL9GTOLAqC4hbptObFo4wW2QRyHsKciGAfQuLU1ShZ1BIVI/+K2BE/W1AWYQMCXAsv4dyKPaw== dependencies: concat-stream "^2.0.0" - conventional-changelog-preset-loader "^2.1.1" - conventional-commits-filter "^2.0.2" - conventional-commits-parser "^3.0.3" - git-raw-commits "2.0.0" - git-semver-tags "^2.0.3" - meow "^4.0.0" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter "^2.0.7" + conventional-commits-parser "^3.2.0" + git-raw-commits "^2.0.8" + git-semver-tags "^4.1.1" + meow "^8.0.0" q "^1.5.1" convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, convert-source-map@^1.7.0: @@ -10214,7 +10264,7 @@ cosmiconfig@6.0.0, cosmiconfig@^6.0.0: path-type "^4.0.0" yaml "^1.7.2" -cosmiconfig@^5.0.0, cosmiconfig@^5.1.0: +cosmiconfig@^5.0.0: version "5.2.1" resolved "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz#040f726809c591e77a17c0a3626ca45b4f168b1a" integrity sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA== @@ -10366,7 +10416,7 @@ cross-spawn@^6.0.0: shebang-command "^1.2.0" which "^1.2.9" -cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2: +cross-spawn@^7.0.0, cross-spawn@^7.0.1, cross-spawn@^7.0.2, cross-spawn@^7.0.3: version "7.0.3" resolved "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz#f73a85b9d5d41d045551c177e2882d4ac85728a6" integrity sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w== @@ -10911,12 +10961,10 @@ damerau-levenshtein@^1.0.4: resolved "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.6.tgz#143c1641cb3d85c60c32329e26899adea8701791" integrity sha512-JVrozIeElnj3QzfUIt8tB8YMluBJom4Vw9qTPpjGYQ9fYlB3D/rb6OordUxf3xeFB35LKWs0xqcO5U6ySvBtug== -dargs@^4.0.1: - version "4.1.0" - resolved "https://registry.npmjs.org/dargs/-/dargs-4.1.0.tgz#03a9dbb4b5c2f139bf14ae53f0b8a2a6a86f4e17" - integrity sha1-A6nbtLXC8Tm/FK5T8LiipqhvThc= - dependencies: - number-is-nan "^1.0.0" +dargs@^7.0.0: + version "7.0.0" + resolved "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz#04015c41de0bcb69ec84050f3d9be0caf8d6d5cc" + integrity sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg== dashdash@^1.12.0: version "1.14.1" @@ -10995,13 +11043,6 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.9: dependencies: ms "2.0.0" -debug@3.1.0, debug@=3.1.0: - version "3.1.0" - resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - debug@4, debug@^4.0.0, debug@^4.0.1, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0: version "4.2.0" resolved "https://registry.npmjs.org/debug/-/debug-4.2.0.tgz#7f150f93920e94c58f5574c2fd01a3110effe7f1" @@ -11016,6 +11057,13 @@ debug@4.1.1: dependencies: ms "^2.1.1" +debug@=3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.5, debug@^3.2.6: version "3.2.6" resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -11028,7 +11076,7 @@ debuglog@^1.0.1: resolved "https://registry.npmjs.org/debuglog/-/debuglog-1.0.1.tgz#aa24ffb9ac3df9a2351837cfb2d279360cd78492" integrity sha1-qiT/uaw9+aI1GDfPstJ5NgzXhJI= -decamelize-keys@^1.0.0, decamelize-keys@^1.1.0: +decamelize-keys@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz#d171a87933252807eb3cb61dc1c1445d078df2d9" integrity sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk= @@ -11210,7 +11258,7 @@ delegates@^1.0.0: resolved "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" integrity sha1-hMbhWbgZBP3KWaDvRM2HDTElD5o= -depd@~1.1.2: +depd@^1.1.2, depd@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" integrity sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak= @@ -11474,6 +11522,15 @@ dom-serializer@0, dom-serializer@^0.2.1: domelementtype "^2.0.1" entities "^2.0.0" +dom-serializer@^1.0.1: + version "1.2.0" + resolved "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz#3433d9136aeb3c627981daa385fc7f32d27c48f1" + integrity sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + entities "^2.0.0" + dom-walk@^0.1.0: version "0.1.1" resolved "https://registry.npmjs.org/dom-walk/-/dom-walk-0.1.1.tgz#672226dc74c8f799ad35307df936aba11acd6018" @@ -11494,6 +11551,11 @@ domelementtype@^2.0.1: resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.0.1.tgz#1f8bdfe91f5a78063274e803b4bdcedf6e94f94d" integrity sha512-5HOHUDsYZWV8FGWN0Njbr/Rn7f/eWSQi1v7+HsUVwXgn8nWWlL64zKDkS0n8ZmQ3mlWOMuXOnR+7Nx/5tMO5AQ== +domelementtype@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz#a851c080a6d1c3d94344aed151d99f669edf585e" + integrity sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w== + domexception@^1.0.1: version "1.0.1" resolved "https://registry.npmjs.org/domexception/-/domexception-1.0.1.tgz#937442644ca6a31261ef36e3ec677fe805582c90" @@ -11522,6 +11584,13 @@ domhandler@^3.0, domhandler@^3.0.0: dependencies: domelementtype "^2.0.1" +domhandler@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz#01ea7821de996d85f69029e81fa873c21833098e" + integrity sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA== + dependencies: + domelementtype "^2.1.0" + dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.3: version "2.2.3" resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.2.3.tgz#ec653ba521b39f397c2ca045769438d593ea8a9f" @@ -11552,6 +11621,15 @@ domutils@^2.0.0: domelementtype "^2.0.1" domhandler "^3.0.0" +domutils@^2.4.4: + version "2.4.4" + resolved "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz#282739c4b150d022d34699797369aad8d19bbbd3" + integrity sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA== + dependencies: + dom-serializer "^1.0.1" + domelementtype "^2.0.1" + domhandler "^4.0.0" + dot-case@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/dot-case/-/dot-case-3.0.3.tgz#21d3b52efaaba2ea5fda875bb1aa8124521cf4aa" @@ -11560,19 +11638,12 @@ dot-case@^3.0.3: no-case "^3.0.3" tslib "^1.10.0" -dot-prop@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-3.0.0.tgz#1b708af094a49c9a0e7dbcad790aba539dac1177" - integrity sha1-G3CK8JSknJoOfbyteQq6U52sEXc= +dot-prop@^5.1.0: + version "5.3.0" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz#90ccce708cd9cd82cc4dc8c3ddd9abdd55b20e88" + integrity sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q== dependencies: - is-obj "^1.0.0" - -dot-prop@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-4.2.0.tgz#1f19e0c2e1aa0e32797c49799f2837ac6af69c57" - integrity sha512-tUMXrxlExSW6U2EXiiKGSBVdYgtV8qlHL+C10TsW4PURY/ic+eaysnSkwB4kA/mBlCyy/IKDJ+Lc3wbWeaXtuQ== - dependencies: - is-obj "^1.0.0" + is-obj "^2.0.0" dot-prop@^5.2.0: version "5.2.0" @@ -11581,6 +11652,13 @@ dot-prop@^5.2.0: dependencies: is-obj "^2.0.0" +dot-prop@^6.0.1: + version "6.0.1" + resolved "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz#fc26b3cf142b9e59b74dbd39ed66ce620c681083" + integrity sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA== + dependencies: + is-obj "^2.0.0" + dotenv-defaults@^1.0.2: version "1.1.1" resolved "https://registry.npmjs.org/dotenv-defaults/-/dotenv-defaults-1.1.1.tgz#032c024f4b5906d9990eb06d722dc74cc60ec1bd" @@ -11782,12 +11860,12 @@ encodeurl@~1.0.2: resolved "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" integrity sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k= -encoding@^0.1.11: - version "0.1.12" - resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" - integrity sha1-U4tm8+5izRq1HsMjgp0flIDHS+s= +encoding@^0.1.12: + version "0.1.13" + resolved "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz#56574afdd791f54a8e9b2785c0582a2d26210fa9" + integrity sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A== dependencies: - iconv-lite "~0.4.13" + iconv-lite "^0.6.2" end-of-stream@^1.0.0, end-of-stream@^1.1.0, end-of-stream@^1.4.1: version "1.4.4" @@ -11846,15 +11924,15 @@ env-variable@0.0.x: resolved "https://registry.npmjs.org/env-variable/-/env-variable-0.0.6.tgz#74ab20b3786c545b62b4a4813ab8cf22726c9808" integrity sha512-bHz59NlBbtS0NhftmR8+ExBEekE7br0e01jw+kk0NDro7TtZzBYZ5ScGPs3OmwnpyfHTHOtr1Y6uedCdrIldtg== -envinfo@^7.3.1: - version "7.5.0" - resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.5.0.tgz#91410bb6db262fb4f1409bd506e9ff57e91023f4" - integrity sha512-jDgnJaF/Btomk+m3PZDTTCb5XIIIX3zYItnCRfF73zVgvinLoRomuhi75Y4su0PtQxWz4v66XnLLckyvyJTOIQ== +envinfo@^7.7.4: + version "7.7.4" + resolved "https://registry.npmjs.org/envinfo/-/envinfo-7.7.4.tgz#c6311cdd38a0e86808c1c9343f667e4267c4a320" + integrity sha512-TQXTYFVVwwluWSFis6K2XKxgrD22jEv0FTuLCQI+OjH7rn93+iY0fSSFM5lrSxFY+H1+B0/cvvlamr3UsBivdQ== -err-code@^1.0.0: - version "1.1.2" - resolved "https://registry.npmjs.org/err-code/-/err-code-1.1.2.tgz#06e0116d3028f6aef4806849eb0ea6a748ae6960" - integrity sha1-BuARbTAo9q70gGhJ6w6mp0iuaWA= +err-code@^2.0.2: + version "2.0.3" + resolved "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz#23c2f3b756ffdfc608d30e27c9a941024807e7f9" + integrity sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA== errno@^0.1.3, errno@~0.1.7: version "0.1.7" @@ -11979,18 +12057,6 @@ 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" @@ -12049,6 +12115,11 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= +escape-string-regexp@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" + integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== + escodegen@^1.14.1, escodegen@^1.9.1: version "1.14.3" resolved "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz#4e7b81fba61581dc97582ed78cab7f0e8d63f503" @@ -12394,6 +12465,11 @@ eventemitter3@^4.0.0: resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.0.tgz#d65176163887ee59f386d64c82610b696a4a74eb" integrity sha512-qerSRB0p+UDEssxTtm6EDKcE7W4OaoisfIMl4CngyEhjpYglocpNg6UEqCvemdGhosAsg4sO2dXJOdyBifPGCg== +eventemitter3@^4.0.4: + version "4.0.7" + resolved "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz#2de9b68f6528d5644ef5c59526a1b4a07306169f" + integrity sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw== + events@1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -12481,6 +12557,21 @@ execa@^4.0.0, execa@^4.0.1: signal-exit "^3.0.2" strip-final-newline "^2.0.0" +execa@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/execa/-/execa-5.0.0.tgz#4029b0007998a841fbd1032e5f4de86a3c1e3376" + integrity sha512-ov6w/2LCiuyO4RLYGdpFGjkcs0wMTgGE8PrkTHikeUy5iJekXyPIKUjifk5CsE0pt7sMCrMZ3YNqoCj6idQOnQ== + dependencies: + cross-spawn "^7.0.3" + get-stream "^6.0.0" + human-signals "^2.1.0" + is-stream "^2.0.0" + merge-stream "^2.0.0" + npm-run-path "^4.0.1" + onetime "^5.1.2" + signal-exit "^3.0.3" + strip-final-newline "^2.0.0" + executable@^4.1.1: version "4.1.1" resolved "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz#41532bff361d3e57af4d763b70582db18f5d133c" @@ -12845,7 +12936,7 @@ fetch-readablestream@^0.2.0: resolved "https://registry.npmjs.org/fetch-readablestream/-/fetch-readablestream-0.2.0.tgz#eaa6d1a76b12de2d4731a343393c6ccdcfe2c795" integrity sha512-qu4mXWf4wus4idBIN/kVH+XSer8IZ9CwHP+Pd7DL7TuKNC1hP7ykon4kkBjwJF3EMX2WsFp4hH7gU7CyL7ucXw== -figgy-pudding@^3.4.1, figgy-pudding@^3.5.1: +figgy-pudding@^3.5.1: version "3.5.1" resolved "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz#862470112901c727a0e495a80744bd5baa1d6790" integrity sha512-vNKxJHTEKNThjfrdJwHc7brvM6eVevuO5nTj6ez8ZQ1qbXTvGthucRF7S4vf2cr71QVnT70V34v0S1DyQsti0w== @@ -13266,6 +13357,16 @@ fs-extra@^7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + fs-minipass@^1.2.5: version "1.2.7" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-1.2.7.tgz#ccff8570841e7fe4265693da88936c55aed7f7c7" @@ -13273,7 +13374,7 @@ fs-minipass@^1.2.5: dependencies: minipass "^2.6.0" -fs-minipass@^2.0.0: +fs-minipass@^2.0.0, fs-minipass@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz#7f5036fdbf12c63c169190cbe4199c852271f9fb" integrity sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg== @@ -13416,11 +13517,6 @@ generic-pool@2.4.3: resolved "https://registry.npmjs.org/generic-pool/-/generic-pool-2.4.3.tgz#780c36f69dfad05a5a045dd37be7adca11a4f6ff" integrity sha1-eAw29p360FpaBF3Te+etyhGk9v8= -genfun@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/genfun/-/genfun-5.0.0.tgz#9dd9710a06900a5c4a5bf57aca5da4e52fe76537" - integrity sha512-KGDOARWVga7+rnB3z9Sd2Letx515owfk0hSxHGuqjANb1M+x2bGZGqHLiozPsYMdM2OubeMni/Hpwmjq6qIUhA== - gensync@^1.0.0-beta.1: version "1.0.0-beta.1" resolved "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.1.tgz#58f4361ff987e5ff6e1e7a210827aa371eaac269" @@ -13469,11 +13565,6 @@ get-pkg-repo@^1.0.0: parse-github-repo-url "^1.3.0" through2 "^2.0.0" -get-port@^4.2.0: - version "4.2.0" - resolved "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz#e37368b1e863b7629c43c5a323625f95cf24b119" - integrity sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw== - get-port@^5.1.1: version "5.1.1" resolved "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz#0469ed07563479de6efb986baf053dcd7d4e3193" @@ -13537,16 +13628,16 @@ getpass@^0.1.1: dependencies: assert-plus "^1.0.0" -git-raw-commits@2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.0.tgz#d92addf74440c14bcc5c83ecce3fb7f8a79118b5" - integrity sha512-w4jFEJFgKXMQJ0H0ikBk2S+4KP2VEjhCvLCNqbNRQC8BgGWgLKNCO7a9K9LI+TVT7Gfoloje502sEnctibffgg== +git-raw-commits@^2.0.8: + version "2.0.10" + resolved "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-2.0.10.tgz#e2255ed9563b1c9c3ea6bd05806410290297bbc1" + integrity sha512-sHhX5lsbG9SOO6yXdlwgEMQ/ljIn7qMpAbJZCGfXX2fq5T8M5SrDnpYk9/4HswTildcIqatsWa91vty6VhWSaQ== dependencies: - dargs "^4.0.1" - lodash.template "^4.0.2" - meow "^4.0.0" - split2 "^2.0.0" - through2 "^2.0.0" + dargs "^7.0.0" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^3.0.0" + through2 "^4.0.0" git-remote-origin-url@^2.0.0: version "2.0.0" @@ -13556,12 +13647,12 @@ git-remote-origin-url@^2.0.0: gitconfiglocal "^1.0.0" pify "^2.3.0" -git-semver-tags@^2.0.3: - version "2.0.3" - resolved "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-2.0.3.tgz#48988a718acf593800f99622a952a77c405bfa34" - integrity sha512-tj4FD4ww2RX2ae//jSrXZzrocla9db5h0V7ikPl1P/WwoZar9epdUhwR7XHXSgc+ZkNq72BEEerqQuicoEQfzA== +git-semver-tags@^4.1.1: + version "4.1.1" + resolved "https://registry.npmjs.org/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" + integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== dependencies: - meow "^4.0.0" + meow "^8.0.0" semver "^6.0.0" git-up@^4.0.0: @@ -13572,13 +13663,6 @@ git-up@^4.0.0: is-ssh "^1.3.0" parse-url "^5.0.0" -git-url-parse@^11.1.2: - version "11.1.3" - resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.1.3.tgz#03625b6fc09905e9ad1da7bb2b84be1bf9123143" - integrity sha512-GPsfwticcu52WQ+eHp0IYkAyaOASgYdtsQDIt4rUp6GbiNt1P9ddrh3O0kQB0eD4UJZszVqNT3+9Zwcg40fywA== - dependencies: - git-up "^4.0.0" - git-url-parse@^11.4.4: version "11.4.4" resolved "https://registry.npmjs.org/git-url-parse/-/git-url-parse-11.4.4.tgz#5d747debc2469c17bc385719f7d0427802d83d77" @@ -13628,7 +13712,7 @@ glob-parent@^3.1.0: is-glob "^3.1.0" path-dirname "^1.0.0" -glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@~5.1.0: +glob-parent@^5.0.0, glob-parent@^5.1.0, glob-parent@^5.1.1, glob-parent@~5.1.0: version "5.1.1" resolved "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.1.tgz#b6c1ef417c4e5663ea498f1c45afac6916bbc229" integrity sha512-FnI+VGOpnlGHWZxthPGR+QhR78fuiK0sNLkHQv+bL9fQi57lNNdquIbna/WrfROrolq8GK5Ek6BiMwqL/voRYQ== @@ -13740,7 +13824,7 @@ globalthis@^1.0.0: dependencies: define-properties "^1.1.3" -globby@11.0.1, globby@^11.0.0, globby@^11.0.1: +globby@11.0.1: version "11.0.1" resolved "https://registry.npmjs.org/globby/-/globby-11.0.1.tgz#9a2bf107a068f3ffeabc49ad702c79ede8cfd357" integrity sha512-iH9RmgwCmUJHi2z5o2l3eTtGBtXek1OYlHrbcxOYugyHLmAsZrPj43OtHThd62Buh/Vv6VyCBD2bdyWcGNQqoQ== @@ -13779,6 +13863,18 @@ globby@^10.0.1: merge2 "^1.2.3" slash "^3.0.0" +globby@^11.0.0, globby@^11.0.1, globby@^11.0.2: + version "11.0.2" + resolved "https://registry.npmjs.org/globby/-/globby-11.0.2.tgz#1af538b766a3b540ebfb58a32b2e2d5897321d83" + integrity sha512-2ZThXDvvV8fYFRVIxnrMQBipZQDr7MxKAmQK1vujaj9/7eF0efG7BPUKJ7jP7G5SLF37xKDXvO4S/KKLj/Z0og== + dependencies: + array-union "^2.1.0" + dir-glob "^3.0.1" + fast-glob "^3.1.1" + ignore "^5.1.4" + merge2 "^1.3.0" + slash "^3.0.0" + globby@^6.1.0: version "6.1.0" resolved "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz#f5a6d70e8395e21c858fb0489d64df02424d506c" @@ -13901,6 +13997,11 @@ graceful-fs@^4.1.11, graceful-fs@^4.1.15, graceful-fs@^4.1.2, graceful-fs@^4.1.5 resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz#2256bde14d3632958c465ebc96dc467ca07a29fb" integrity sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw== +graceful-fs@^4.2.3: + version "4.2.6" + resolved "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz#ff040b2b0853b23c3d31027523706f1885d76bee" + integrity sha512-nTnJ528pbqxYanhpDYsi4Rd8MAeaBA67+RZ10CM1m3bTAVFEDcd5AuA4a6W5YkGZ1iNXHzZz8T6TBKLeBuNriQ== + grapheme-splitter@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/grapheme-splitter/-/grapheme-splitter-1.0.4.tgz#9cf3a665c6247479896834af35cf1dbb4400767e" @@ -14100,7 +14201,7 @@ handle-thing@^2.0.0: resolved "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.0.tgz#0e039695ff50c93fc288557d696f3c1dc6776754" integrity sha512-d4sze1JNC454Wdo2fkuyzCr6aHcbL6PGGuFAz0Li/NcOm1tCHGnWDRmJP85dh9IhQErTc2svWFEX5xHIOo//kQ== -handlebars@^4.4.0, handlebars@^4.7.3: +handlebars@^4.7.3, handlebars@^4.7.6: version "4.7.6" resolved "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz#d4c05c1baf90e9945f77aa68a7a219aa4a7df74e" integrity sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA== @@ -14329,11 +14430,18 @@ hoopy@^0.1.4: resolved "https://registry.npmjs.org/hoopy/-/hoopy-0.1.4.tgz#609207d661100033a9a9402ad3dea677381c1b1d" integrity sha512-HRcs+2mr52W0K+x8RzcLzuPPmVIKMSv97RGHy0Ea9y/mpcaK+xTrjICA04KAHi4GRzxliNqNJEFYWHghy3rSfQ== -hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: +hosted-git-info@^2.1.4: version "2.8.8" resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== +hosted-git-info@^3.0.6: + version "3.0.8" + resolved "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-3.0.8.tgz#6e35d4cc87af2c5f816e4cb9ce350ba87a3f370d" + integrity sha512-aXpmwoOhRBrw6X3j0h5RloK4x1OzsxMPyxqIHyNfSe2pypkVTZFpEiRoSipPEPlMrh0HW/XsjkJ5WgnCirpNUw== + dependencies: + lru-cache "^6.0.0" + hpack.js@^2.1.6: version "2.1.6" resolved "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz#87774c0949e513f42e84575b3c45681fade2a0b2" @@ -14445,7 +14553,7 @@ htmlparser2@^3.3.0: inherits "^2.0.1" readable-stream "^3.1.1" -htmlparser2@^4.0, htmlparser2@^4.1.0: +htmlparser2@^4.0: version "4.1.0" resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz#9a4ef161f2e4625ebf7dfbe6c0a2f52d18a59e78" integrity sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q== @@ -14455,12 +14563,17 @@ htmlparser2@^4.0, htmlparser2@^4.1.0: domutils "^2.0.0" entities "^2.0.0" -http-cache-semantics@^3.8.1: - version "3.8.1" - resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz#39b0e16add9b605bf0a9ef3d9daaf4843b4cacd2" - integrity sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w== +htmlparser2@^6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.0.0.tgz#c2da005030390908ca4c91e5629e418e0665ac01" + integrity sha512-numTQtDZMoh78zJpaNdJ9MXb2cv5G3jwUoe3dMQODubZvLoGvTE/Ofp6sHvH8OGKcN/8A47pGLi/k58xHP/Tfw== + dependencies: + domelementtype "^2.0.1" + domhandler "^4.0.0" + domutils "^2.4.4" + entities "^2.0.0" -http-cache-semantics@^4.0.0: +http-cache-semantics@^4.0.0, http-cache-semantics@^4.1.0: version "4.1.0" resolved "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.0.tgz#49e91c5cbf36c9b94bcfcd71c23d5249ec74e390" integrity sha512-carPklcUh7ROWRK7Cv27RPtdhYhUsela/ue5/jKzjegVvXDqM2ILE9Q2BGn9JZJh1g87cp56su/FgQSzcWS8cQ== @@ -14518,14 +14631,6 @@ http-errors@~1.7.2: resolved "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.4.10.tgz#92c9c1374c35085f75db359ec56cc257cbb93fa4" integrity sha1-ksnBN0w1CF912zWexWzCV8u5P6Q= -http-proxy-agent@^2.1.0: - version "2.1.0" - resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-2.1.0.tgz#e4821beef5b2142a2026bd73926fe537631c5405" - integrity sha512-qwHbBLV7WviBl0rQsOzH6o5lwyOIvwp/BdFnvVxXORldu5TmjFfjzBcWUWS5kWAZhmv+JtiDhSuQCp4sBfbIgg== - dependencies: - agent-base "4" - debug "3.1.0" - http-proxy-agent@^4.0.0, http-proxy-agent@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz#8a8c8ef7f5932ccf953c296ca8291b95aa74aa3a" @@ -14586,14 +14691,6 @@ 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.3: - 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" @@ -14612,6 +14709,11 @@ human-signals@^1.1.1: resolved "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3" integrity sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw== +human-signals@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" + integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -14640,13 +14742,20 @@ hyphenate-style-name@^1.0.2, hyphenate-style-name@^1.0.3: resolved "https://registry.npmjs.org/hyphenate-style-name/-/hyphenate-style-name-1.0.3.tgz#097bb7fa0b8f1a9cf0bd5c734cf95899981a9b48" integrity sha512-EcuixamT82oplpoJ2XU4pDtKGWQ7b00CD9f1ug9IaQ3p1bkHMiKCZ9ut9QDI6qsa6cpUuB+A/I+zLtdNK4n2DQ== -iconv-lite@0.4.24, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4, iconv-lite@~0.4.13: +iconv-lite@0.4.24, iconv-lite@^0.4.21, iconv-lite@^0.4.24, iconv-lite@^0.4.4: version "0.4.24" resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA== dependencies: safer-buffer ">= 2.1.2 < 3" +iconv-lite@^0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz#ce13d1875b0c3a674bd6a04b7f76b01b1b6ded01" + integrity sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ== + dependencies: + safer-buffer ">= 2.1.2 < 3.0.0" + icss-replace-symbols@1.1.0, icss-replace-symbols@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/icss-replace-symbols/-/icss-replace-symbols-1.1.0.tgz#06ea6f83679a7749e386cfe1fe812ae5db223ded" @@ -14686,7 +14795,7 @@ ignore-by-default@^1.0.1: resolved "https://registry.npmjs.org/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09" integrity sha1-SMptcvbGo68Aqa1K5odr44ieKwk= -ignore-walk@^3.0.1: +ignore-walk@^3.0.1, ignore-walk@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/ignore-walk/-/ignore-walk-3.0.3.tgz#017e2447184bfeade7c238e4aefdd1e8f95b1e37" integrity sha512-m7o6xuOaT1aqheYHKf8W6J5pYH85ZI9w077erOzLje3JsB1gkafkAhHHY19dqjulgIZHFm32Cp5uNZgcQqdJKw== @@ -14853,18 +14962,18 @@ ini@^1.3.2, ini@^1.3.4, ini@^1.3.5, ini@~1.3.0: resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.8.tgz#a29da425b48806f34767a4efce397269af28432c" integrity sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew== -init-package-json@^1.10.3: - version "1.10.3" - resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-1.10.3.tgz#45ffe2f610a8ca134f2bd1db5637b235070f6cbe" - integrity sha512-zKSiXKhQveNteyhcj1CoOP8tqp1QuxPIPBl8Bid99DGLFqA1p87M6lNgfjJHSBoWJJlidGOv5rWjyYKEB3g2Jw== +init-package-json@^2.0.2: + version "2.0.2" + resolved "https://registry.npmjs.org/init-package-json/-/init-package-json-2.0.2.tgz#d81a7e6775af9b618f20bba288e440b8d1ce05f3" + integrity sha512-PO64kVeArePvhX7Ff0jVWkpnE1DfGRvaWcStYrPugcJz9twQGYibagKJuIMHCX7ENcp0M6LJlcjLBuLD5KeJMg== dependencies: glob "^7.1.1" - npm-package-arg "^4.0.0 || ^5.0.0 || ^6.0.0" + npm-package-arg "^8.1.0" promzard "^0.3.0" read "~1.0.1" - read-package-json "1 || 2" - semver "2.x || 3.x || 4 || 5" - validate-npm-package-license "^3.0.1" + read-package-json "^3.0.0" + semver "^7.3.2" + validate-npm-package-license "^3.0.4" validate-npm-package-name "^3.0.0" inline-style-prefixer@^4.0.0: @@ -14894,25 +15003,6 @@ inquirer@7.0.4: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^6.2.0: - version "6.5.2" - resolved "https://registry.npmjs.org/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" - integrity sha512-cntlB5ghuB0iuO65Ovoi8ogLHiWGs/5yNrtUcKjFhSSiVeAIVpD7koaSU9RM8mpXw5YDi9RdYXGQMaOURB7ycQ== - dependencies: - ansi-escapes "^3.2.0" - chalk "^2.4.2" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^3.0.3" - figures "^2.0.0" - lodash "^4.17.12" - mute-stream "0.0.7" - run-async "^2.2.0" - rxjs "^6.4.0" - string-width "^2.1.0" - strip-ansi "^5.1.0" - through "^2.3.6" - inquirer@^7.0.0, inquirer@^7.0.4, inquirer@^7.3.3: version "7.3.3" resolved "https://registry.npmjs.org/inquirer/-/inquirer-7.3.3.tgz#04d176b2af04afc157a83fd7c100e98ee0aad003" @@ -14976,7 +15066,7 @@ ip-regex@^2.1.0: resolved "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz#fa78bf5d2e6913c911ce9f819ee5146bb6d844e9" integrity sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk= -ip@1.1.5, ip@^1.1.0, ip@^1.1.5: +ip@^1.1.0, ip@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz#bdded70114290828c0a039e72ef25f5aaec4354a" integrity sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo= @@ -15264,6 +15354,11 @@ is-interactive@^1.0.0: resolved "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz#cea6e6ae5c870a7b0a0004070b7b587e0252912e" integrity sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w== +is-lambda@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz#3d9877899e6a53efc0160504cde15f82e6f061d5" + integrity sha1-PZh3iZ5qU+/AFgUEzeFfgubwYdU= + is-map@^2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/is-map/-/is-map-2.0.1.tgz#520dafc4307bb8ebc33b813de5ce7c9400d644a1" @@ -15296,7 +15391,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.0, is-obj@^1.0.1: +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= @@ -16302,11 +16397,16 @@ json-buffer@3.0.1: resolved "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz#9338802a30d3b6605fbe0613e094008ca8c05a13" integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== -json-parse-better-errors@^1.0.0, json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: +json-parse-better-errors@^1.0.1, json-parse-better-errors@^1.0.2: version "1.0.2" resolved "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz#bb867cfb3450e69107c131d1c514bab3dc8bcaa9" integrity sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw== +json-parse-even-better-errors@^2.3.0: + version "2.3.1" + resolved "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz#7c47805a94319928e05777405dc12e1f7a4ee02d" + integrity sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w== + json-pointer@^0.6.0: version "0.6.0" resolved "https://registry.npmjs.org/json-pointer/-/json-pointer-0.6.0.tgz#8e500550a6aac5464a473377da57aa6cc22828d7" @@ -16439,7 +16539,7 @@ jsonify@~0.0.0: resolved "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73" integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM= -jsonparse@^1.2.0: +jsonparse@^1.2.0, jsonparse@^1.3.1: version "1.3.1" resolved "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha1-P02uSpH6wxX3EGL4UhzCOfE2YoA= @@ -16707,6 +16807,11 @@ kleur@^3.0.3: resolved "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== +klona@^2.0.3: + version "2.0.4" + 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.8" resolved "https://registry.npmjs.org/knex/-/knex-0.21.8.tgz#e5c07af61ee6aa006d3468e10e3a69351deb0c26" @@ -16806,28 +16911,28 @@ left-pad@^1.3.0: resolved "https://registry.npmjs.org/left-pad/-/left-pad-1.3.0.tgz#5b8a3a7765dfe001261dde915589e782f8c94d1e" integrity sha512-XI5MPzVNApjAyhQzphX8BkmKsKUxD4LdyK24iZeQGinBN9yTQT3bFlCBy/aVx2HrNcqQGsdot8ghrjyrvMCoEA== -lerna@^3.20.2: - version "3.22.1" - resolved "https://registry.npmjs.org/lerna/-/lerna-3.22.1.tgz#82027ac3da9c627fd8bf02ccfeff806a98e65b62" - integrity sha512-vk1lfVRFm+UuEFA7wkLKeSF7Iz13W+N/vFd48aW2yuS7Kv0RbNm2/qcDPV863056LMfkRlsEe+QYOw3palj5Lg== +lerna@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/lerna/-/lerna-4.0.0.tgz#b139d685d50ea0ca1be87713a7c2f44a5b678e9e" + integrity sha512-DD/i1znurfOmNJb0OBw66NmNqiM8kF6uIrzrJ0wGE3VNdzeOhz9ziWLYiRaZDGGwgbcjOo6eIfcx9O5Qynz+kg== dependencies: - "@lerna/add" "3.21.0" - "@lerna/bootstrap" "3.21.0" - "@lerna/changed" "3.21.0" - "@lerna/clean" "3.21.0" - "@lerna/cli" "3.18.5" - "@lerna/create" "3.22.0" - "@lerna/diff" "3.21.0" - "@lerna/exec" "3.21.0" - "@lerna/import" "3.22.0" - "@lerna/info" "3.21.0" - "@lerna/init" "3.21.0" - "@lerna/link" "3.21.0" - "@lerna/list" "3.21.0" - "@lerna/publish" "3.22.1" - "@lerna/run" "3.21.0" - "@lerna/version" "3.22.1" - import-local "^2.0.0" + "@lerna/add" "4.0.0" + "@lerna/bootstrap" "4.0.0" + "@lerna/changed" "4.0.0" + "@lerna/clean" "4.0.0" + "@lerna/cli" "4.0.0" + "@lerna/create" "4.0.0" + "@lerna/diff" "4.0.0" + "@lerna/exec" "4.0.0" + "@lerna/import" "4.0.0" + "@lerna/info" "4.0.0" + "@lerna/init" "4.0.0" + "@lerna/link" "4.0.0" + "@lerna/list" "4.0.0" + "@lerna/publish" "4.0.0" + "@lerna/run" "4.0.0" + "@lerna/version" "4.0.0" + import-local "^3.0.2" npmlog "^4.1.2" leven@^3.1.0: @@ -16856,6 +16961,27 @@ li@^1.3.0: resolved "https://registry.npmjs.org/li/-/li-1.3.0.tgz#22c59bcaefaa9a8ef359cf759784e4bf106aea1b" integrity sha1-IsWbyu+qmo7zWc91l4TkvxBq6hs= +libnpmaccess@^4.0.1: + version "4.0.1" + resolved "https://registry.npmjs.org/libnpmaccess/-/libnpmaccess-4.0.1.tgz#17e842e03bef759854adf6eb6c2ede32e782639f" + integrity sha512-ZiAgvfUbvmkHoMTzdwmNWCrQRsDkOC+aM5BDfO0C9aOSwF3R1LdFDBD+Rer1KWtsoQYO35nXgmMR7OUHpDRxyA== + dependencies: + aproba "^2.0.0" + minipass "^3.1.1" + npm-package-arg "^8.0.0" + npm-registry-fetch "^9.0.0" + +libnpmpublish@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/libnpmpublish/-/libnpmpublish-4.0.0.tgz#ad6413914e0dfd78df868ce14ba3d3a4cc8b385b" + integrity sha512-2RwYXRfZAB1x/9udKpZmqEzSqNd7ouBRU52jyG14/xG8EF+O9A62d7/XVR3iABEQHf1iYhkm0Oq9iXjrL3tsXA== + dependencies: + normalize-package-data "^3.0.0" + npm-package-arg "^8.1.0" + npm-registry-fetch "^9.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" @@ -17004,16 +17130,15 @@ load-json-file@^4.0.0: pify "^3.0.0" strip-bom "^3.0.0" -load-json-file@^5.3.0: - version "5.3.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-5.3.0.tgz#4d3c1e01fa1c03ea78a60ac7af932c9ce53403f3" - integrity sha512-cJGP40Jc/VXUsp8/OrnyKyTZ1y6v/dphm3bioS+RrKXjK2BB6wHUd6JptZEFDGgGahMT+InnZO5i1Ei9mpC8Bw== +load-json-file@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-6.2.0.tgz#5c7770b42cafa97074ca2848707c61662f4251a1" + integrity sha512-gUD/epcRms75Cw8RT1pUdHugZYM5ce64ucs2GEISABwkRsOQr0q2wm/MV2TKThycIe5e0ytRweW2RZxclogCdQ== dependencies: graceful-fs "^4.1.15" - parse-json "^4.0.0" - pify "^4.0.1" - strip-bom "^3.0.0" - type-fest "^0.3.0" + parse-json "^5.0.0" + strip-bom "^4.0.0" + type-fest "^0.6.0" load-yaml-file@^0.2.0: version "0.2.0" @@ -17137,11 +17262,6 @@ lodash.flattendeep@^4.0.0: resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= -lodash.get@^4.4.2: - version "4.4.2" - resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" - integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= - lodash.includes@^4.3.0: version "4.3.0" resolved "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz#60bb98a87cb923c68ca1e51325483314849f553f" @@ -17202,7 +17322,7 @@ lodash.startcase@^4.4.0: resolved "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz#9436e34ed26093ed7ffae1936144350915d9add8" integrity sha1-lDbjTtJgk+1/+uGTYUQ1CRXZrdg= -lodash.template@^4.0.2, lodash.template@^4.5.0: +lodash.template@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.template/-/lodash.template-4.5.0.tgz#f976195cf3f347d0d5f52483569fe8031ccce8ab" integrity sha512-84vYFxIkmidUiFxidA/KjjH9pAycqW+h980j7Fuz5qxRtO9pgB7MDFTdys1N7A5mcucRiDyEq4fusljItR1T/A== @@ -17242,7 +17362,7 @@ lodash@4.17.15: resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548" integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== -lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.20, lodash@~4.17.4: +lodash@^4.0.1, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.19, lodash@^4.17.20, lodash@^4.17.4, lodash@^4.17.5, lodash@^4.2.1, lodash@~4.17.20, lodash@~4.17.4: version "4.17.20" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52" integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA== @@ -17407,13 +17527,6 @@ 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" @@ -17434,22 +17547,26 @@ make-error@1.x, make-error@^1.1.1, make-error@^1.3.6: resolved "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== -make-fetch-happen@^5.0.0: - version "5.0.2" - resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-5.0.2.tgz#aa8387104f2687edca01c8687ee45013d02d19bd" - integrity sha512-07JHC0r1ykIoruKO8ifMXu+xEU8qOXDFETylktdug6vJDACnP+HKevOu3PXyNPzFyTSlz8vrBYlBO1JZRe8Cag== +make-fetch-happen@^8.0.9: + version "8.0.14" + resolved "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-8.0.14.tgz#aaba73ae0ab5586ad8eaa68bd83332669393e222" + integrity sha512-EsS89h6l4vbfJEtBZnENTOFk8mCRpY5ru36Xe5bcX1KYIli2mkSHqoFsp5O1wMDvTJJzxe/4THpCTtygjeeGWQ== dependencies: - agentkeepalive "^3.4.1" - cacache "^12.0.0" - http-cache-semantics "^3.8.1" - http-proxy-agent "^2.1.0" - https-proxy-agent "^2.2.3" - lru-cache "^5.1.1" - mississippi "^3.0.0" - node-fetch-npm "^2.0.2" - promise-retry "^1.1.1" - socks-proxy-agent "^4.0.0" - ssri "^6.0.0" + agentkeepalive "^4.1.3" + cacache "^15.0.5" + http-cache-semantics "^4.1.0" + http-proxy-agent "^4.0.1" + https-proxy-agent "^5.0.0" + is-lambda "^1.0.1" + lru-cache "^6.0.0" + minipass "^3.1.3" + minipass-collect "^1.0.2" + minipass-fetch "^1.3.2" + minipass-flush "^1.0.5" + minipass-pipeline "^1.2.4" + promise-retry "^2.0.1" + socks-proxy-agent "^5.0.0" + ssri "^8.0.0" make-iterator@^1.0.0: version "1.0.1" @@ -17475,11 +17592,6 @@ map-obj@^1.0.0, map-obj@^1.0.1: resolved "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz#d933ceb9205d82bdcf4886f6742bdc2b4dea146d" integrity sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0= -map-obj@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/map-obj/-/map-obj-2.0.0.tgz#a65cd29087a92598b8791257a523e021222ac1f9" - integrity sha1-plzSkIepJZi4eRJXpSPgISIqwfk= - map-obj@^4.0.0: version "4.1.0" resolved "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz#b91221b542734b9f14256c0132c897c5d7256fd5" @@ -17725,36 +17837,6 @@ meow@^3.3.0: redent "^1.0.0" trim-newlines "^1.0.0" -meow@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/meow/-/meow-4.0.1.tgz#d48598f6f4b1472f35bf6317a95945ace347f975" - integrity sha512-xcSBHD5Z86zaOc+781KrupuHAzeGXSLtiAOmBsiLDiPSaYSB6hdew2ng9EBAnZ62jagG9MHAOdxpDi/lWBFJ/A== - dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist "^1.1.3" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - -meow@^5.0.0: - version "5.0.0" - resolved "https://registry.npmjs.org/meow/-/meow-5.0.0.tgz#dfc73d63a9afc714a5e371760eb5c88b91078aa4" - integrity sha512-CbTqYU17ABaLefO8vCU153ZZlprKYWDljcndKKDCFcYQITzWCXZAVk4QMFZPgvzrnUQ3uItnIE/LoUOwrT15Ig== - dependencies: - camelcase-keys "^4.0.0" - decamelize-keys "^1.0.0" - loud-rejection "^1.0.0" - minimist-options "^3.0.1" - normalize-package-data "^2.3.4" - read-pkg-up "^3.0.0" - redent "^2.0.0" - trim-newlines "^2.0.0" - yargs-parser "^10.0.0" - meow@^6.0.0: version "6.1.1" resolved "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz#1ad64c4b76b2a24dfb2f635fddcadf320d251467" @@ -17772,6 +17854,23 @@ meow@^6.0.0: type-fest "^0.13.1" yargs-parser "^18.1.3" +meow@^8.0.0: + version "8.1.2" + resolved "https://registry.npmjs.org/meow/-/meow-8.1.2.tgz#bcbe45bda0ee1729d350c03cffc8395a36c4e897" + integrity sha512-r85E3NdZ+mpYk1C6RjPFEMSE+s1iZMuHtsHAqY0DT3jZczl0diWUZ8g6oU7h0M9cD2EL+PzaYghhCLzR0ZNn5Q== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + merge-deep@^3.0.2: version "3.0.2" resolved "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.2.tgz#f39fa100a4f1bd34ff29f7d2bf4508fbb8d83ad2" @@ -17993,15 +18092,7 @@ minimalistic-crypto-utils@^1.0.0, minimalistic-crypto-utils@^1.0.1: dependencies: brace-expansion "^1.1.7" -minimist-options@^3.0.1: - version "3.0.2" - resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-3.0.2.tgz#fba4c8191339e13ecf4d61beb03f070103f3d954" - integrity sha512-FyBrT/d0d4+uiZRbqznPXqw3IpZZG3gl3wKWiX784FycUKVwBt0uLBFkQrtE4tZOrgo78nZp2jnKz3L65T5LdQ== - dependencies: - arrify "^1.0.1" - is-plain-obj "^1.1.0" - -minimist-options@^4.0.2: +minimist-options@4.1.0, minimist-options@^4.0.2: version "4.1.0" resolved "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz#c0655713c53a8a2ebd77ffa247d342c40f010619" integrity sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A== @@ -18029,6 +18120,17 @@ minipass-collect@^1.0.2: dependencies: minipass "^3.0.0" +minipass-fetch@^1.3.0, minipass-fetch@^1.3.2: + version "1.3.3" + resolved "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-1.3.3.tgz#34c7cea038c817a8658461bf35174551dce17a0a" + integrity sha512-akCrLDWfbdAWkMLBxJEeWTdNsjML+dt5YgOI4gJ53vuO0vrmYQkUPxa6j6V65s9CcePIr2SSWqjT2EcrNseryQ== + dependencies: + minipass "^3.1.0" + minipass-sized "^1.0.3" + minizlib "^2.0.0" + optionalDependencies: + encoding "^0.1.12" + minipass-flush@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz#82e7135d7e89a50ffe64610a787953c4c4cbb373" @@ -18036,6 +18138,14 @@ minipass-flush@^1.0.5: dependencies: minipass "^3.0.0" +minipass-json-stream@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/minipass-json-stream/-/minipass-json-stream-1.0.1.tgz#7edbb92588fbfc2ff1db2fc10397acb7b6b44aa7" + integrity sha512-ODqY18UZt/I8k+b7rl2AENgbWE8IDYam+undIJONvigAz8KR5GWblsFTEfQs0WODsjbSXWlm+JHEv8Gr6Tfdbg== + dependencies: + jsonparse "^1.3.1" + minipass "^3.0.0" + minipass-pipeline@^1.2.2: version "1.2.2" resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.2.tgz#3dcb6bb4a546e32969c7ad710f2c79a86abba93a" @@ -18043,7 +18153,21 @@ minipass-pipeline@^1.2.2: dependencies: minipass "^3.0.0" -minipass@^2.3.5, minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: +minipass-pipeline@^1.2.4: + version "1.2.4" + resolved "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz#68472f79711c084657c067c5c6ad93cddea8214c" + integrity sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A== + dependencies: + minipass "^3.0.0" + +minipass-sized@^1.0.3: + version "1.0.3" + resolved "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz#70ee5a7c5052070afacfbc22977ea79def353b70" + integrity sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g== + dependencies: + minipass "^3.0.0" + +minipass@^2.6.0, minipass@^2.8.6, minipass@^2.9.0: version "2.9.0" resolved "https://registry.npmjs.org/minipass/-/minipass-2.9.0.tgz#e713762e7d3e32fed803115cf93e04bca9fcc9a6" integrity sha512-wxfUjg9WebH+CUDX/CdbRlh5SmfZiy/hpkxaRI16Y9W56Pa75sWgd/rvFilSgrauD9NyFymP/+JFV3KwzIsJeg== @@ -18058,6 +18182,13 @@ minipass@^3.0.0, minipass@^3.1.1: dependencies: yallist "^4.0.0" +minipass@^3.1.0, minipass@^3.1.3: + version "3.1.3" + resolved "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz#7d42ff1f39635482e15f9cdb53184deebd5815fd" + integrity sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg== + dependencies: + yallist "^4.0.0" + minizlib@^1.2.1: version "1.3.3" resolved "https://registry.npmjs.org/minizlib/-/minizlib-1.3.3.tgz#2290de96818a34c29551c8a8d301216bd65a861d" @@ -18065,7 +18196,7 @@ minizlib@^1.2.1: dependencies: minipass "^2.9.0" -minizlib@^2.1.1: +minizlib@^2.0.0, minizlib@^2.1.1: version "2.1.2" resolved "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz#e90d3466ba209b932451508a11ce3d3632145931" integrity sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg== @@ -18120,14 +18251,16 @@ mkdirp-classic@^0.5.2: resolved "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.2.tgz#54c441ce4c96cd7790e10b41a87aa51068ecab2b" integrity sha512-ejdnDQcR75gwknmMw/tx02AuRs8jCtqFoFqDZMjiNxsu85sRIJVXDKHuLYvUUPRBUtV2FpSZa9bL1BUa3BdR2g== -mkdirp-promise@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/mkdirp-promise/-/mkdirp-promise-5.0.1.tgz#e9b8f68e552c68a9c1713b84883f7a1dd039b8a1" - integrity sha1-6bj2jlUsaKnBcTuEiD96HdA5uKE= +mkdirp-infer-owner@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/mkdirp-infer-owner/-/mkdirp-infer-owner-2.0.0.tgz#55d3b368e7d89065c38f32fd38e638f0ab61d316" + integrity sha512-sdqtiFt3lkOaYvTXSRIUjkIdPTcxgv5+fgqYE/5qgwdw12cOrAuzzgzvVExIkH/ul1oeHN3bCLOWSG3XOqbKKw== dependencies: - mkdirp "*" + chownr "^2.0.0" + infer-owner "^1.0.4" + mkdirp "^1.0.3" -mkdirp@*, mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: +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" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -18279,21 +18412,17 @@ multicast-dns@^6.0.1: dns-packet "^1.3.1" thunky "^1.0.2" -multimatch@^3.0.0: - version "3.0.0" - resolved "https://registry.npmjs.org/multimatch/-/multimatch-3.0.0.tgz#0e2534cc6bc238d9ab67e1b9cd5fcd85a6dbf70b" - integrity sha512-22foS/gqQfANZ3o+W7ST2x25ueHDVNWl/b9OlGcLpy/iKxjCpvcNCM51YCenUi7Mt/jAjjqv8JwZRs8YP5sRjA== +multimatch@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/multimatch/-/multimatch-5.0.0.tgz#932b800963cea7a31a033328fa1e0c3a1874dbe6" + integrity sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA== dependencies: - array-differ "^2.0.3" - array-union "^1.0.2" - arrify "^1.0.1" + "@types/minimatch" "^3.0.3" + array-differ "^3.0.0" + array-union "^2.1.0" + arrify "^2.0.1" minimatch "^3.0.4" -mute-stream@0.0.7: - version "0.0.7" - resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" - integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= - mute-stream@0.0.8, mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" @@ -18308,7 +18437,7 @@ mv@~2: ncp "~2.0.0" rimraf "~2.4.0" -mz@^2.5.0, mz@^2.7.0: +mz@^2.7.0: version "2.7.0" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q== @@ -18341,6 +18470,11 @@ nanoid@^2.1.0: resolved "https://registry.npmjs.org/nanoid/-/nanoid-2.1.11.tgz#ec24b8a758d591561531b4176a01e3ab4f0f0280" integrity sha512-s/snB+WGm6uwi0WjsZdaVcuf3KJXlfGl2LcxgwkEwJF0D/BWzVWAZW/XY4bFaiR7s0Jk3FPvlnepg1H1b1UwlA== +nanoid@^3.1.20: + version "3.1.20" + resolved "https://registry.npmjs.org/nanoid/-/nanoid-3.1.20.tgz#badc263c6b1dcf14b71efaa85f6ab4c1d6cfc788" + integrity sha512-a1cQNyczgKbLX9jwbS/+d7W8fX/RfgYR7lVWwWOGIPNgK2m0MWvrGF6/m4kk6U3QcFMnZf3RIhL0v2Jgh/0Uxw== + nanomatch@^1.2.9: version "1.2.13" resolved "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz#b87a8aa4fc0de8fe6be88895b38983ff265bd119" @@ -18463,16 +18597,7 @@ node-dir@^0.1.10: dependencies: minimatch "^3.0.2" -node-fetch-npm@^2.0.2: - version "2.0.3" - resolved "https://registry.npmjs.org/node-fetch-npm/-/node-fetch-npm-2.0.3.tgz#efae4aacb0500444e449a51fc1467397775ebc38" - integrity sha512-DgwoKEsqLnFZtk3ap7GWBHcHwnUhsNmQqEDcdjfQ8GofLEFJ081NAd4Uin3R7RFZBWVJCwHISw1oaEqPgSLloA== - dependencies: - encoding "^0.1.11" - json-parse-better-errors "^1.0.0" - safe-buffer "^5.1.1" - -node-fetch@2.6.1, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.5.0, node-fetch@^2.6.0, node-fetch@^2.6.1: +node-fetch@2.6.1, node-fetch@^2.1.2, node-fetch@^2.2.0, node-fetch@^2.3.0, node-fetch@^2.6.0, node-fetch@^2.6.1: version "2.6.1" resolved "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052" integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw== @@ -18522,6 +18647,22 @@ node-gyp@^5.0.2: tar "^4.4.12" which "^1.3.1" +node-gyp@^7.1.0: + version "7.1.2" + resolved "https://registry.npmjs.org/node-gyp/-/node-gyp-7.1.2.tgz#21a810aebb187120251c3bcec979af1587b188ae" + integrity sha512-CbpcIo7C3eMu3dL1c3d0xw449fHIGALIJsRP4DDPHpyiW8vcriNY7ubh9TE4zEKfSxscY7PjeFnshE7h75ynjQ== + dependencies: + env-paths "^2.2.0" + glob "^7.1.4" + graceful-fs "^4.2.3" + nopt "^5.0.0" + npmlog "^4.1.2" + request "^2.88.2" + rimraf "^3.0.2" + semver "^7.3.2" + tar "^6.0.2" + which "^2.0.2" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -18658,6 +18799,13 @@ nopt@^4.0.1: abbrev "1" osenv "^0.1.4" +nopt@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz#530942bb58a512fccafe53fe210f13a25355dc88" + integrity sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ== + dependencies: + abbrev "1" + nopt@~1.0.10: version "1.0.10" resolved "https://registry.npmjs.org/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -18665,7 +18813,7 @@ nopt@~1.0.10: dependencies: abbrev "1" -normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.3.5, normalize-package-data@^2.4.0, normalize-package-data@^2.5.0: +normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package-data@^2.3.2, normalize-package-data@^2.3.4, normalize-package-data@^2.5.0: version "2.5.0" resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz#e66db1838b200c1dfc233225d12cb36520e234a8" integrity sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA== @@ -18675,6 +18823,16 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-package-data@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.0.tgz#1f8a7c423b3d2e85eb36985eaf81de381d01301a" + integrity sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw== + dependencies: + hosted-git-info "^3.0.6" + resolve "^1.17.0" + semver "^7.3.2" + validate-npm-package-license "^3.0.1" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -18712,17 +18870,24 @@ normalize-url@^4.1.0: resolved "https://registry.npmjs.org/normalize-url/-/normalize-url-4.5.0.tgz#453354087e6ca96957bd8f5baf753f5982142129" integrity sha512-2s47yzUxdexf1OhyRi4Em83iQk0aPvwTddtFz4hnSSw9dCEsLEGf6SwIO8ss/19S9iBb5sJaOuTvTGDeZI00BQ== -npm-bundled@^1.0.1: +npm-bundled@^1.0.1, npm-bundled@^1.1.1: version "1.1.1" resolved "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.1.tgz#1edd570865a94cdb1bc8220775e29466c9fb234b" integrity sha512-gqkfgGePhTpAEgUsGEgcq1rqPXA+tv/aVBlgEzfXwA1yiUJF7xtEt3CtVwOjNYQOVknDk0F20w58Fnm3EtG0fA== dependencies: npm-normalize-package-bin "^1.0.1" -npm-lifecycle@^3.1.2: - version "3.1.4" - resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.4.tgz#de6975c7d8df65f5150db110b57cce498b0b604c" - integrity sha512-tgs1PaucZwkxECGKhC/stbEgFyc3TGh2TJcg2CDr6jbvQRdteHNhmMeljRzpe4wgFAXQADoy1cSqqi7mtiAa5A== +npm-install-checks@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-4.0.0.tgz#a37facc763a2fde0497ef2c6d0ac7c3fbe00d7b4" + integrity sha512-09OmyDkNLYwqKPOnbI8exiOZU2GVVmQp7tgez2BPi5OZC8M82elDAps7sxC4l//uSUtotWqoEIDwjRvWH4qz8w== + dependencies: + semver "^7.1.1" + +npm-lifecycle@^3.1.5: + version "3.1.5" + resolved "https://registry.npmjs.org/npm-lifecycle/-/npm-lifecycle-3.1.5.tgz#9882d3642b8c82c815782a12e6a1bfeed0026309" + integrity sha512-lDLVkjfZmvmfvpvBzA4vzee9cn+Me4orq0QF8glbswJVEbIcSNWib7qGOffolysc3teCqbbPZZkzbr3GQZTL1g== dependencies: byline "^5.0.0" graceful-fs "^4.1.15" @@ -18738,17 +18903,16 @@ npm-normalize-package-bin@^1.0.0, npm-normalize-package-bin@^1.0.1: resolved "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz#6e79a41f23fd235c0623218228da7d9c23b8f6e2" integrity sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA== -"npm-package-arg@^4.0.0 || ^5.0.0 || ^6.0.0", npm-package-arg@^6.0.0, npm-package-arg@^6.1.0: - version "6.1.1" - resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-6.1.1.tgz#02168cb0a49a2b75bf988a28698de7b529df5cb7" - integrity sha512-qBpssaL3IOZWi5vEKUKW0cO7kzLeT+EQO9W8RsLOZf76KF9E/K9+wH0C7t06HXPpaH8WH5xF1MExLuCwbTqRUg== +npm-package-arg@^8.0.0, npm-package-arg@^8.0.1, npm-package-arg@^8.1.0: + version "8.1.0" + resolved "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-8.1.0.tgz#b5f6319418c3246a1c38e1a8fbaa06231bc5308f" + integrity sha512-/ep6QDxBkm9HvOhOg0heitSd7JHA1U7y1qhhlRlteYYAi9Pdb/ZV7FW5aHpkrpM8+P+4p/jjR8zCyKPBMBjSig== dependencies: - hosted-git-info "^2.7.1" - osenv "^0.1.5" - semver "^5.6.0" + hosted-git-info "^3.0.6" + semver "^7.0.0" validate-npm-package-name "^3.0.0" -npm-packlist@^1.1.6, npm-packlist@^1.4.4: +npm-packlist@^1.1.6: version "1.4.8" resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-1.4.8.tgz#56ee6cc135b9f98ad3d51c1c95da22bbb9b2ef3e" integrity sha512-5+AZgwru5IevF5ZdnFglB5wNlHG1AOOuw28WhUq8/8emhBmLv6jX5by4WJCh7lW0uSYZYS6DXqIsyZVIXRZU9A== @@ -18757,14 +18921,38 @@ npm-packlist@^1.1.6, npm-packlist@^1.4.4: npm-bundled "^1.0.1" npm-normalize-package-bin "^1.0.1" -npm-pick-manifest@^3.0.0: - version "3.0.2" - resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-3.0.2.tgz#f4d9e5fd4be2153e5f4e5f9b7be8dc419a99abb7" - integrity sha512-wNprTNg+X5nf+tDi+hbjdHhM4bX+mKqv6XmPh7B5eG+QY9VARfQPfCEH013H5GqfNj6ee8Ij2fg8yk0mzps1Vw== +npm-packlist@^2.1.4: + version "2.1.4" + resolved "https://registry.npmjs.org/npm-packlist/-/npm-packlist-2.1.4.tgz#40e96b2b43787d0546a574542d01e066640d09da" + integrity sha512-Qzg2pvXC9U4I4fLnUrBmcIT4x0woLtUgxUi9eC+Zrcv1Xx5eamytGAfbDWQ67j7xOcQ2VW1I3su9smVTIdu7Hw== dependencies: - figgy-pudding "^3.5.1" - npm-package-arg "^6.0.0" - semver "^5.4.1" + glob "^7.1.6" + ignore-walk "^3.0.3" + npm-bundled "^1.1.1" + npm-normalize-package-bin "^1.0.1" + +npm-pick-manifest@^6.0.0: + version "6.1.0" + resolved "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-6.1.0.tgz#2befed87b0fce956790f62d32afb56d7539c022a" + integrity sha512-ygs4k6f54ZxJXrzT0x34NybRlLeZ4+6nECAIbr2i0foTnijtS1TJiyzpqtuUAJOps/hO0tNDr8fRV5g+BtRlTw== + dependencies: + npm-install-checks "^4.0.0" + npm-package-arg "^8.0.0" + semver "^7.0.0" + +npm-registry-fetch@^9.0.0: + version "9.0.0" + resolved "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-9.0.0.tgz#86f3feb4ce00313bc0b8f1f8f69daae6face1661" + integrity sha512-PuFYYtnQ8IyVl6ib9d3PepeehcUeHN9IO5N/iCRhyg9tStQcqGQBRVHmfmMWPDERU3KwZoHFvbJ4FPXPspvzbA== + dependencies: + "@npmcli/ci-detect" "^1.0.0" + lru-cache "^6.0.0" + make-fetch-happen "^8.0.9" + minipass "^3.1.3" + minipass-fetch "^1.3.0" + minipass-json-stream "^1.0.1" + minizlib "^2.0.0" + npm-package-arg "^8.0.0" npm-run-path@^2.0.0: version "2.0.2" @@ -18773,7 +18961,7 @@ npm-run-path@^2.0.0: dependencies: path-key "^2.0.0" -npm-run-path@^4.0.0: +npm-run-path@^4.0.0, npm-run-path@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz#b7ecd1e5ed53da8e37a55e1c2269e0b97ed748ea" integrity sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw== @@ -18975,11 +19163,6 @@ obuf@^1.0.0, obuf@^1.1.2: resolved "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz#09bea3343d41859ebd446292d11c9d4db619084e" integrity sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg== -octokit-pagination-methods@^1.1.0: - version "1.1.0" - resolved "https://registry.npmjs.org/octokit-pagination-methods/-/octokit-pagination-methods-1.1.0.tgz#cf472edc9d551055f9ef73f6e42b4dbb4c80bea4" - integrity sha512-fZ4qZdQ2nxJvtcasX7Ghl+WlWS/d9IgnBIwFZXVNNZUmzpno91SX5bc5vuxiuKoCtK78XxGGNuSCrDC7xYB3OQ== - oidc-token-hash@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/oidc-token-hash/-/oidc-token-hash-5.0.0.tgz#acdfb1f4310f58e64d5d74a4e8671a426986e888" @@ -19028,6 +19211,13 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" +onetime@^5.1.2: + version "5.1.2" + resolved "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz#d0e96ebb56b07476df1dd9c4806e5237985ca45e" + integrity sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg== + dependencies: + mimic-fn "^2.1.0" + open@^7.0.0: version "7.3.1" resolved "https://registry.npmjs.org/open/-/open-7.3.1.tgz#111119cb919ca1acd988f49685c4fdd0f4755356" @@ -19152,7 +19342,7 @@ os-tmpdir@^1.0.0, os-tmpdir@~1.0.2: resolved "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" integrity sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ= -osenv@0, osenv@^0.1.4, osenv@^0.1.5: +osenv@0, osenv@^0.1.4: version "0.1.5" resolved "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" integrity sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g== @@ -19285,14 +19475,12 @@ p-locate@^5.0.0: dependencies: p-limit "^3.0.2" -p-map-series@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz#bf98fe575705658a9e1351befb85ae4c1f07bdca" - integrity sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco= - dependencies: - p-reduce "^1.0.0" +p-map-series@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-map-series/-/p-map-series-2.1.0.tgz#7560d4c452d9da0c07e692fdbfe6e2c81a2a91f2" + integrity sha512-RpYIIK1zXSNEOdwxcfe7FdvGcs7+y5n8rifMhMNWvaxRNMPINJHF5GDeuVxWqnfrcHPSCnp7Oo5yNXHId9Av2Q== -p-map@^2.0.0, p-map@^2.1.0: +p-map@^2.0.0: version "2.1.0" resolved "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz#310928feef9c9ecc65b68b17693018a665cea175" integrity sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw== @@ -19311,17 +19499,10 @@ p-map@^4.0.0: dependencies: aggregate-error "^3.0.0" -p-pipe@^1.2.0: - version "1.2.0" - resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz#4b1a11399a11520a67790ee5a0c1d5881d6befe9" - integrity sha1-SxoROZoRUgpneQ7loMHViB1r7+k= - -p-queue@^4.0.0: - version "4.0.0" - resolved "https://registry.npmjs.org/p-queue/-/p-queue-4.0.0.tgz#ed0eee8798927ed6f2c2f5f5b77fdb2061a5d346" - integrity sha512-3cRXXn3/O0o3+eVmUroJPSj/esxoEFIm0ZOno/T+NzG/VZgPOqQ8WKmlNqubSEpZmCIngEy34unkHGg83ZIBmg== - dependencies: - eventemitter3 "^3.1.0" +p-pipe@^3.1.0: + version "3.1.0" + resolved "https://registry.npmjs.org/p-pipe/-/p-pipe-3.1.0.tgz#48b57c922aa2e1af6a6404cb7c6bf0eb9cc8e60e" + integrity sha512-08pj8ATpzMR0Y80x50yJHn37NF6vjrqHutASaX5LiH5npS9XPvrUmscd9MF5R4fuYRHOxQR1FfMIlF7AzwoPqw== p-queue@^6.3.0: version "6.4.0" @@ -19331,10 +19512,18 @@ p-queue@^6.3.0: eventemitter3 "^4.0.0" p-timeout "^3.1.0" -p-reduce@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz#18c2b0dd936a4690a529f8231f58a0fdb6a47dfa" - integrity sha1-GMKw3ZNqRpClKfgjH1ig/bakffo= +p-queue@^6.6.2: + version "6.6.2" + resolved "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz#2068a9dcf8e67dd0ec3e7a2bcb76810faa85e426" + integrity sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ== + dependencies: + eventemitter3 "^4.0.4" + p-timeout "^3.2.0" + +p-reduce@^2.0.0, p-reduce@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz#09408da49507c6c274faa31f28df334bc712b64a" + integrity sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw== p-retry@^3.0.1: version "3.0.1" @@ -19351,7 +19540,7 @@ p-some@^5.0.0: aggregate-error "^3.0.0" p-cancelable "^2.0.0" -p-timeout@^3.1.0: +p-timeout@^3.1.0, p-timeout@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz#c7e17abc971d2a7962ef83626b35d635acf23dfe" integrity sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg== @@ -19368,12 +19557,12 @@ p-try@^2.0.0: resolved "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6" integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ== -p-waterfall@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/p-waterfall/-/p-waterfall-1.0.0.tgz#7ed94b3ceb3332782353af6aae11aa9fc235bb00" - integrity sha1-ftlLPOszMngjU69qrhGqn8I1uwA= +p-waterfall@^2.1.1: + version "2.1.1" + resolved "https://registry.npmjs.org/p-waterfall/-/p-waterfall-2.1.1.tgz#63153a774f472ccdc4eb281cdb2967fcf158b2ee" + integrity sha512-RRTnDb2TBG/epPRI2yYXsimO0v3BXC8Yd3ogr1545IaqKK17VGhbWVeGGN+XfCm/08OK8635nH31c8bATkHuSw== dependencies: - p-reduce "^1.0.0" + p-reduce "^2.0.0" package-json@^6.3.0: version "6.5.0" @@ -19395,6 +19584,31 @@ packet-reader@1.0.0: resolved "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz#9238e5480dedabacfe1fe3f2771063f164157d74" integrity sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ== +pacote@^11.2.6: + version "11.2.6" + resolved "https://registry.npmjs.org/pacote/-/pacote-11.2.6.tgz#c0426e5d5c8d33aeea3461a75e1390f1ba78f953" + integrity sha512-xCl++Hb3aBC7LaWMimbO4xUqZVsEbKDVc6KKDIIyAeBYrmMwY1yJC2nES/lsGd8sdQLUosgBxQyuVNncZ2Ru0w== + dependencies: + "@npmcli/git" "^2.0.1" + "@npmcli/installed-package-contents" "^1.0.6" + "@npmcli/promise-spawn" "^1.2.0" + "@npmcli/run-script" "^1.8.2" + cacache "^15.0.5" + chownr "^2.0.0" + fs-minipass "^2.1.0" + infer-owner "^1.0.4" + minipass "^3.1.3" + mkdirp "^1.0.3" + npm-package-arg "^8.0.1" + npm-packlist "^2.1.4" + npm-pick-manifest "^6.0.0" + npm-registry-fetch "^9.0.0" + promise-retry "^2.0.1" + read-package-json-fast "^2.0.1" + rimraf "^3.0.2" + ssri "^8.0.1" + tar "^6.1.0" + pako@^1.0.10, pako@~1.0.5: version "1.0.11" resolved "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf" @@ -19510,6 +19724,11 @@ parse-path@^4.0.0: is-ssh "^1.3.0" protocols "^1.4.0" +parse-srcset@^1.0.2: + version "1.0.2" + resolved "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz#f2bd221f6cc970a938d88556abc589caaaa2bde1" + integrity sha1-8r0iH2zJcKk42IVWq8WJyqqiveE= + parse-url@^5.0.0: version "5.0.1" resolved "https://registry.npmjs.org/parse-url/-/parse-url-5.0.1.tgz#99c4084fc11be14141efa41b3d117a96fcb9527f" @@ -20433,6 +20652,15 @@ postcss@^6.0.1: source-map "^0.6.1" supports-color "^5.4.0" +postcss@^8.0.2: + version "8.2.6" + resolved "https://registry.npmjs.org/postcss/-/postcss-8.2.6.tgz#5d69a974543b45f87e464bc4c3e392a97d6be9fe" + integrity sha512-xpB8qYxgPuly166AGlpRjUdEYtmOWx2iCwGmrv4vqZL9YPVviDVPZPRXxnXr6xPZOdxQ9lp3ZBFCRgWJ7LE3Sg== + dependencies: + colorette "^1.2.1" + nanoid "^3.1.20" + source-map "^0.6.1" + postgres-array@~1.0.0: version "1.0.3" resolved "https://registry.npmjs.org/postgres-array/-/postgres-array-1.0.3.tgz#c561fc3b266b21451fc6555384f4986d78ec80f5" @@ -20618,13 +20846,13 @@ promise-inflight@^1.0.1: resolved "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz#98472870bf228132fcbdd868129bad12c3c029e3" integrity sha1-mEcocL8igTL8vdhoEputEsPAKeM= -promise-retry@^1.1.1: - version "1.1.1" - resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-1.1.1.tgz#6739e968e3051da20ce6497fb2b50f6911df3d6d" - integrity sha1-ZznpaOMFHaIM5kl/srUPaRHfPW0= +promise-retry@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz#ff747a13620ab57ba688f5fc67855410c370da22" + integrity sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g== dependencies: - err-code "^1.0.0" - retry "^0.10.0" + err-code "^2.0.2" + retry "^0.12.0" promise.allsettled@^1.0.0: version "1.0.2" @@ -20716,13 +20944,6 @@ protocols@^1.1.0, protocols@^1.4.0: resolved "https://registry.npmjs.org/protocols/-/protocols-1.4.7.tgz#95f788a4f0e979b291ffefcf5636ad113d037d32" integrity sha512-Fx65lf9/YDn3hUX08XUc0J8rSux36rEsyiv21ZGUC1mOyeM3lTRpZLcrm8aAolzS4itwVfm7TAPyxC2E5zd6xg== -protoduck@^5.0.1: - version "5.0.1" - resolved "https://registry.npmjs.org/protoduck/-/protoduck-5.0.1.tgz#03c3659ca18007b69a50fd82a7ebcc516261151f" - integrity sha512-WxoCeDCoCBY55BMvj4cAEjdVUFGRWed9ZxPlqTKYyw1nDDTQ4pqmnIMAGfJlg7Dx35uB/M+PHJPTmGOvaCaPTg== - dependencies: - genfun "^5.0.0" - proxy-addr@~2.0.5: version "2.0.6" resolved "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz#fdc2336505447d3f2f2c638ed272caf614bbb2bf" @@ -20770,6 +20991,11 @@ public-encrypt@^4.0.0: randombytes "^2.0.1" safe-buffer "^5.1.2" +puka@^1.0.1: + version "1.0.1" + resolved "https://registry.npmjs.org/puka/-/puka-1.0.1.tgz#a2df782b7eb4cf9564e4c93a5da422de0dfacc02" + integrity sha512-ssjRZxBd7BT3dte1RR3VoeT2cT/ODH8x+h0rUF1rMqB0srHYf48stSDWfiYakTp5UBZMxroZhB2+ExLDHm7W3g== + pump@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz#12399add6e4cf7526d973cbc8b5ce2e2908b3909" @@ -20888,11 +21114,6 @@ querystringify@^2.1.1: resolved "https://registry.npmjs.org/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== -quick-lru@^1.0.0: - version "1.1.0" - resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-1.1.0.tgz#4360b17c61136ad38078397ff11416e186dcfbb8" - integrity sha1-Q2CxfGETatOAeDl/8RQW4Ybc+7g= - quick-lru@^4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz#5b8878f113a58217848c6482026c73e1ba57727f" @@ -21525,14 +21746,20 @@ reactcss@^1.2.0: dependencies: lodash "^4.0.1" -read-cmd-shim@^1.0.1: - version "1.0.5" - resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-1.0.5.tgz#87e43eba50098ba5a32d0ceb583ab8e43b961c16" - integrity sha512-v5yCqQ/7okKoZZkBQUAfTsQ3sVJtXdNfbPnI5cceppoxEVLYA3k+VtV2omkeo8MS94JCy4fSiUwlRBAwCVRPUA== - dependencies: - graceful-fs "^4.1.2" +read-cmd-shim@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/read-cmd-shim/-/read-cmd-shim-2.0.0.tgz#4a50a71d6f0965364938e9038476f7eede3928d9" + integrity sha512-HJpV9bQpkl6KwjxlJcBoqu9Ba0PQg8TqSNIOrulGt54a0uup0HtevreFHzYzkm0lpnleRdNBzXznKrgxglEHQw== -"read-package-json@1 || 2", read-package-json@^2.0.0, read-package-json@^2.0.13: +read-package-json-fast@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-2.0.1.tgz#c767f6c634873ffb6bb73788191b65559734f555" + integrity sha512-bp6z0tdgLy9KzdfENDIw/53HWAolOVoQTRWXv7PUiqAo3YvvoUVeLr7RWPWq+mu7KUOu9kiT4DvxhUgNUBsvug== + dependencies: + json-parse-even-better-errors "^2.3.0" + npm-normalize-package-bin "^1.0.1" + +read-package-json@^2.0.0: version "2.1.1" resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.1.tgz#16aa66c59e7d4dad6288f179dd9295fd59bb98f1" integrity sha512-dAiqGtVc/q5doFz6096CcnXhpYk0ZN8dEKVkGLU0CsASt8SrgF6SF7OTKAYubfvFhWaqofl+Y8HK19GR8jwW+A== @@ -21544,7 +21771,17 @@ read-cmd-shim@^1.0.1: optionalDependencies: graceful-fs "^4.1.2" -read-package-tree@^5.1.6: +read-package-json@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/read-package-json/-/read-package-json-3.0.0.tgz#2219328e77c9be34f035a4ce58d1fb8e2979adf9" + integrity sha512-4TnJZ5fnDs+/3deg1AuMExL4R1SFNRLQeOhV9c8oDKm3eoG6u8xU0r0mNNRJHi3K6B+jXmT7JOhwhAklWw9SSQ== + dependencies: + glob "^7.1.1" + json-parse-even-better-errors "^2.3.0" + normalize-package-data "^3.0.0" + npm-normalize-package-bin "^1.0.0" + +read-package-tree@^5.3.1: version "5.3.1" resolved "https://registry.npmjs.org/read-package-tree/-/read-package-tree-5.3.1.tgz#a32cb64c7f31eb8a6f31ef06f9cedf74068fe636" integrity sha512-mLUDsD5JVtlZxjSlPPx1RETkNjjvQYuweKwNVt1Sn8kP5Jh44pvYuUHCp6xSVDZWbNxVxG5lyZJ921aJH61sTw== @@ -21662,7 +21899,7 @@ read@1, read@~1.0.1: string_decoder "~1.1.1" util-deprecate "~1.0.1" -"readable-stream@2 || 3", 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@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== @@ -21750,14 +21987,6 @@ redent@^1.0.0: indent-string "^2.1.0" strip-indent "^1.0.1" -redent@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/redent/-/redent-2.0.0.tgz#c1b2007b42d57eb1389079b3c8333639d5e1ccaa" - integrity sha1-wbIAe0LVfrE4kHmzyDM2OdXhzKo= - dependencies: - indent-string "^3.0.0" - strip-indent "^2.0.0" - redent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz#e557b7998316bb53c9f1f56fa626352c6963059f" @@ -22257,11 +22486,6 @@ retry@0.12.0, retry@^0.12.0: resolved "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz#1b42a6266a21f07421d1b0b54b7dc167b01c013b" integrity sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs= -retry@^0.10.0: - version "0.10.1" - resolved "https://registry.npmjs.org/retry/-/retry-0.10.1.tgz#e76388d217992c252750241d3d3956fed98d8ff4" - integrity sha1-52OI0heZLCUnUCQdPTlW/tmNj/Q= - reusify@^1.0.4: version "1.0.4" resolved "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz#90da382b1e126efc02146e90845a88db12925d76" @@ -22294,7 +22518,7 @@ rifm@^0.7.0: dependencies: "@babel/runtime" "^7.3.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3: +rimraf@2, rimraf@^2.2.8, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.3: version "2.7.1" resolved "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz#35797f13a7fdadc566142c29d4f07ccad483e3ec" integrity sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w== @@ -22452,7 +22676,7 @@ safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== -safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@^5.2.0, safe-buffer@~5.2.0: +safe-buffer@5.2.0, safe-buffer@>=5.1.0, safe-buffer@^5.0.1, safe-buffer@^5.1.0, safe-buffer@^5.1.1, safe-buffer@^5.1.2, safe-buffer@~5.2.0: version "5.2.0" resolved "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz#b74daec49b1148f88c64b68d49b1e815c1f2f519" integrity sha512-fZEwUGbVl7kouZs1jCdMLdt95hdIv0ZeHg6L7qPeciMZhZ+/gdesW4wgTARkrFWEpspjEATAzUGPG8N2jJiwbg== @@ -22474,7 +22698,7 @@ safe-regex@^1.1.0: dependencies: ret "~0.1.10" -"safer-buffer@>= 2.1.2 < 3", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: +"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@^2.0.2, safer-buffer@^2.1.0, safer-buffer@~2.1.0: version "2.1.2" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz#44fa161b0187b9549dd84bb91802f9bd8385cd6a" integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== @@ -22494,17 +22718,18 @@ sane@^4.0.3: minimist "^1.1.1" walker "~1.0.5" -sanitize-html@^1.27.0: - version "1.27.0" - resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.0.tgz#42104a2d59f1a48b616b5165ad5349824861e580" - integrity sha512-U1btucGeYVpg0GoK43jPpe/bDCV4cBOGuxzv5NBd0bOjyZdMKY0n98S/vNlO1wVwre0VCj8H3hbzE7gD2+RjKA== +sanitize-html@^2.3.2: + version "2.3.2" + resolved "https://registry.npmjs.org/sanitize-html/-/sanitize-html-2.3.2.tgz#a1954aea877a096c408aca7b0c260bef6e4fc402" + integrity sha512-p7neuskvC8pSurUjdVmbWPXmc9A4+QpOXIL+4gwFC+av5h+lYCXFT8uEneqsFQg/wEA1IH+cKQA60AaQI6p3cg== dependencies: - chalk "^2.4.1" - htmlparser2 "^4.1.0" - lodash "^4.17.15" - postcss "^7.0.27" - srcset "^2.0.1" - xtend "^4.0.1" + deepmerge "^4.2.2" + escape-string-regexp "^4.0.0" + htmlparser2 "^6.0.0" + is-plain-object "^5.0.0" + klona "^2.0.3" + parse-srcset "^1.0.2" + postcss "^8.0.2" sax@1.2.1: version "1.2.1" @@ -22593,7 +22818,7 @@ semver-regex@^3.1.2: resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== -"semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.6.0, semver@^5.7.1: version "5.7.1" resolved "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== @@ -22608,7 +22833,7 @@ semver@7.0.0: resolved "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz#5f3ca35761e47e05b206c6daff2cf814f0316b8e" integrity sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A== -semver@7.x, semver@^7.2.1, semver@^7.3.2: +semver@7.x, semver@^7.0.0, semver@^7.1.1, semver@^7.1.3, semver@^7.2.1, semver@^7.3.2, semver@^7.3.4: version "7.3.4" resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.4.tgz#27aaa7d2e4ca76452f98d3add093a72c943edc97" integrity sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw== @@ -22794,7 +23019,7 @@ shell-quote@1.7.2: resolved "https://registry.npmjs.org/shell-quote/-/shell-quote-1.7.2.tgz#67a7d02c76c9da24f99d20808fcaded0e0e04be2" integrity sha512-mRz/m/JVscCrkMyPqHc/bczi3OQHkLTqXHEFu0zDhK/qfv3UcOA4SVmRCLmos4bhjr9ekVQubj/R7waKapmiQg== -shelljs@^0.8.2, shelljs@^0.8.4: +shelljs@^0.8.2, shelljs@^0.8.3, shelljs@^0.8.4: version "0.8.4" resolved "https://registry.npmjs.org/shelljs/-/shelljs-0.8.4.tgz#de7684feeb767f8716b326078a8a00875890e3c2" integrity sha512-7gk3UZ9kOfPLIAbslLzyWeGiEqx9e3rxwZM0KE6EL8GlGwjym9Mrlx5/p33bWTu9YG6vcS4MBxYZDHYr5lr8BQ== @@ -22831,7 +23056,7 @@ side-channel@^1.0.2: es-abstract "^1.17.0-next.1" object-inspect "^1.7.0" -signal-exit@^3.0.0, signal-exit@^3.0.2: +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" integrity sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA== @@ -22991,20 +23216,21 @@ sockjs@0.3.20: uuid "^3.4.0" websocket-driver "0.6.5" -socks-proxy-agent@^4.0.0: - version "4.0.2" - resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-4.0.2.tgz#3c8991f3145b2799e70e11bd5fbc8b1963116386" - integrity sha512-NT6syHhI9LmuEMSK6Kd2V7gNv5KFZoLE7V5udWmn0de+3Mkj3UMA/AJPLyeNUVmElCurSHtUdM3ETpR3z770Wg== +socks-proxy-agent@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-5.0.0.tgz#7c0f364e7b1cf4a7a437e71253bed72e9004be60" + integrity sha512-lEpa1zsWCChxiynk+lCycKuC502RxDWLKJZoIhnxrWNjLSDGYRFflHA1/228VkRcnv9TIb8w98derGbpKxJRgA== dependencies: - agent-base "~4.2.1" - socks "~2.3.2" + agent-base "6" + debug "4" + socks "^2.3.3" -socks@~2.3.2: - version "2.3.3" - resolved "https://registry.npmjs.org/socks/-/socks-2.3.3.tgz#01129f0a5d534d2b897712ed8aceab7ee65d78e3" - integrity sha512-o5t52PCNtVdiOvzMry7wU4aOqYWL0PeCXRWBEiJow4/i/wr+wpsJQ9awEu1EonLIqsfGd5qSgDdxEOvCdmBEpA== +socks@^2.3.3: + version "2.5.1" + resolved "https://registry.npmjs.org/socks/-/socks-2.5.1.tgz#7720640b6b5ec9a07d556419203baa3f0596df5f" + integrity sha512-oZCsJJxapULAYJaEYBSzMcz8m3jqgGrHaGhkmU/o/PQfFWYWxkAaA0UMGImb6s6tEXfKi959X6VJjMMQ3P6TTQ== dependencies: - ip "1.1.5" + ip "^1.1.5" smart-buffer "^4.1.0" sort-keys@^1.0.0: @@ -23021,6 +23247,13 @@ sort-keys@^2.0.0: dependencies: is-plain-obj "^1.0.0" +sort-keys@^4.0.0: + version "4.2.0" + resolved "https://registry.npmjs.org/sort-keys/-/sort-keys-4.2.0.tgz#6b7638cee42c506fff8c1cecde7376d21315be18" + integrity sha512-aUYIEU/UviqPgc8mHR6IW1EGxkAXpeRETYcrzg8cLAvUPZcpAlleSXHV2mY7G12GphSH6Gzv+4MMVSSkbdteHg== + dependencies: + is-plain-obj "^2.0.0" + source-list-map@^2.0.0: version "2.0.1" resolved "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34" @@ -23167,12 +23400,12 @@ split-string@^3.0.1, split-string@^3.0.2: dependencies: extend-shallow "^3.0.0" -split2@^2.0.0: - version "2.2.0" - resolved "https://registry.npmjs.org/split2/-/split2-2.2.0.tgz#186b2575bcf83e85b7d18465756238ee4ee42493" - integrity sha512-RAb22TG39LhI31MbreBgIuKiIKhVsawfTgEGqKHTK87aG+ul/PB8Sqoi3I7kVdRWiCfrKxK3uo4/YUkpNvhPbw== +split2@^3.0.0: + version "3.2.2" + resolved "https://registry.npmjs.org/split2/-/split2-3.2.2.tgz#bf2cf2a37d838312c249c89206fd7a17dd12365f" + integrity sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg== dependencies: - through2 "^2.0.2" + readable-stream "^3.0.0" split@0.3: version "0.3.3" @@ -23203,11 +23436,6 @@ sqlite3@^5.0.0: optionalDependencies: node-gyp "3.x" -srcset@^2.0.1: - version "2.0.1" - resolved "https://registry.npmjs.org/srcset/-/srcset-2.0.1.tgz#8f842d357487eb797f413d9c309de7a5149df5ac" - integrity sha512-00kZI87TdRKwt+P8jj8UZxbfp7mK2ufxcIMWvhAOZNJTRROimpHeruWrGvCZneiuVDLqdyHefVp748ECTnyUBQ== - ssh2-streams@~0.4.10: version "0.4.10" resolved "https://registry.npmjs.org/ssh2-streams/-/ssh2-streams-0.4.10.tgz#48ef7e8a0e39d8f2921c30521d56dacb31d23a34" @@ -23239,7 +23467,7 @@ sshpk@^1.7.0: safer-buffer "^2.0.2" tweetnacl "~0.14.0" -ssri@^6.0.0, ssri@^6.0.1: +ssri@^6.0.1: version "6.0.1" resolved "https://registry.npmjs.org/ssri/-/ssri-6.0.1.tgz#2a3c41b28dd45b62b63676ecb74001265ae9edd8" integrity sha512-3Wge10hNcT1Kur4PDFwEieXSCMCJs/7WvSACcrMYrNp+b8kDL1/0wJch5Ni2WrtwEa2IO8OsVfeKIciKCDx/QA== @@ -23253,6 +23481,13 @@ ssri@^8.0.0: dependencies: minipass "^3.1.1" +ssri@^8.0.1: + version "8.0.1" + resolved "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz#638e4e439e2ffbd2cd289776d5ca457c4f51a2af" + integrity sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ== + dependencies: + minipass "^3.1.1" + stable@^0.1.8: version "0.1.8" resolved "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf" @@ -23482,7 +23717,7 @@ string-width@^1.0.1, string-width@^1.0.2: is-fullwidth-code-point "^1.0.0" strip-ansi "^3.0.0" -"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.0, string-width@^2.1.1: +"string-width@^1.0.2 || 2", string-width@^2.0.0, string-width@^2.1.1: version "2.1.1" resolved "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz#ab93f27a8dc13d28cac815c462143a6d9012ae9e" integrity sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw== @@ -23653,11 +23888,6 @@ strip-indent@^1.0.1: dependencies: get-stdin "^4.0.1" -strip-indent@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" - integrity sha1-XvjbKV0B5u1sv3qrlpmNeCJSe2g= - strip-indent@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz#c32e1cee940b6b3432c771bc2c54bcce73cd3001" @@ -23675,7 +23905,7 @@ strip-json-comments@~2.0.1: resolved "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" integrity sha1-PFMZQukIwml8DsNEhYwobHygpgo= -strong-log-transformer@^2.0.0: +strong-log-transformer@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/strong-log-transformer/-/strong-log-transformer-2.1.0.tgz#0f5ed78d325e0421ac6f90f7f10e691d6ae3ae10" integrity sha512-B3Hgul+z0L9a236FAUC9iZsL+nVHgoCJnqCbN588DjYxvGXaXaaFbfmQ/JhvKjZwsOukuR72XbHv71Qkug0HxA== @@ -24004,7 +24234,7 @@ tar@^2.0.0: fstream "^1.0.12" inherits "2" -tar@^4, tar@^4.4.10, tar@^4.4.12, tar@^4.4.8: +tar@^4, tar@^4.4.12: version "4.4.13" resolved "https://registry.npmjs.org/tar/-/tar-4.4.13.tgz#43b364bc52888d555298637b10d60790254ab525" integrity sha512-w2VwSrBoHa5BsSyH+KxEqeQBAllHhccyMFVHtGtdMpF4W7IRWfZjFiQceJPChOeTsSDVUpER2T8FA93pr0L+QA== @@ -24029,6 +24259,18 @@ tar@^6.0.1, tar@^6.0.2, tar@^6.0.5: mkdirp "^1.0.3" yallist "^4.0.0" +tar@^6.1.0: + version "6.1.0" + resolved "https://registry.npmjs.org/tar/-/tar-6.1.0.tgz#d1724e9bcc04b977b18d5c573b333a2207229a83" + integrity sha512-DUCttfhsnLCjwoDoFcI+B2iJgYa93vBnDUATYEeRx6sntCTdN01VnqsIuTlALXla/LWooNg0yEGeB+Y8WdFxGA== + dependencies: + chownr "^2.0.0" + fs-minipass "^2.0.0" + minipass "^3.0.0" + minizlib "^2.1.1" + mkdirp "^1.0.3" + yallist "^4.0.0" + tarn@^3.0.1: version "3.0.1" resolved "https://registry.npmjs.org/tarn/-/tarn-3.0.1.tgz#ebac2c6dbc6977d34d4526e0a7814200386a8aec" @@ -24064,17 +24306,16 @@ temp-dir@^1.0.0: resolved "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz#0a7c0ea26d3a39afa7e0ebea9c1fc0bc4daa011d" integrity sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0= -temp-write@^3.4.0: - version "3.4.0" - resolved "https://registry.npmjs.org/temp-write/-/temp-write-3.4.0.tgz#8cff630fb7e9da05f047c74ce4ce4d685457d492" - integrity sha1-jP9jD7fp2gXwR8dM5M5NaFRX1JI= +temp-write@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/temp-write/-/temp-write-4.0.0.tgz#cd2e0825fc826ae72d201dc26eef3bf7e6fc9320" + integrity sha512-HIeWmj77uOOHb0QX7siN3OtwV3CTntquin6TNVg6SHOqCP3hYKmox90eeFOGaY1MqJ9WYDDjkyZrW6qS5AWpbw== dependencies: - graceful-fs "^4.1.2" - is-stream "^1.1.0" - make-dir "^1.0.0" - pify "^3.0.0" + graceful-fs "^4.1.15" + is-stream "^2.0.0" + make-dir "^3.0.0" temp-dir "^1.0.0" - uuid "^3.0.1" + uuid "^3.3.2" term-size@^1.2.0: version "1.2.0" @@ -24205,7 +24446,7 @@ throttleit@^1.0.0: resolved "https://registry.npmjs.org/throttleit/-/throttleit-1.0.0.tgz#9e785836daf46743145a5984b6268d828528ac6c" integrity sha1-nnhYNtr0Z0MUWlmEtiaNgoUorGw= -through2@^2.0.0, through2@^2.0.2: +through2@^2.0.0: version "2.0.5" resolved "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== @@ -24213,12 +24454,12 @@ through2@^2.0.0, through2@^2.0.2: readable-stream "~2.3.6" xtend "~4.0.1" -through2@^3.0.0: - version "3.0.1" - resolved "https://registry.npmjs.org/through2/-/through2-3.0.1.tgz#39276e713c3302edf9e388dd9c812dd3b825bd5a" - integrity sha512-M96dvTalPT3YbYLaKaCuwu+j06D/8Jfib0o/PxbVt6Amhv3dUAtW6rTV1jPgJSBG83I/e04Y6xkVdVhSRhi0ww== +through2@^4.0.0: + version "4.0.2" + resolved "https://registry.npmjs.org/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== dependencies: - readable-stream "2 || 3" + readable-stream "3" through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8, through@~2.3, through@~2.3.1: version "2.3.8" @@ -24452,11 +24693,6 @@ trim-newlines@^1.0.0: resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz#5887966bb582a4503a41eb524f7d35011815a613" integrity sha1-WIeWa7WCpFA6QetST301ARgVphM= -trim-newlines@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-2.0.0.tgz#b403d0b91be50c331dfc4b82eeceb22c3de16d20" - integrity sha1-tAPQuRvlDDMd/EuC7s6yLD3hbSA= - trim-newlines@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.0.tgz#79726304a6a898aa8373427298d54c2ee8b1cb30" @@ -24669,10 +24905,15 @@ type-fest@^0.13.1: resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== -type-fest@^0.3.0: - version "0.3.1" - resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" - integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== +type-fest@^0.18.0: + version "0.18.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz#db4bc151a4a2cf4eebf9add5db75508db6cc841f" + integrity sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw== + +type-fest@^0.4.1: + version "0.4.1" + resolved "https://registry.npmjs.org/type-fest/-/type-fest-0.4.1.tgz#8bdf77743385d8a4f13ba95f610f5ccd68c728f8" + integrity sha512-IwzA/LSfD2vC1/YDYMv/zHP4rDF1usCwllsDpbolT3D4fUepIO7f9K70jjmUewU/LmGUKJcwcVtDCpnKk4BPMw== type-fest@^0.6.0: version "0.6.0" @@ -24924,13 +25165,6 @@ universal-github-app-jwt@^1.0.1: "@types/jsonwebtoken" "^8.3.3" jsonwebtoken "^8.5.1" -universal-user-agent@^4.0.0: - version "4.0.1" - resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-4.0.1.tgz#fd8d6cb773a679a709e967ef8288a31fcc03e557" - integrity sha512-LnST3ebHwVL2aNe4mejI9IQh2HfZ1RLo8Io2HugSif8ekzD1TlWpHpColOB/eh8JHMLkGH3Akqf040I+4ylNxg== - dependencies: - os-name "^3.1.0" - universal-user-agent@^5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-5.0.0.tgz#a3182aa758069bf0e79952570ca757de3579c1d9" @@ -24953,6 +25187,11 @@ universalify@^1.0.0: resolved "https://registry.npmjs.org/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + unixify@1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/unixify/-/unixify-1.0.0.tgz#3a641c8c2ffbce4da683a5c70f03a462940c2090" @@ -24999,11 +25238,16 @@ unzipper@^0.10.11: readable-stream "~2.3.6" setimmediate "~1.0.4" -upath@^1.1.1, upath@^1.2.0: +upath@^1.1.1: version "1.2.0" resolved "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== +upath@^2.0.1: + version "2.0.1" + resolved "https://registry.npmjs.org/upath/-/upath-2.0.1.tgz#50c73dea68d6f6b990f51d279ce6081665d61a8b" + integrity sha512-1uEe95xksV1O0CYKXo8vQvN1JEbtJp7lb7C5U9HMsIp6IVwntkH/oNUzyVNQSd4S1sYk2FpSSW44FqMc8qee5w== + update-notifier@^4.1.0: version "4.1.3" resolved "https://registry.npmjs.org/update-notifier/-/update-notifier-4.1.3.tgz#be86ee13e8ce48fb50043ff72057b5bd598e1ea3" @@ -25177,7 +25421,7 @@ uuid@3.3.2: resolved "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131" integrity sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA== -uuid@^3.0.1, uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: +uuid@^3.1.0, uuid@^3.3.2, uuid@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -25218,7 +25462,7 @@ valid-url@1.0.9, valid-url@^1.0.9: resolved "https://registry.npmjs.org/valid-url/-/valid-url-1.0.9.tgz#1c14479b40f1397a75782f115e4086447433a200" integrity sha1-HBRHm0DxOXp1eC8RXkCGRHQzogA= -validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.3: +validate-npm-package-license@^3.0.1, validate-npm-package-license@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz#fc91f6b9c7ba15c857f4cb2c5defeec39d4f410a" integrity sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew== @@ -25616,6 +25860,15 @@ whatwg-url@^8.0.0: tr46 "^2.0.2" webidl-conversions "^5.0.0" +whatwg-url@^8.4.0: + version "8.4.0" + resolved "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.4.0.tgz#50fb9615b05469591d2b2bd6dfaed2942ed72837" + integrity sha512-vwTUFf6V4zhcPkWp/4CQPr1TW9Ml6SF4lVyaIMBdJw5i6qUUJ1QWM4Z6YYVkfka0OUIzVo/0aNtGVGk256IKWw== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^2.0.2" + webidl-conversions "^6.1.0" + which-module@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz#bba63ca861948994ff307736089e3b96026c2a4f" @@ -25781,7 +26034,7 @@ wrappy@1: resolved "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" integrity sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= -write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.2: +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== @@ -25790,7 +26043,7 @@ write-file-atomic@^2.0.0, write-file-atomic@^2.3.0, write-file-atomic@^2.4.2: imurmurhash "^0.1.4" signal-exit "^3.0.2" -write-file-atomic@^3.0.0: +write-file-atomic@^3.0.0, write-file-atomic@^3.0.3: version "3.0.3" resolved "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz#56bd5c5a5c70481cd19c571bd39ab965a5de56e8" integrity sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q== @@ -25800,18 +26053,6 @@ write-file-atomic@^3.0.0: signal-exit "^3.0.2" typedarray-to-buffer "^3.1.5" -write-json-file@^2.2.0: - version "2.3.0" - resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-2.3.0.tgz#2b64c8a33004d54b8698c76d585a77ceb61da32f" - integrity sha1-K2TIozAE1UuGmMdtWFp3zrYdoy8= - dependencies: - detect-indent "^5.0.0" - graceful-fs "^4.1.2" - make-dir "^1.0.0" - pify "^3.0.0" - sort-keys "^2.0.0" - write-file-atomic "^2.0.0" - write-json-file@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-3.2.0.tgz#65bbdc9ecd8a1458e15952770ccbadfcff5fe62a" @@ -25824,13 +26065,26 @@ write-json-file@^3.2.0: sort-keys "^2.0.0" write-file-atomic "^2.4.2" -write-pkg@^3.1.0: - version "3.2.0" - resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-3.2.0.tgz#0e178fe97820d389a8928bc79535dbe68c2cff21" - integrity sha512-tX2ifZ0YqEFOF1wjRW2Pk93NLsj02+n1UP5RvO6rCs0K6R2g1padvf006cY74PQJKMGS2r42NK7FD0dG6Y6paw== +write-json-file@^4.3.0: + version "4.3.0" + resolved "https://registry.npmjs.org/write-json-file/-/write-json-file-4.3.0.tgz#908493d6fd23225344af324016e4ca8f702dd12d" + integrity sha512-PxiShnxf0IlnQuMYOPPhPkhExoCQuTUNPOa/2JWCYTmBquU9njyyDuwRKN26IZBlp4yn1nt+Agh2HOOBl+55HQ== + dependencies: + detect-indent "^6.0.0" + graceful-fs "^4.1.15" + is-plain-obj "^2.0.0" + make-dir "^3.0.0" + sort-keys "^4.0.0" + write-file-atomic "^3.0.0" + +write-pkg@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/write-pkg/-/write-pkg-4.0.0.tgz#675cc04ef6c11faacbbc7771b24c0abbf2a20039" + integrity sha512-v2UQ+50TNf2rNHJ8NyWttfm/EJUBWMJcx6ZTYZr6Qp52uuegWw/lBkCtCbnYZEmPRNL61m+u67dAmGxo+HTULA== dependencies: sort-keys "^2.0.0" - write-json-file "^2.2.0" + type-fest "^0.4.1" + write-json-file "^3.2.0" write@1.0.3: version "1.0.3" @@ -26002,18 +26256,16 @@ yaml@^1.10.0, yaml@^1.7.2, yaml@^1.9.2: resolved "https://registry.npmjs.org/yaml/-/yaml-1.10.0.tgz#3b593add944876077d4d683fee01081bd9fff31e" integrity sha512-yr2icI4glYaNG+KWONODapy2/jDdMSDnrONSjblABjD9B4Z5LgiircSt8m8sRZFNi08kG9Sm0uSHtEmP3zaEGg== +yargs-parser@20.2.4, yargs-parser@^20.2.2, yargs-parser@^20.2.3: + version "20.2.4" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" + integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== + yargs-parser@20.x: version "20.2.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26" integrity sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww== -yargs-parser@^10.0.0: - version "10.1.0" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-10.1.0.tgz#7202265b89f7e9e9f2e5765e0fe735a905edbaa8" - integrity sha512-VCIyR1wJoEBZUqk5PA+oOBF6ypbwh5aNB3I50guxAL/quggdfs4TtNHQrSazFA3fYZ+tEqfs0zIGlv0c/rgjbQ== - dependencies: - camelcase "^4.1.0" - yargs-parser@^13.1.2: version "13.1.2" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" @@ -26022,14 +26274,6 @@ yargs-parser@^13.1.2: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^15.0.1: - version "15.0.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.1.tgz#54786af40b820dcb2fb8025b11b4d659d76323b3" - integrity sha512-0OAMV2mAZQrs3FkNpDQcBk1x5HXb8X4twADss4S0Iuk+2dGnLOE/fRHrsYm542GduMveyA77OF4wrNJuanRCWw== - dependencies: - camelcase "^5.0.0" - decamelize "^1.2.0" - yargs-parser@^18.1.2, yargs-parser@^18.1.3: version "18.1.3" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0" @@ -26038,11 +26282,6 @@ yargs-parser@^18.1.2, yargs-parser@^18.1.3: camelcase "^5.0.0" decamelize "^1.2.0" -yargs-parser@^20.2.2: - version "20.2.4" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz#b42890f14566796f85ae8e3a25290d205f154a54" - integrity sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA== - yargs-parser@^3.2.0: version "3.2.0" resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-3.2.0.tgz#5081355d19d9d0c8c5d81ada908cb4e6d186664f" @@ -26067,23 +26306,6 @@ yargs@^13.3.0, yargs@^13.3.2: y18n "^4.0.0" yargs-parser "^13.1.2" -yargs@^14.2.2: - version "14.2.3" - resolved "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" - integrity sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg== - dependencies: - cliui "^5.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^2.0.1" - require-directory "^2.1.1" - require-main-filename "^2.0.0" - set-blocking "^2.0.0" - string-width "^3.0.0" - which-module "^2.0.0" - y18n "^4.0.0" - yargs-parser "^15.0.1" - yargs@^15.1.0, yargs@^15.3.1, yargs@^15.4.1: version "15.4.1" resolved "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
{emptyContent}