diff --git a/.changeset/breezy-planets-sparkle.md b/.changeset/breezy-planets-sparkle.md new file mode 100644 index 0000000000..eea9e5c6bf --- /dev/null +++ b/.changeset/breezy-planets-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend-module-onelogin-provider': minor +--- + +Separate out the OneLogin provider into its own module diff --git a/.changeset/forty-experts-live.md b/.changeset/forty-experts-live.md new file mode 100644 index 0000000000..11fc3b688f --- /dev/null +++ b/.changeset/forty-experts-live.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Add function to `EntityRelationsGraph` filter that excludes entities from graph diff --git a/.changeset/gentle-llamas-cross.md b/.changeset/gentle-llamas-cross.md new file mode 100644 index 0000000000..42a6d70cd6 --- /dev/null +++ b/.changeset/gentle-llamas-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Use `inherit` variant on OverflowTooltip underlying Typography component. diff --git a/.changeset/great-clouds-hang.md b/.changeset/great-clouds-hang.md new file mode 100644 index 0000000000..4e8bf28d9d --- /dev/null +++ b/.changeset/great-clouds-hang.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Exposed `DefaultSchedulerService` diff --git a/.changeset/nice-kangaroos-occur.md b/.changeset/nice-kangaroos-occur.md new file mode 100644 index 0000000000..9798885d1a --- /dev/null +++ b/.changeset/nice-kangaroos-occur.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Fix bug with missing Actions column after adding "pagination" prop to catalog table diff --git a/.changeset/popular-boxes-press.md b/.changeset/popular-boxes-press.md new file mode 100644 index 0000000000..44b50d711a --- /dev/null +++ b/.changeset/popular-boxes-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-defaults': patch +--- + +Internal minor refactors of the database connectors diff --git a/.changeset/pretty-spoons-knock.md b/.changeset/pretty-spoons-knock.md new file mode 100644 index 0000000000..5eddce54d9 --- /dev/null +++ b/.changeset/pretty-spoons-knock.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-catalog-react': patch +--- + +Prevents Autocomplete dropdown from overlapping sidebar on hovering it diff --git a/.changeset/purple-mugs-beg.md b/.changeset/purple-mugs-beg.md new file mode 100644 index 0000000000..2507b5d706 --- /dev/null +++ b/.changeset/purple-mugs-beg.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli-common': patch +--- + +The monorepo root check in `findPaths` will now accept a shorthand `workspaces` config in `package.json`, no longer requiring `workspaces.packages`. diff --git a/.changeset/rude-snakes-clap.md b/.changeset/rude-snakes-clap.md new file mode 100644 index 0000000000..6292730c4c --- /dev/null +++ b/.changeset/rude-snakes-clap.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-user-settings-backend': patch +'@backstage/plugin-user-settings-common': patch +'@backstage/plugin-user-settings': patch +--- + +Use signals to update user settings across sessions diff --git a/.changeset/shaggy-mugs-kiss.md b/.changeset/shaggy-mugs-kiss.md new file mode 100644 index 0000000000..8f0e800acd --- /dev/null +++ b/.changeset/shaggy-mugs-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-tasks': patch +--- + +More detailed deprecation messages diff --git a/.changeset/slow-dolls-type.md b/.changeset/slow-dolls-type.md new file mode 100644 index 0000000000..d2626f3ab3 --- /dev/null +++ b/.changeset/slow-dolls-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Changed the way to display entities in EntityPicker to use entityPresentationApi instead of humanizeEntityRef diff --git a/.changeset/tall-bananas-reflect.md b/.changeset/tall-bananas-reflect.md new file mode 100644 index 0000000000..00e8fd8b43 --- /dev/null +++ b/.changeset/tall-bananas-reflect.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Remove Tech Radar menu item from sidebar of scaffolded app to align with removal of tech-radar plugin from backend diff --git a/.changeset/twelve-pumpkins-fold.md b/.changeset/twelve-pumpkins-fold.md new file mode 100644 index 0000000000..ae63a8fce2 --- /dev/null +++ b/.changeset/twelve-pumpkins-fold.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Increase max wait time in debug:wait action to 10 minutes diff --git a/.changeset/wild-coats-doubt.md b/.changeset/wild-coats-doubt.md new file mode 100644 index 0000000000..4e86911127 --- /dev/null +++ b/.changeset/wild-coats-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Updated to use the new `@backstage/plugin-auth-backend-module-onelogin-provider` implementation diff --git a/.github/vale/config/vocabularies/Backstage/accept.txt b/.github/vale/config/vocabularies/Backstage/accept.txt index f2fe96f3ab..4f89369011 100644 --- a/.github/vale/config/vocabularies/Backstage/accept.txt +++ b/.github/vale/config/vocabularies/Backstage/accept.txt @@ -268,6 +268,7 @@ Olausson Oldsberg onboarding Onboarding +onelogin OpenSearch OpenShift openssl diff --git a/beps/0009-plugin-metadata/README.md b/beps/0009-plugin-metadata/README.md new file mode 100644 index 0000000000..a23a2d7ce4 --- /dev/null +++ b/beps/0009-plugin-metadata/README.md @@ -0,0 +1,215 @@ +--- +title: Plugin Metadata +status: provisional +authors: + - '@Rugvip' +owners: + - '@backstage/maintainers' +project-areas: + - core +creation-date: yyyy-mm-dd +--- + + + +# BEP: Plugin Metadata + + + +[**Discussion Issue**](https://github.com/backstage/backstage/issues/25076) + +- [Summary](#summary) +- [Motivation](#motivation) + - [Goals](#goals) + - [Non-Goals](#non-goals) +- [Proposal](#proposal) +- [Design Details](#design-details) +- [Release Plan](#release-plan) +- [Dependencies](#dependencies) +- [Alternatives](#alternatives) + +## Summary + +The goal of this BEP is to define a strategy for how we define metadata for Backstage packages, as well as define a new set of metadata for how to associate packages with each other. The proposed solution is to store all plugin metadata in `package.json` under the `backstage` field, unless there is an existing field that can be used instead. Each piece of metadata is documented in the Backstage documentation, and as much of the metadata as possible is generated and validated by the Backstage CLI. The validation is structured in such a way that it does not interfere with internal Backstage setups, and only applies when publishing packages to NPM. + +## Motivation + +All Backstage adopters benefit from it being easy to find, install, configure, and validate the setup of plugins and modules. That's regardless of whether one is only using a minimal set of core plugins, or installing a large number of 3rd party features from the Backstage ecosystem. This proposal aims to build upon our [existing package metadata](https://backstage.io/docs/tooling/cli/build-system#package-roles) to introduce a new set of standardized fields that plugins can use to provide additional information about themselves. + +This metadata will be used to improve tooling in and around Backstage, such as providing better validation and more powerful utilities in the Backstage CLI, and more information to be surfaced in association with plugins for better discoverability. + +### Goals + +We want to provide a new set of standardized fields that makes it possible to build tooling that improves the following interactions with the Backstage ecosystem: + +- Browsing packages related to a plugin, i.e. listing all available library packages and frontend/backend variants of a specific plugin. +- Discoverability of plugin modules. Given a plugin you should be able to identify all of its modules, and given a module you should be able to discovery its plugin. + +Furthermore this proposal should also lay the foundation for how we define additional metadata fields in the future. How they are validated, documented, and surfaced in the Backstage ecosystem. + +In relation to the above, this proposal will aim to define what existing `package.json` fields are known in the Backstage ecosystem, and what their purpose is. This will help avoid duplication of fields, and aim to reduce metadata fragmentation. + +Where appropriate, this new metadata will be made required for all packages that are published using Backstage's tooling. This will ensure that packages across the ecosystem are consistent in how they provide metadata. This requirement should not be unnecessarily strict, ideally only requiring fields that either have sensible defaults or that can be inferred from workspace content. + +These goals must all be reached in a way that does not risk intentional or unintentional disruption of the metadata. For example it should not be possible for an unrelated package to claim to be part of a group of plugin packages that it is not part of. + +### Non-Goals + +The surfacing of extensive implementation metadata is not in scope for this proposal. This includes for example the extension point and extensions exported by a plugin, what route references a frontend plugin has, or what API endpoints a backend plugin exposes. + +This proposal will not aim to standardize any other fields that are not included in the above [goals](#goals). It will provide the foundation for how to define such fields, but not the fields themselves. This includes any informational fields such as package lifecycle or usage, but also fields that provide functional requirements, such as plugin dependencies or environment restrictions. + +This proposal does not aim to provide any direct solution for plugin support metadata as proposed in [#10256](https://github.com/backstage/backstage/pull/10256). Many of the fields defined as followups to this proposal may cover the support metadata use-case, but what is also important for such a solution is to be able to provide local overrides for the metadata, which this proposal does not cover. We do however want to make sure that this use-case is taken into consideration when defining new metadata fields. + +## Proposal + +The proposal is split into separate sections, each contributing to the overall solution in different ways. + +### Validation & Automation + +The purpose of this section is to define how we make it as simple as possible to populate the metadata fields, and how we ensure that the metadata is present and correct. + +Validation and automation is split into two separate phases. The first is phase is manual tooling that is run by the developer and typically validated in CI, for example the `backstage-cli fix` command. The second phase is validation at the time of publishing the package, which is done by the `yarn prepack` command. By splitting the tooling into these two phases we can ensure that a minimal set of metadata is present for all packages published using the Backstage tooling, but at the same time avoid adding unnecessary friction to the development process. + +An important aspect to take into consideration is that most Backstage packages are never published to NPM, as they're part of an internal monorepo and get built directly into an internal Backstage application. Defining the full set of metadata for these packages is likely to be wasteful, which means we should avoid building too many requirements for that path. + +### Package Metadata and Documentation + +This section defines our strategy for defining new metadata fields, and how they are documented. + +To the furthest extent possible we will rely on established standards for `package.json` fields, but we will do so within the bounds of the intended usage of these fields. This means that we will only use existing fields for their intended purpose, and instead define new fields if no existing field is suitable. An index of existing fields is found in the [NPM documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json), but we are not limited to this list. Any fields that we can find within the NPM ecosystem may be used, as long as they are well established, for example if there could be a use for the `prettier` field [defined by prettier](https://prettier.io/docs/en/configuration.html#sharing-configurations). + +In the case of new fields we always define them to be part of the top-level `"backstage"` field. The assumption is that any field that we define will always be tied to the Backstage ecosystem, even in the case of general fields such as "lifecycle". The only exception to this is if we believe that a field is generic enough that it could genuinely become part of the broader NPM ecosystem. + +All fields must be documented in a new section of the "Tooling" documentation on backstage.io. This documentation should include a description of the field, what it is used for, and what the expected values are. The documentation must also mention whether the field is expected to be filled in manually, or if it will be generated by tooling. Changes to this documentation must be approved by the Backstage core maintainers, it is not owned by individual project area maintainers. + +### Package Relationships + +This section defines how we associate packages from the same plugin with each other. + +Packages that are part of the same plugin should always be managed within the same monorepo and workspace. This does not apply to modules for a plugin, which may be hosted separately, but may still need to refer to the plugin that it is a module for. + +Each plugin package must define a `backstage.pluginId` field, which is the same identifier as is used in the implementation of the plugin. This field is inferred from the package name by the `backstage-cli repo fix` command if it is not present. It should only be defined for plugin package, for example `@backstage/errors` should not define a plugin ID. The `backstage.pluginId` field is required when publishing a package with a plugin or module role, or a library role with "plugin" in its name. + +The package relationships are defined in the `backstage.pluginPackages` field. The value of the field is an object where each key is the role of the package as defined by the `backstage.role` field, and the value is the package name. For example: + +```json +{ + "name": "@backstage/plugin-catalog", + "backstage": { + "role": "frontend-plugin", + "pluginId": "catalog", + "pluginPackages": { + "frontend-plugin": "@backstage/plugin-catalog", + "backend-plugin": "@backstage/plugin-catalog-backend", + "web-library": "@backstage/plugin-catalog-react", + "node-library": "@backstage/plugin-catalog-node", + "common-library": "@backstage/plugin-catalog-common" + } + } +} +``` + +The `backstage.pluginPackages` field is generated and updated by the `backstage-cli repo fix` command based on the packages that are present in the workspace and their `backstage.pluginId` and `backstage.role` fields. There can only be a single package of each role with a given plugin ID. The `backstage.pluginPackages` field is required when publishing a package with a `backstage.pluginId` field that is not using a module role. + +Module packages define their target plugin both via the `backstage.pluginId` field, as well as via `backstage.pluginPackage`. For example: + +```json +{ + "name": "@backstage/plugin-catalog-backend-module-github", + "backstage": { + "role": "backend-plugin-module", + "pluginId": "catalog", + "pluginPackage": "@backstage/plugin-catalog-backend" + } +} +``` + +The `backstage.pluginPackage` field is required when publishing a package with a module role. + +## Design Details + +### Package Relationships Conflict Resolution + +Conflicts in the defined metadata are handled as follows: + +- The union of all `pluginPackages` is always used. If there are multiple packages defined for the same role, conflicts are resolved using the `backstage.moved` field if possible, but otherwise all packages are rejected. +- All plugin packages with references to each other must have the same `pluginId`, or they are all rejected. +- A module must have the same `pluginId` as the plugin it is a module for, or it is rejected. +- If the role of a package does not match the definition in the `pluginPackages` field, the package is rejected. + +When metadata is "rejected" the consumer should either throw an error, or treat the conflicted fields as if they do not exist. + +### References & Prior Art + +The following resources have been used to inform this proposal: + +- [package.json documentation](https://docs.npmjs.com/cli/v10/configuring-npm/package-json) +- [VSCode Extensions](https://code.visualstudio.com/api/references/extension-manifest) + +## Release Plan + +The new metadata fields and any generation and validation of fields will initially be rolled out as part of the `repo fix` command in the Backstage CLI. In addition we will also add validation to the `yarn prepack` command in such a way that it does not interfere with backend package bundling, but does apply when packing packages for publishing. These changes will all be rolled out in a single release of the Backstage CLI, meaning that anyone publishing Backstage packages using the Backstage CLI will be required to provide the new metadata. + +## Dependencies + +None + +## Alternatives + +### Simplified Library Relationships + +Rather than listing libraries for each role, we could simply have an array of libraries that are available for the plugin: + +```json +{ + "name": "@backstage/plugin-catalog", + "backstage": { + "role": "frontend-plugin", + "pluginId": "catalog", + "pluginPackages": { + "frontend-plugin": "@backstage/plugin-catalog", + "backend-plugin": "@backstage/plugin-catalog-backend", + "libraries": [ + "@backstage/plugin-catalog-react", + "@backstage/plugin-catalog-node", + "@backstage/plugin-catalog-common" + ] + } + } +} +``` + +A benefit of this approach is that it keeps the metadata simpler and reduces the need for conflict resolution logic since we can use the union of all listed library packages. + +It also allows for the use-case of having multiple libraries of the same role, in case that would provide a benefit. Examples of this are the `@backstage/catalog-client` and `@backstage/catalog-model` packages. + +A downside of this approach could be that it may encourage a larger number of library packages, which is not necessarily what we want. Another downside is that the role of each package is not immediately available. + +### Separate Metadata File + +This proposal suggests that we keep metadata in the `package.json` file. An alternative would be to define a separate metadata file, for example `backstage.json` or `backstage.yaml`. + +A benefit of this approach is that we do not pollute the `package.json` file with additional metadata, which can help keep overall metadata easier to browse and understand. It also allows us to use our own format, such as allowing comments in JSON, or using YAML for easier readability. + +A downside of this approach is that it introduces a bit more complexity and friction in the tooling and package publishing, because we need to make sure that the metadata file is always included in the published package. We also end up with additional logic for finding and parsing this file, whereas resolving and parsing `package.json` is already a solved problem. + +One of the larger benefits of keeping package metadata `package.json` is that it makes the data immediately available in registry APIs. It avoids the need to download and parse additional files to get the metadata for a package. For this reason we believe that we should stick to using `package.json` for metadata for the time being. If we for some reason in the future find that we need to use a separate file, for example because the metadata becomes too large, we can always evolve the `backstage` field in a couple of different ways: + +```json +{ + // Moving all data to a separate file + "backstage": "backstage.json" +} +{ + // Moving some data to a separate file + "backstage": { + "role": "frontend-plugin", + "metadata": "backstage.json" + } +} +``` diff --git a/docs/auth/service-to-service-auth.md b/docs/auth/service-to-service-auth.md index 1d6de30ce6..8c9130173c 100644 --- a/docs/auth/service-to-service-auth.md +++ b/docs/auth/service-to-service-auth.md @@ -94,6 +94,9 @@ backend: options: token: ${CICD_TOKEN} subject: cicd-system-completion-events + # Restrictions are optional; see below + accessRestrictions: + - plugin: events - type: static options: token: ${ADMIN_CURL_TOKEN} @@ -260,3 +263,97 @@ header: ```yaml Authorization: Bearer eZv5o+fW3KnR3kVabMW4ZcDNLPl8nmMW ``` + +## Access Restrictions + +Each `externalAccess` entry may optionally have an `accessRestrictions` key, +which limits what that particular access method can do. Let's look at an +example: + +```yaml title="in e.g. app-config.production.yaml" +backend: + auth: + externalAccess: + - type: static + options: + token: ${CICD_TOKEN} + subject: cicd-system-completion-events + accessRestrictions: + - plugin: events +``` + +In this short example there's only one entry. It says that for anyone trying to +make access with the CICD token, they will be rejected if they try to contact +anything but the `events` backend plugin. You could add additional entries to +the array that allow targeting more plugins if that's what you want. + +:::note Note + +If no `accessRestrictions` are added, the access method has unlimited access to +all functionality of all plugins. It is recommended that you try to specify +access restrictions whenever possible, to reduce risk. + +::: + +Each entry has one or more of the following fields: + +- **`plugin`**: Required. A plugin ID as a string, for example `'catalog'`. Permits + access to make requests to this plugin. Can be further refined by setting + additional fields as per below. + + Example: + + ```yaml + accessRestrictions: + # access to any other plugin will be rejected + - plugin: my-plugin + ``` + +- **`permission`**: Optional. A collection (comma/space separated string or + string array) of permission names. If given, this method is limited to only + performing actions with these named permissions in the plugin with the ID + given above. + + Note that this only applies where permissions checks are enabled in the first + place. Endpoints that are not protected by the permissions system at all, are + not affected by this setting. + + Example: + + ```yaml + accessRestrictions: + - plugin: my-plugin + # Any other permission check will be rejected. + permission: + - my-plugin.add-item + - my-plugin.remove-item + # Also supports the shorthand form: + # permission: my-plugin.add-item, my-plugin.remove-item + ``` + +- **`permissionAttribute`**: Optional. A key-value object of permission attributes + where each value is a collection (comma/space separated string or string + array) of allowed such values. If given, this method is limited to only + performing actions whose permissions have these attributes. + + Note that this only applies where permissions checks are + enabled in the first place. Endpoints that are not protected by + the permissions system at all, are not affected by this + setting. + + In practice, this is typically used to limit by the `action` attribute, for + `'create'`, `'read'`, `'update'`, or `'delete'` values. + + Example: + + ```yaml + accessRestrictions: + - plugin: my-plugin + permissionAttribute: + # Updates and deletes will be rejected. + action: + - create + - read + # Also supports the shorthand form: + # action: create, read + ``` diff --git a/docs/backend-system/core-services/auth.md b/docs/backend-system/core-services/auth.md index 4f1229a9d7..f1243f11fe 100644 --- a/docs/backend-system/core-services/auth.md +++ b/docs/backend-system/core-services/auth.md @@ -5,4 +5,136 @@ sidebar_label: Auth description: Documentation for the Auth service --- -TODO +This service deals with the generation and verification of tokens and their +associated representations as credentials objects. You can use it for validating +incoming tokens, and generating tokens for calling other services. + +If you want to deal with credentials specifically in the HTTP request/response +flow, see also [the `httpAuth` service](./http-auth.md). If you want to extract +more details about authenticated users such as their ownership entity refs, use +[the `userInfo` service](./user-info.md). + +## Using the Service + +In the following code examples, the `auth` and `httpAuth` variables are assumed +to be dependency-injected instances of the `coreServices.auth` and +`coreServices.httpAuth` service, respectively. For a backend plugin, it might +look like this: + +```ts +export default createBackendPlugin({ + pluginId: 'my-plugin', + register(env) { + env.registerInit({ + deps: { + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + }, + async init({ auth, httpAuth, httpRouter }) { + // Your code goes here + }, + }); + }, +}); +``` + +### Creating Request Tokens + +If you need to create a token that can be used for making a request to another +backend plugin: + +```ts +const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await auth.getOwnServiceCredentials(), + targetPluginId: 'catalog', +}); +``` + +:::note Note + +Never store and reuse tokens. Always call `getPluginRequestToken` immediately +before making a request. Otherwise you run the risk of running into permission +problems when expired tokens are being used for requests. + +::: + +This example is suitable when you need to make the request "as your own plugin", +i.e. when your code is the original initiator of the call. An example of this +could be periodic batch processes that index content in another service. + +In situations where you are making a call on-behalf-of someone else, for example +when making upstream requests inside a request handler, please always instead +use the extracted credentials from the request. + +```ts +router.get('/makes-calls', async (req, res) => { + const { token } = await auth.getPluginRequestToken({ + onBehalfOf: await httpAuth.credentials(req), + targetPluginId: 'catalog', + }); + // make a call using the token +``` + +This ensures that the original caller and their associated permissions are +properly carried along with the request chain. See [the `httpAuth` service docs](./http-auth.md) +for more details. + +The [service to service auth docs](../../auth/service-to-service-auth.md) +contain more details about how to properly use tokens in your HTTP request +paths. + +### Authorizing Tokens + +Most plugins should not deal with incoming request tokens directly at all, but +rather use [`httpAuth.credentials`](./http-auth.md) instead as part of their +request handlers. But in the rare cases where you are holding an incoming token +and want to validate it and turn it into a credentials object, you can do so: + +```ts +const credentials = await auth.authenticate(token); +``` + +There is an optional second parameter that you can set to `{ allowLimitedAccess: +true }` if you specifically built a plugin that deals with cookie based access, +which is rare. + +### Inspecting Credentials + +The `auth` service also contains facilities for working with credentials +objects. For example checking what type of principal (caller type - e.g. user or +service) they represent. For example: + +```ts +if (auth.isPrincipal(credentials, 'user)) { + // In here, the TypeScript type of the credentials object has been properly + // narrowed to `BackstageCredentials` so you can + // access its specific properties such as `credentials.principal.userEntityRef`. +} +``` + +## Configuring the service + +:::note Note + +The `auth` service is not suitable for having its implementation replaced +entirely in your private repo. If you desire additional service auth related +features, don't hesitate to [file an issue](https://github.com/backstage/backstage/issues/new/choose) +or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) to the open source features. + +::: + +For configuring service-to-service access methods, see [the auth docs](../../auth/service-to-service-auth.md). + +The default auth policy requires all requests to be authenticated with either +user or service credentials. This can be disabled by setting the +`backend.auth.dangerouslyDisableDefaultAuthPolicy` app-config flag to `true`. +Disabling this check means that the backend will no longer block unauthenticated +requests, but instead allow them to pass through to plugins. Do not do this in +production unless absolutely necessary. + +If permissions are enabled, unauthenticated requests will be treated exactly as +such, leaving it to the permission policy to determine what permissions should +be allowed for an unauthenticated identity. Note that this will also apply to +service-to-service calls between plugins unless you configure credentials for +service calls. diff --git a/docs/backend-system/core-services/http-auth.md b/docs/backend-system/core-services/http-auth.md index 7af30c3251..05a26fc075 100644 --- a/docs/backend-system/core-services/http-auth.md +++ b/docs/backend-system/core-services/http-auth.md @@ -5,4 +5,102 @@ sidebar_label: Http Auth description: Documentation for the Http Auth service --- -TODO +The `httpAuth` service deals with submitting and receiving credentials on Express +HTTP request/response objects. This service is frequently used in plugins that +have REST interfaces. + +If you want to deal with raw tokens and do low level credentials handling, see +also [the `auth` service](./auth.md). If you want to extract more details about +authenticated users such as their ownership entity refs, use [the `userInfo` service](./user-info.md). + +## Using the Service + +In the following code examples, the `auth` and `httpAuth` variables are assumed +to be dependency-injected instances of the `coreServices.auth` and +`coreServices.httpAuth` service, respectively. For a backend plugin, it might +look like this: + +```ts +export default createBackendPlugin({ + pluginId: 'my-plugin', + register(env) { + env.registerInit({ + deps: { + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + }, + async init({ auth, httpAuth, httpRouter }) { + // Your code goes here + }, + }); + }, +}); +``` + +### Getting Request Credentials + +If you need to extract the validated credentials out of an incoming request, you +can do so like this: + +```ts +router.get('/some-request', async (req, res) => { + const credentials = await httpAuth.credentials(req, { allow: ['user'] }); + // Do something with the credentials here +``` + +The second argument is optional, but in this example we specified that we only +want to allow user based requests. The credentials returned will then have a +narrowed TypeScript type that reflects that the principal is known to be of the +user type. This second argument can also specify `allowLimitedAccess: true` if +you specifically built a plugin that deals with cookie based access, which is +rare. + +The default is to accept both service and user credentials (excluding limited +access), but in the example above, any attempt to call this endpoint with +service credentials will result in an Unauthorized error being thrown. + +:::note Note + +You don't need to call `httpAuth.credentials` _just_ to ensure that incoming +credentials are valid in the first place; only use this method if you actually +need to act upon the credentials somehow. The Backstage backend framework will have +ensured the actual validity of any incoming token before your backend code is +reached. The policy for these upfront framework level rules is controlled using +[the `httpRouter` service](./http-router.md) when you register your routes. + +::: + +If you want to further work with the credentials object, [the `auth` service](./auth.md) +has helper methods for that. + +### Issuing Cookies + +For some rare use cases, plugins may want to issue cookies with _limited access_ +user credentials. This is mostly relevant when browsers need to be able to +request static resources, such as in the TechDocs plugin. + +Plugins should almost never interact with the cookie functionality of the +`httpAuth` service directly. The framework has builtin handling of cookie +creation/deletion requests on a dedicated well-known endpoint. All you normally +have to do to accept limited user access is to inform [the `httpRouter` service](./http-router.md) +when creating your route that you want to permit +cookie based access for a specific route, and then setting `allowLimitedAccess` +to `true` when extracting credentials. + +Due to the above, we do not document the `httpAuth.issueUserCookie` method here. + +## Configuring the service + +:::note Note + +The `httpAuth` service is not suitable for having its implementation replaced +entirely in your private repo. If you desire additional service auth related +features, don't hesitate to [file an issue](https://github.com/backstage/backstage/issues/new/choose) +or [contribute](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) to the open source features. + +::: + +This service has no configuration options, but it abides by the policies you +have set up using [the `httpRouter` service](./http-router.md) for your routes, +if any. diff --git a/docs/backend-system/core-services/http-router.md b/docs/backend-system/core-services/http-router.md index 4dfa7d80e1..6ab8218c83 100644 --- a/docs/backend-system/core-services/http-router.md +++ b/docs/backend-system/core-services/http-router.md @@ -5,7 +5,8 @@ sidebar_label: Http Router description: Documentation for the Http Router service --- -One of the most common services is the HTTP router service which is used to expose HTTP endpoints for other plugins to consume. +One of the most common services is the HTTP router service which is used to +expose HTTP endpoints for other plugins to consume. ## Using the service @@ -37,6 +38,36 @@ createBackendPlugin({ }); ``` +This service is also responsible for keeping track of the auth policies that +apply to your routes. The default policy is to require that auth is present with +every incoming request, and to accept both service and user credentials +(excluding limited access tokens). You can override this while registering your +routes. This dangerously allows unauthenticated access on a specific route: + +```ts +http.addAuthPolicy({ + path: '/static/:id', + allow: 'unauthenticated', +}); +``` + +Note that the path is exactly the same format as what you used in your routes, +including placeholders. + +If your plugin uses cookie based access (which is rare), you need to allow that +as follows: + +```ts +http.addAuthPolicy({ + path: '/static/:id', + allow: 'user-cookie', +}); +``` + +For those routes you will also have to specify `allowLimitedAccess: true` when +using the [`auth`](./auth.md) and [`httpAuth`](./http-auth.md) services to +access the incoming credentials. + ## Configuring the service This service does not have any configuration options. diff --git a/docs/backend-system/core-services/user-info.md b/docs/backend-system/core-services/user-info.md index 5ca717ddd5..e9d17f1dd7 100644 --- a/docs/backend-system/core-services/user-info.md +++ b/docs/backend-system/core-services/user-info.md @@ -5,4 +5,63 @@ sidebar_label: User Info description: Documentation for the User Info service --- -TODO +This service lets you extract more information about a set of user credentials. +Specifically, it can be used to extract the ownership entity refs for a user +principal. + +See also the [`auth`](./auth.md) and [`httpAuth`](./http-auth.md) services for +general credentials handling. + +## Using the Service + +In the following code examples, the `userInfo`, `auth`, and `httpAuth` variables are assumed +to be dependency-injected instances of the `coreServices.userInfo` and +`coreServices.httpAuth` service, respectively. For a backend plugin, it might +look like this: + +```ts +export default createBackendPlugin({ + pluginId: 'my-plugin', + register(env) { + env.registerInit({ + deps: { + auth: coreServices.auth, + httpAuth: coreServices.httpAuth, + httpRouter: coreServices.httpRouter, + userInfo: coreServices.userInfo, + }, + async init({ auth, httpAuth, httpRouter, userInfo }) { + // Your code goes here + }, + }); + }, +}); +``` + +### Getting User Info + +This example extracts some user credentials out of a request and fetches +additional information about that principal. + +```ts +router.get('/some-request', async (req, res) => { + const credentials = await httpAuth.credentials(req, { allow: ['user'] }); + const info = await userInfo.getUserInfo(credentials); +``` + +The `userInfo` service only deals with credentials that contain user principals, +it won't accept requests for service principals. In our example code the initial +credentials extraction limits it to user credentials upfront. If you have an +endpoint that allows both user and service credentials, you may want to wrap +your user info extraction in a principal type check: + +```ts +router.get('/some-request', async (req, res) => { + const credentials = await httpAuth.credentials(req); + if (auth.isPrincipal(credentials, 'user')) { + const info = await userInfo.getUserInfo(credentials); + // ... +``` + +The user info contains data that was extracted during sign-in for the given +user. diff --git a/docs/tooling/local-dev/linking-local-packages.md b/docs/tooling/local-dev/linking-local-packages.md index 40b722e302..eb2a2cba37 100644 --- a/docs/tooling/local-dev/linking-local-packages.md +++ b/docs/tooling/local-dev/linking-local-packages.md @@ -4,34 +4,68 @@ title: Linking in Local Packages description: How to link in other local packages into your Backstage monorepo --- -It can often be useful to try out changes to the packages in the main Backstage -repo within your own app. For example if you want to make modifications to -`@backstage/core-plugin-api` and try them out in your app. +## Why? -To link in external packages, add them to your `package.json` and `lerna.json` -workspace paths. These can be either relative or absolute paths with or without -globs. For example: +If you are looking to make changes within the core Backstage repository and test +those changes within your Backstage application, you will need to link the two +together: -```json +```text +~/backstage // cloned from Github +~/my-backstage-application // generated using npx +``` + +For example, you might want to make modifications to `@backstage/core-plugin-api` and try them out in your company's +instance of Backstage. + +## Linking in Backstage NPM Packages + +To link in external packages, add them to your root `package.json` and `lerna.json` +`"workspace"` paths. These can be either relative or absolute paths with or without +globs. + +For example: + +```json title="/lerna.json" +... "packages": [ "packages/*", "plugins/*", "../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api ], +... ``` -Then reinstall packages to make yarn set up symlinks: +```json title="/package.json" +... +"workspaces": { + "packages": [ + "packages/*", + "plugins/*", + "../backstage/packages/core-plugin-api", // New path added to work on @backstage/core-plugin-api + ], +} +... +``` + +Now reinstall all packages from the root to make yarn set up symlinks from your application to the core Backstage clone: ```bash yarn install ``` +## Making Backstage Changes + With this in place you can now modify the `@backstage/core-plugin-api` package within the main repo, and have those changes be reflected and tested in your app. Simply run your app using `yarn dev` (or `yarn start` for just frontend) as normal. -Note that for backend packages you need to make sure that linked packages are +## Common Problems + +### Backend Issues + +For backend packages you need to make sure that linked packages are not dependencies of any non-linked package. If you for example want to work on `@backstage/backend-common`, you need to also link in other backend plugins and packages that depend on `@backstage/backend-common`, or temporarily disable @@ -39,6 +73,8 @@ those plugins in your backend. This is because the transformation of backend module tree stops whenever a non-local package is encountered, and from that point node will `require` packages directly for that entire module subtree. +### Typescript Issues + Type checking can also have issues when linking in external packages, since the linked in packages will use the types in the external project and dependency version mismatches between the two projects may cause errors. To fix any of @@ -54,3 +90,17 @@ invalidate correctly for the linked in packages, causing type checking to not reflect changes made to types. You can work around this by either setting `compilerOptions.incremental = false` in `tsconfig.json`, or by deleting the types cache folder `dist-types` before running `yarn tsc`. + +### Version Issues + +While `yarn install` might not error, it does not mean that the linking worked properly. +You will know that linking worked properly when: + +1. Your Backstage application root `/node_modules/@backstage/[some package]` is a symlink +2. Your Backstage application `/packages/app/node_modules` and `/packages/backend/node_modules` does + not contain the package you are attempting to link! + +If you see Yarn continuing to download the package you are trying to link from NPM, you might need to be +explicit in your `package.json` version so that it exactly matches what you have in the cloned Backstage +repository on your machine. For example, if you have cloned `/plugins/catalog` with version +`"version": "1.19.1-next.1"` you will need to be explicit in your application to point to `"1.19.1-next.1"`. diff --git a/microsite/blog/2020-05-14-tech-radar-plugin.mdx b/microsite/blog/2020-05-14-tech-radar-plugin.mdx index 457a03d488..511e764ce3 100644 --- a/microsite/blog/2020-05-14-tech-radar-plugin.mdx +++ b/microsite/blog/2020-05-14-tech-radar-plugin.mdx @@ -22,7 +22,7 @@ We also assign clear definitions for each lifecycle: - **Use:** This technology is recommended for use by the majority of teams with a specific use case. - **Trial:** This technology has been evaluated for specific use cases and has showed clear benefits. Some teams adopt it in production, although it should be limited to low-impact projects as it might incur a higher risk. - **Assess:** This technology has the potential to be beneficial for the company. Some teams are evaluating it and using it in experimental projects. Using it in production comes with a high cost and risk due to lack of in-house knowledge, maintenance, and support. -- **Hold:** We don’t want to further invest in this technology or we evaluated it and we don’t see it as beneficial for the company. Teams should not use it in new projects and should plan on migrating to a supported alternative if they use it for historical reasons. For broadly adopted technologies, the Radar should refer to a migration path to a supported alternative. +- **Hold:** We don't want to further invest in this technology or we evaluated it and we don't see it as beneficial for the company. Teams should not use it in new projects and should plan on migrating to a supported alternative if they use it for historical reasons. For broadly adopted technologies, the Radar should refer to a migration path to a supported alternative. Since rolling out the Tech Radar, it has become the source of truth when creating, maintaining, or evolving our software ecosystem. Spotify has dozens of entries in our Radar and it can scale quite well whilst being easy for our engineers and engineering managers to consume. @@ -32,13 +32,13 @@ To learn about how you can bring the Tech Radar to your Backstage installation, ## A personal note -I want to thank both the Backstage team and Spotify. Firstly, I’ve been working with our internal version of Backstage for over a year, and the developer experience since open sourcing has been even more of a joy to work with. Secondly, the 10% hack time that Spotify generously provides to all engineers enabled me to open source the Tech Radar plugin. +I want to thank both the Backstage team and Spotify. Firstly, I've been working with our internal version of Backstage for over a year, and the developer experience since open sourcing has been even more of a joy to work with. Secondly, the 10% hack time that Spotify generously provides to all engineers enabled me to open source the Tech Radar plugin. Since open sourcing it, the community has shown great interest in yet another powerful use case of Backstage. There was also an enthusiastic open source contributor who volunteered to migrate the plugin to TypeScript and React Hooks [in just 29 minutes](https://github.com/backstage/backstage/issues/661) of opening the issue! -I can’t wait to see how others benefit from the Tech Radar in their organizations! +I can't wait to see how others benefit from the Tech Radar in their organizations! [lighthouse website audits]: https://backstage.io/blog/2020/04/06/lighthouse-plugin -[tech radar plugin]: https://github.com/backstage/backstage/tree/master/plugins/tech-radar +[tech radar plugin]: https://github.com/backstage/community-plugins/tree/main/workspaces/tech-radar/plugins/tech-radar [thoughtworks]: https://www.thoughtworks.com/radar [zalando]: https://opensource.zalando.com/tech-radar/ diff --git a/microsite/data/plugins/kong-service-manager.yaml b/microsite/data/plugins/kong-service-manager.yaml new file mode 100644 index 0000000000..78ff37e36e --- /dev/null +++ b/microsite/data/plugins/kong-service-manager.yaml @@ -0,0 +1,16 @@ +--- +title: VeeCode Kong Service Manager +author: VeeCode Platform +authorUrl: https://platform.vee.codes/ +category: api-manager +description: The Kong Service Manager plugin provides information about the kong service, a list of the routes it has and also offers the possibility of manipulating plugins without leaving the backstage. +documentation: https://platform.vee.codes/plugin/Kong%20Service%20Manager/ +iconUrl: https://veecode-platform.github.io/support/imgs/logo_3.svg +npmPackageName: '@veecode-platform/plugin-kong-service-manager' +tags: + - kong + - api-manager + - services + - routes + - plugins +addedDate: '2024-05-27' diff --git a/microsite/docusaurus.config.ts b/microsite/docusaurus.config.ts index 75a67406b6..9e2a70e74e 100644 --- a/microsite/docusaurus.config.ts +++ b/microsite/docusaurus.config.ts @@ -175,6 +175,26 @@ const config: Config = { from: '/docs/features/software-templates/authorizing-parameters-steps-and-actions', to: '/docs/features/software-templates/authorizing-scaffolder-template-details', }, + { + from: '/docs/local-dev/cli-commands/', + to: '/docs/tooling/cli/commands/', + }, + { + from: '/docs/local-dev/cli-build-system/', + to: '/docs/tooling/cli/build-system/', + }, + { + from: '/docs/local-dev/cli-overview/', + to: '/docs/tooling/cli/overview/', + }, + { + from: '/docs/local-dev/linking-local-packages/', + to: '/docs/tooling/local-dev/linking-local-packages', + }, + { + from: '/docs/local-dev/debugging/', + to: '/docs/tooling/local-dev/debugging', + }, ], }, ], diff --git a/packages/backend-defaults/api-report-scheduler.md b/packages/backend-defaults/api-report-scheduler.md index a2dad43d60..58991ddca3 100644 --- a/packages/backend-defaults/api-report-scheduler.md +++ b/packages/backend-defaults/api-report-scheduler.md @@ -3,9 +3,20 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { DatabaseService } from '@backstage/backend-plugin-api'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; +// @public +export class DefaultSchedulerService { + // (undocumented) + static create(options: { + database: DatabaseService; + logger: LoggerService; + }): SchedulerService; +} + // @public export const schedulerServiceFactory: () => ServiceFactory< SchedulerService, diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/index.ts b/packages/backend-defaults/src/entrypoints/database/connectors/index.ts index 9d18e7460a..117f727825 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/index.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ -export * from './mysql'; -export * from './postgres'; -export * from './sqlite3'; +export { MysqlConnector } from './mysql'; +export { PgConnector } from './postgres'; +export { Sqlite3Connector } from './sqlite3'; diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/mysql.ts b/packages/backend-defaults/src/entrypoints/database/connectors/mysql.ts index 2fb3545047..b11b659a6d 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/mysql.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/mysql.ts @@ -25,7 +25,7 @@ import knexFactory, { Knex } from 'knex'; import { merge, omit } from 'lodash'; import limiterFactory from 'p-limit'; import yn from 'yn'; -import { Connector, DatabaseConnector } from '../types'; +import { Connector } from '../types'; import defaultNameOverride from './defaultNameOverride'; import { mergeDatabaseConfig } from './mergeDatabaseConfig'; @@ -226,19 +226,6 @@ export async function dropMysqlDatabase( } } -/** - * MySQL database connector. - * - * Exposes database connector functionality via an immutable object. - */ -export const mysqlConnector: DatabaseConnector = Object.freeze({ - createClient: createMysqlDatabaseClient, - ensureDatabaseExists: ensureMysqlDatabaseExists, - createNameOverride: defaultNameOverride, - parseConnectionString: parseMysqlConnectionString, - dropDatabase: dropMysqlDatabase, -}); - /** * Provides a config lookup path for a plugin's config block. */ @@ -248,31 +235,16 @@ function pluginPath(pluginId: string): string { function normalizeConnection( connection: Knex.StaticConnectionConfig | JsonObject | string | undefined, - client: string, ): Partial { if (typeof connection === 'undefined' || connection === null) { return {}; } return typeof connection === 'string' || connection instanceof String - ? mysqlConnector.parseConnectionString(connection as string, client) + ? parseMysqlConnectionString(connection as string) : connection; } -function createNameOverride( - client: string, - name: string, -): Partial { - try { - return mysqlConnector.createNameOverride(name); - } catch (e) { - throw new InputError( - `Unable to create database name override for '${client}' connector`, - e, - ); - } -} - export class MysqlConnector implements Connector { constructor( private readonly config: Config, @@ -281,7 +253,7 @@ export class MysqlConnector implements Connector { async getClient( pluginId: string, - deps?: { + _deps?: { lifecycle: LifecycleService; pluginMetadata: PluginMetadataService; }, @@ -293,7 +265,7 @@ export class MysqlConnector implements Connector { const databaseName = this.getDatabaseName(pluginId); if (databaseName && this.getEnsureExistsConfig(pluginId)) { try { - await mysqlConnector.ensureDatabaseExists!(pluginConfig, databaseName); + await ensureMysqlDatabaseExists(pluginConfig, databaseName); } catch (error) { throw new Error( `Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`, @@ -313,10 +285,9 @@ export class MysqlConnector implements Connector { this.getDatabaseOverrides(pluginId), ); - const client = mysqlConnector.createClient( + const client = createMysqlDatabaseClient( pluginConfig, databaseClientOverrides, - deps, ); return client; @@ -418,12 +389,9 @@ export class MysqlConnector implements Connector { * unless `pluginDivisionMode` is set to `schema`. */ private getConnectionConfig(pluginId: string): Knex.StaticConnectionConfig { - const { client, overridden } = this.getClientType(pluginId); + const { overridden } = this.getClientType(pluginId); - let baseConnection = normalizeConnection( - this.config.get('connection'), - this.config.getString('client'), - ); + let baseConnection = normalizeConnection(this.config.get('connection')); // Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`. The // `database` property from the base connection is omitted unless `pluginDivisionMode` @@ -435,7 +403,6 @@ export class MysqlConnector implements Connector { // get and normalize optional plugin specific database connection const connection = normalizeConnection( this.config.getOptional(`${pluginPath(pluginId)}.connection`), - client, ); return { @@ -473,8 +440,6 @@ export class MysqlConnector implements Connector { */ private getDatabaseOverrides(pluginId: string): Knex.Config { const databaseName = this.getDatabaseName(pluginId); - return databaseName - ? createNameOverride(this.getClientType(pluginId).client, databaseName) - : {}; + return databaseName ? defaultNameOverride(databaseName) : {}; } } diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts index b81bfd5b50..444bbc0d18 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/postgres.ts @@ -19,13 +19,13 @@ import { PluginMetadataService, } from '@backstage/backend-plugin-api'; import { Config, ConfigReader } from '@backstage/config'; -import { ForwardedError, InputError } from '@backstage/errors'; +import { ForwardedError } from '@backstage/errors'; import { JsonObject } from '@backstage/types'; import knexFactory, { Knex } from 'knex'; import { merge, omit } from 'lodash'; import limiterFactory from 'p-limit'; import { Client } from 'pg'; -import { Connector, DatabaseConnector } from '../types'; +import { Connector } from '../types'; import defaultNameOverride from './defaultNameOverride'; import defaultSchemaOverride from './defaultSchemaOverride'; import { mergeDatabaseConfig } from './mergeDatabaseConfig'; @@ -232,16 +232,6 @@ export async function dropPgDatabase( } } -export const pgConnector: DatabaseConnector = Object.freeze({ - createClient: createPgDatabaseClient, - ensureDatabaseExists: ensurePgDatabaseExists, - ensureSchemaExists: ensurePgSchemaExists, - createNameOverride: defaultNameOverride, - createSchemaOverride: defaultSchemaOverride, - parseConnectionString: parsePgConnectionString, - dropDatabase: dropPgDatabase, -}); - /** * Provides a config lookup path for a plugin's config block. */ @@ -251,45 +241,16 @@ function pluginPath(pluginId: string): string { function normalizeConnection( connection: Knex.StaticConnectionConfig | JsonObject | string | undefined, - client: string, ): Partial { if (typeof connection === 'undefined' || connection === null) { return {}; } return typeof connection === 'string' || connection instanceof String - ? pgConnector.parseConnectionString(connection as string, client) + ? parsePgConnectionString(connection as string) : connection; } -function createSchemaOverride( - client: string, - name: string, -): Partial { - try { - return pgConnector.createSchemaOverride?.(name); - } catch (e) { - throw new InputError( - `Unable to create database schema override for '${client}' connector`, - e, - ); - } -} - -function createNameOverride( - client: string, - name: string, -): Partial { - try { - return pgConnector.createNameOverride(name); - } catch (e) { - throw new InputError( - `Unable to create database name override for '${client}' connector`, - e, - ); - } -} - export class PgConnector implements Connector { constructor( private readonly config: Config, @@ -298,7 +259,7 @@ export class PgConnector implements Connector { async getClient( pluginId: string, - deps?: { + _deps?: { lifecycle: LifecycleService; pluginMetadata: PluginMetadataService; }, @@ -310,7 +271,7 @@ export class PgConnector implements Connector { const databaseName = this.getDatabaseName(pluginId); if (databaseName && this.getEnsureExistsConfig(pluginId)) { try { - await pgConnector.ensureDatabaseExists!(pluginConfig, databaseName); + await ensurePgDatabaseExists(pluginConfig, databaseName); } catch (error) { throw new Error( `Failed to connect to the database to make sure that '${databaseName}' exists, ${error}`, @@ -320,13 +281,13 @@ export class PgConnector implements Connector { let schemaOverrides; if (this.getPluginDivisionModeConfig() === 'schema') { - schemaOverrides = this.getSchemaOverrides(pluginId); + schemaOverrides = defaultSchemaOverride(pluginId); if ( this.getEnsureSchemaExistsConfig(pluginId) || this.getEnsureExistsConfig(pluginId) ) { try { - await pgConnector.ensureSchemaExists!(pluginConfig, pluginId); + await ensurePgSchemaExists(pluginConfig, pluginId); } catch (error) { throw new Error( `Failed to connect to the database to make sure that schema for plugin '${pluginId}' exists, ${error}`, @@ -341,10 +302,9 @@ export class PgConnector implements Connector { schemaOverrides, ); - const client = pgConnector.createClient( + const client = createPgDatabaseClient( pluginConfig, databaseClientOverrides, - deps, ); return client; @@ -465,12 +425,9 @@ export class PgConnector implements Connector { * unless `pluginDivisionMode` is set to `schema`. */ private getConnectionConfig(pluginId: string): Knex.StaticConnectionConfig { - const { client, overridden } = this.getClientType(pluginId); + const { overridden } = this.getClientType(pluginId); - let baseConnection = normalizeConnection( - this.config.get('connection'), - this.config.getString('client'), - ); + let baseConnection = normalizeConnection(this.config.get('connection')); // Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`. The // `database` property from the base connection is omitted unless `pluginDivisionMode` @@ -482,7 +439,6 @@ export class PgConnector implements Connector { // get and normalize optional plugin specific database connection const connection = normalizeConnection( this.config.getOptional(`${pluginPath(pluginId)}.connection`), - client, ); ( @@ -516,17 +472,6 @@ export class PgConnector implements Connector { }; } - /** - * Provides a partial `Knex.Config` database schema override for a given - * plugin. - * - * @param pluginId - Target plugin to get database schema override - * @returns Partial `Knex.Config` with database schema override - */ - private getSchemaOverrides(pluginId: string): Knex.Config | undefined { - return createSchemaOverride(this.getClientType(pluginId).client, pluginId); - } - /** * Provides a partial `Knex.Config`• database name override for a given plugin. * @@ -535,8 +480,6 @@ export class PgConnector implements Connector { */ private getDatabaseOverrides(pluginId: string): Knex.Config { const databaseName = this.getDatabaseName(pluginId); - return databaseName - ? createNameOverride(this.getClientType(pluginId).client, databaseName) - : {}; + return databaseName ? defaultNameOverride(databaseName) : {}; } } diff --git a/packages/backend-defaults/src/entrypoints/database/connectors/sqlite3.ts b/packages/backend-defaults/src/entrypoints/database/connectors/sqlite3.ts index 46ff756854..f7d55b34da 100644 --- a/packages/backend-defaults/src/entrypoints/database/connectors/sqlite3.ts +++ b/packages/backend-defaults/src/entrypoints/database/connectors/sqlite3.ts @@ -20,13 +20,12 @@ import { PluginMetadataService, } from '@backstage/backend-plugin-api'; import { Config, ConfigReader } from '@backstage/config'; -import { InputError } from '@backstage/errors'; import { JsonObject } from '@backstage/types'; import { ensureDirSync } from 'fs-extra'; import knexFactory, { Knex } from 'knex'; import { merge, omit } from 'lodash'; import path from 'path'; -import { Connector, DatabaseConnector } from '../types'; +import { Connector } from '../types'; import { mergeDatabaseConfig } from './mergeDatabaseConfig'; /** @@ -157,17 +156,6 @@ export function parseSqliteConnectionString( }; } -/** - * SQLite3 database connector. - * - * Exposes database connector functionality via an immutable object. - */ -export const sqliteConnector: DatabaseConnector = Object.freeze({ - createClient: createSqliteDatabaseClient, - createNameOverride: createSqliteNameOverride, - parseConnectionString: parseSqliteConnectionString, -}); - /** * Provides a config lookup path for a plugin's config block. */ @@ -177,31 +165,16 @@ function pluginPath(pluginId: string): string { function normalizeConnection( connection: Knex.StaticConnectionConfig | JsonObject | string | undefined, - client: string, ): Partial { if (typeof connection === 'undefined' || connection === null) { return {}; } return typeof connection === 'string' || connection instanceof String - ? sqliteConnector.parseConnectionString(connection as string, client) + ? parseSqliteConnectionString(connection as string) : connection; } -function createNameOverride( - client: string, - name: string, -): Partial { - try { - return sqliteConnector.createNameOverride(name); - } catch (e) { - throw new InputError( - `Unable to create database name override for '${client}' connector`, - e, - ); - } -} - export class Sqlite3Connector implements Connector { constructor(private readonly config: Config) {} @@ -228,7 +201,7 @@ export class Sqlite3Connector implements Connector { this.getDatabaseOverrides(pluginId), ); - const client = sqliteConnector.createClient( + const client = createSqliteDatabaseClient( pluginConfig, databaseClientOverrides, deps, @@ -340,10 +313,7 @@ export class Sqlite3Connector implements Connector { private getConnectionConfig(pluginId: string): Knex.StaticConnectionConfig { const { client, overridden } = this.getClientType(pluginId); - let baseConnection = normalizeConnection( - this.config.get('connection'), - this.config.getString('client'), - ); + let baseConnection = normalizeConnection(this.config.get('connection')); if ( client.includes('sqlite3') && @@ -366,7 +336,6 @@ export class Sqlite3Connector implements Connector { // get and normalize optional plugin specific database connection const connection = normalizeConnection( this.config.getOptional(`${pluginPath(pluginId)}.connection`), - client, ); return { @@ -404,8 +373,6 @@ export class Sqlite3Connector implements Connector { */ private getDatabaseOverrides(pluginId: string): Knex.Config { const databaseName = this.getDatabaseName(pluginId); - return databaseName - ? createNameOverride(this.getClientType(pluginId).client, databaseName) - : {}; + return databaseName ? createSqliteNameOverride(databaseName) : {}; } } diff --git a/packages/backend-defaults/src/entrypoints/scheduler/index.ts b/packages/backend-defaults/src/entrypoints/scheduler/index.ts index 77d4ea561d..dff53a19b4 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/index.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/index.ts @@ -14,4 +14,5 @@ * limitations under the License. */ +export { DefaultSchedulerService } from './lib/DefaultSchedulerService'; export { schedulerServiceFactory } from './schedulerServiceFactory'; diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.test.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts similarity index 77% rename from packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.test.ts rename to packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts index d286eaee38..63b900fab2 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.test.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.test.ts @@ -14,37 +14,38 @@ * limitations under the License. */ -import { DatabaseManager, getVoidLogger } from '@backstage/backend-common'; -import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { + TestDatabaseId, + TestDatabases, + mockServices, +} from '@backstage/backend-test-utils'; import { Duration } from 'luxon'; import waitForExpect from 'wait-for-expect'; -import { TaskScheduler } from './TaskScheduler'; +import { DefaultSchedulerService } from './DefaultSchedulerService'; import { createTestScopedSignal } from './__testUtils__/createTestScopedSignal'; +import { DatabaseService } from '@backstage/backend-plugin-api'; jest.setTimeout(60_000); describe('TaskScheduler', () => { - const logger = getVoidLogger(); + const logger = mockServices.logger.mock(); const databases = TestDatabases.create(); const testScopedSignal = createTestScopedSignal(); async function createDatabase( databaseId: TestDatabaseId, - ): Promise { + ): Promise { const knex = await databases.init(databaseId); - const databaseManager: Partial = { - forPlugin: () => ({ - getClient: async () => knex, - }), + return { + getClient: async () => knex, }; - return databaseManager as DatabaseManager; } it.each(databases.eachSupportedId())( 'can return a working v1 plugin impl, %p', async databaseId => { const database = await createDatabase(databaseId); - const manager = new TaskScheduler(database, logger).forPlugin('test'); + const manager = DefaultSchedulerService.create({ database, logger }); const fn = jest.fn(); await manager.scheduleTask({ @@ -65,7 +66,7 @@ describe('TaskScheduler', () => { 'can return a working v2 plugin impl, %p', async databaseId => { const database = await createDatabase(databaseId); - const manager = new TaskScheduler(database, logger).forPlugin('test'); + const manager = DefaultSchedulerService.create({ database, logger }); const fn = jest.fn(); await manager.scheduleTask({ diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts new file mode 100644 index 0000000000..8e2ac5cd07 --- /dev/null +++ b/packages/backend-defaults/src/entrypoints/scheduler/lib/DefaultSchedulerService.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DatabaseService, + LoggerService, + SchedulerService, +} from '@backstage/backend-plugin-api'; +import { once } from 'lodash'; +import { Duration } from 'luxon'; +import { migrateBackendTasks } from '../database/migrateBackendTasks'; +import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; +import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; + +/** + * Default implementation of the task scheduler service. + * + * @public + */ +export class DefaultSchedulerService { + static create(options: { + database: DatabaseService; + logger: LoggerService; + }): SchedulerService { + const databaseFactory = once(async () => { + const knex = await options.database.getClient(); + + if (!options.database.migrations?.skip) { + await migrateBackendTasks(knex); + } + + if (process.env.NODE_ENV !== 'test') { + const janitor = new PluginTaskSchedulerJanitor({ + knex, + waitBetweenRuns: Duration.fromObject({ minutes: 1 }), + logger: options.logger, + }); + janitor.start(); + } + + return knex; + }); + + return new PluginTaskSchedulerImpl(databaseFactory, options.logger); + } +} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.ts b/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.ts deleted file mode 100644 index 21a19ce700..0000000000 --- a/packages/backend-defaults/src/entrypoints/scheduler/lib/TaskScheduler.ts +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - DatabaseManager, - getRootLogger, - LegacyRootDatabaseService, - PluginDatabaseManager, -} from '@backstage/backend-common'; -import { - LoggerService, - RootConfigService, - SchedulerService, -} from '@backstage/backend-plugin-api'; -import { once } from 'lodash'; -import { Duration } from 'luxon'; -import { migrateBackendTasks } from '../database/migrateBackendTasks'; -import { PluginTaskSchedulerImpl } from './PluginTaskSchedulerImpl'; -import { PluginTaskSchedulerJanitor } from './PluginTaskSchedulerJanitor'; - -/** - * Deals with the scheduling of distributed tasks. - */ -export class TaskScheduler { - static fromConfig( - config: RootConfigService, - options?: { - databaseManager?: LegacyRootDatabaseService; - logger?: LoggerService; - }, - ): TaskScheduler { - const databaseManager = - options?.databaseManager ?? DatabaseManager.fromConfig(config); - const logger = (options?.logger || getRootLogger()).child({ - type: 'taskManager', - }); - return new TaskScheduler(databaseManager, logger); - } - - constructor( - private readonly databaseManager: LegacyRootDatabaseService, - private readonly logger: LoggerService, - ) {} - - /** - * Instantiates a task manager instance for the given plugin. - * - * @param pluginId - The unique ID of the plugin, for example "catalog" - * @returns A {@link SchedulerService} instance - */ - forPlugin(pluginId: string): SchedulerService { - return TaskScheduler.forPlugin({ - pluginId, - databaseManager: this.databaseManager.forPlugin(pluginId), - logger: this.logger, - }); - } - - static forPlugin(opts: { - pluginId: string; - databaseManager: PluginDatabaseManager; - logger: LoggerService; - }): SchedulerService { - const databaseFactory = once(async () => { - const knex = await opts.databaseManager.getClient(); - - if (!opts.databaseManager.migrations?.skip) { - await migrateBackendTasks(knex); - } - - if (process.env.NODE_ENV !== 'test') { - const janitor = new PluginTaskSchedulerJanitor({ - knex, - waitBetweenRuns: Duration.fromObject({ minutes: 1 }), - logger: opts.logger, - }); - janitor.start(); - } - - return knex; - }); - - return new PluginTaskSchedulerImpl(databaseFactory, opts.logger); - } -} diff --git a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts index 55c0d58e7b..1105c86332 100644 --- a/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/scheduler/schedulerServiceFactory.ts @@ -18,7 +18,7 @@ import { coreServices, createServiceFactory, } from '@backstage/backend-plugin-api'; -import { TaskScheduler } from './lib/TaskScheduler'; +import { DefaultSchedulerService } from './lib/DefaultSchedulerService'; /** * The default service factory for {@link @backstage/backend-plugin-api#coreServices.scheduler}. @@ -28,15 +28,10 @@ import { TaskScheduler } from './lib/TaskScheduler'; export const schedulerServiceFactory = createServiceFactory({ service: coreServices.scheduler, deps: { - plugin: coreServices.pluginMetadata, database: coreServices.database, logger: coreServices.logger, }, - async factory({ plugin, database, logger }) { - return TaskScheduler.forPlugin({ - pluginId: plugin.getId(), - databaseManager: database, - logger, - }); + async factory({ database, logger }) { + return DefaultSchedulerService.create({ database, logger }); }, }); diff --git a/packages/backend-tasks/api-report.md b/packages/backend-tasks/api-report.md index 6e8d6eeb2f..0d5ced044a 100644 --- a/packages/backend-tasks/api-report.md +++ b/packages/backend-tasks/api-report.md @@ -87,8 +87,9 @@ export class TaskScheduler { databaseManager: LegacyRootDatabaseService, logger: LoggerService, ); + // @deprecated forPlugin(pluginId: string): PluginTaskScheduler; - // (undocumented) + // @deprecated (undocumented) static forPlugin(opts: { pluginId: string; databaseManager: PluginDatabaseManager; diff --git a/packages/backend-tasks/src/tasks/TaskScheduler.ts b/packages/backend-tasks/src/tasks/TaskScheduler.ts index 6608ba8e84..ff6fd12017 100644 --- a/packages/backend-tasks/src/tasks/TaskScheduler.ts +++ b/packages/backend-tasks/src/tasks/TaskScheduler.ts @@ -33,7 +33,7 @@ import { LoggerService } from '@backstage/backend-plugin-api'; * Deals with the scheduling of distributed tasks. * * @public - * @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead. + * @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead, or use `DefaultSchedulerService` from `@backstage/backend-defaults` */ export class TaskScheduler { /** @@ -65,6 +65,7 @@ export class TaskScheduler { * * @param pluginId - The unique ID of the plugin, for example "catalog" * @returns A {@link PluginTaskScheduler} instance + * @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead, or use `DefaultSchedulerService` from `@backstage/backend-defaults` */ forPlugin(pluginId: string): PluginTaskScheduler { return TaskScheduler.forPlugin({ @@ -74,6 +75,9 @@ export class TaskScheduler { }); } + /** + * @deprecated Please migrate to the new backend system, and depend on `coreServices.scheduler` from `@backstage/backend-plugin-api` instead, or use `DefaultSchedulerService` from `@backstage/backend-defaults` + */ static forPlugin(opts: { pluginId: string; databaseManager: PluginDatabaseManager; diff --git a/packages/cli-common/src/paths.test.ts b/packages/cli-common/src/paths.test.ts index 5c1cf8af08..5fcef9cadf 100644 --- a/packages/cli-common/src/paths.test.ts +++ b/packages/cli-common/src/paths.test.ts @@ -19,6 +19,10 @@ import { resolve as resolvePath } from 'path'; import { findPaths, findRootPath, findOwnDir, findOwnRootDir } from './paths'; describe('paths', () => { + afterEach(() => { + jest.restoreAllMocks(); + }); + it('findOwnDir and findOwnRootDir should find owns paths', () => { const dir = findOwnDir(__dirname); const root = findOwnRootDir(dir); @@ -92,4 +96,28 @@ describe('paths', () => { resolvePath(root, 'derp.txt'), ); }); + + it('findPaths should find workspace root with object', () => { + jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: { packages: [] } }); + jest.spyOn(process, 'cwd').mockReturnValue(__dirname); + + const paths = findPaths(__dirname); + + expect(paths.targetDir).toBe( + resolvePath(__dirname, '../../cli-common/src'), + ); + expect(paths.targetRoot).toBe(resolvePath(__dirname, '../../cli-common')); + }); + + it('findPaths should find workspace root with array', () => { + jest.spyOn(JSON, 'parse').mockReturnValue({ workspaces: [] }); + jest.spyOn(process, 'cwd').mockReturnValue(__dirname); + + const paths = findPaths(__dirname); + + expect(paths.targetDir).toBe( + resolvePath(__dirname, '../../cli-common/src'), + ); + expect(paths.targetRoot).toBe(resolvePath(__dirname, '../../cli-common')); + }); }); diff --git a/packages/cli-common/src/paths.ts b/packages/cli-common/src/paths.ts index bdd0245c76..762e660140 100644 --- a/packages/cli-common/src/paths.ts +++ b/packages/cli-common/src/paths.ts @@ -141,7 +141,7 @@ export function findPaths(searchDir: string): Paths { try { const content = fs.readFileSync(path, 'utf8'); const data = JSON.parse(content); - return Boolean(data.workspaces?.packages); + return Boolean(data.workspaces); } catch (error) { throw new Error( `Failed to parse package.json file while searching for root, ${error}`, diff --git a/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx b/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx index 37b40ce26b..dd4838bd8d 100644 --- a/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx +++ b/packages/core-components/src/components/OverflowTooltip/OverflowTooltip.tsx @@ -53,7 +53,9 @@ export function OverflowTooltip(props: Props) { title={props.title ?? (props.text || '')} placement={props.placement} > - {props.text} + + {props.text} + ); } diff --git a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx index 6768b48dad..71c04122ae 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/Root/Root.tsx @@ -2,7 +2,6 @@ import React, { PropsWithChildren } from 'react'; import { makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExtensionIcon from '@material-ui/icons/Extension'; -import MapIcon from '@material-ui/icons/MyLocation'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import LogoFull from './LogoFull'; @@ -72,7 +71,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => ( {/* End global nav */} - + {/* Items in this group will be scrollable if they run out of space */} diff --git a/packages/yarn-plugin/.eslintrc.js b/packages/yarn-plugin/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/yarn-plugin/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/yarn-plugin/.gitignore b/packages/yarn-plugin/.gitignore new file mode 100644 index 0000000000..fba89407fd --- /dev/null +++ b/packages/yarn-plugin/.gitignore @@ -0,0 +1,2 @@ +# @yarnpkg/builder build output +bundles/ diff --git a/packages/yarn-plugin/README.md b/packages/yarn-plugin/README.md new file mode 100644 index 0000000000..16507713af --- /dev/null +++ b/packages/yarn-plugin/README.md @@ -0,0 +1,13 @@ +# yarn-plugin-backstage + +This yarn plugin adds a `backstage:` version protocol to yarn, which replaces +specific version ranges for `@backstage/` packages. The only version range +supported by this plugin is `backstage:^`, which has similar semantics to the +corresponding [`workspace` range](https://yarnpkg.com/features/workspaces#cross-references) when using +workspace dependencies; locally, the package will always resolve to the exact +version specified in the manifest for the Backstage release listed in +backstage.json. If the dependent package is published, this version will be +prefixed by `^`. + +**This plugin is still under active development, and requires some further +testing before we recommend it for general use.** diff --git a/packages/yarn-plugin/api-report.md b/packages/yarn-plugin/api-report.md new file mode 100644 index 0000000000..f0300ec9a1 --- /dev/null +++ b/packages/yarn-plugin/api-report.md @@ -0,0 +1,11 @@ +## API Report File for "yarn-plugin-backstage" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Plugin as Plugin_2 } from '@yarnpkg/core'; + +// @public (undocumented) +const plugin: Plugin_2; +export default plugin; +``` diff --git a/packages/yarn-plugin/catalog-info.yaml b/packages/yarn-plugin/catalog-info.yaml new file mode 100644 index 0000000000..c3a9be8fa5 --- /dev/null +++ b/packages/yarn-plugin/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: yarn-plugin-backstage + title: yarn-plugin-backstage + description: Yarn plugin for working with Backstage monorepos +spec: + lifecycle: experimental + type: backstage-node-library + owner: maintainers diff --git a/packages/yarn-plugin/package.json b/packages/yarn-plugin/package.json new file mode 100644 index 0000000000..b6b97c0475 --- /dev/null +++ b/packages/yarn-plugin/package.json @@ -0,0 +1,45 @@ +{ + "name": "yarn-plugin-backstage", + "version": "0.0.0", + "description": "Yarn plugin for working with Backstage monorepos", + "backstage": { + "role": "node-library" + }, + "private": true, + "keywords": [ + "backstage" + ], + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/yarn-plugin" + }, + "license": "Apache-2.0", + "main": "./src/index.ts", + "scripts": { + "build": "builder build plugin", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "start": "nodemon --", + "test": "backstage-cli package test" + }, + "nodemonConfig": { + "exec": "builder build plugin", + "ext": "ts", + "watch": "./src" + }, + "dependencies": { + "@backstage/cli-common": "workspace:^", + "@backstage/release-manifests": "workspace:^", + "@yarnpkg/core": "^4.0.3", + "@yarnpkg/fslib": "^3.0.2", + "semver": "^7.6.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@yarnpkg/builder": "^4.0.0", + "nodemon": "^3.0.1" + } +} diff --git a/packages/yarn-plugin/src/constants.ts b/packages/yarn-plugin/src/constants.ts new file mode 100644 index 0000000000..4473b1e347 --- /dev/null +++ b/packages/yarn-plugin/src/constants.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const PROTOCOL = 'backstage:'; diff --git a/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.test.ts b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.test.ts new file mode 100644 index 0000000000..ca99b88437 --- /dev/null +++ b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.test.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Manifest, Workspace } from '@yarnpkg/core'; +import { npath, ppath } from '@yarnpkg/fslib'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +import { beforeWorkspacePacking } from './beforeWorkspacePacking'; + +jest.mock('@backstage/release-manifests', () => ({ + getManifestByVersion: jest.fn().mockResolvedValue({ + releaseVersion: '1.23.45', + packages: [ + { + name: '@backstage/core', + version: '3.2.1', + }, + ], + }), +})); + +const makeWorkspace = (manifest: object) => { + return { + manifest: Manifest.fromText(JSON.stringify(manifest)), + } as Workspace; +}; + +describe('beforeWorkspacePacking', () => { + const mockDir = createMockDirectory(); + + beforeEach(() => { + jest + .spyOn(ppath, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + jest + .spyOn(process, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + mockDir.setContent({ + 'backstage.json': JSON.stringify({ + version: '1.23.45', + }), + 'package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe.each` + dependencyType + ${'dependencies'} + ${'devDependencies'} + ${'optionalDependencies'} + `('$dependencyType', ({ dependencyType }) => { + it(`ignores ${dependencyType} that don't use the backstage: protocol`, () => { + const result = { + name: 'test-package', + [dependencyType]: { + foo: '^1.1.1', + }, + }; + + beforeWorkspacePacking(makeWorkspace(result), result); + + expect(result).toEqual({ + name: 'test-package', + [dependencyType]: { + foo: '^1.1.1', + }, + }); + }); + + it(`throws an error for any backstage: versions with a selector other than ^`, async () => { + const result = { + name: 'test-package', + [dependencyType]: { + '@backstage/core': 'backstage:^1.1.1', + }, + }; + + await expect(() => + beforeWorkspacePacking(makeWorkspace(result), result), + ).rejects.toThrow(); + }); + + it('converts backstage:^ versions to the corresponding package version prefixed by ^', async () => { + const result = { + name: 'test-package', + [dependencyType]: { + '@backstage/core': 'backstage:^', + }, + }; + + await beforeWorkspacePacking(makeWorkspace(result), result); + + expect(result).toEqual({ + name: 'test-package', + [dependencyType]: { + '@backstage/core': '^3.2.1', + }, + }); + }); + }); +}); diff --git a/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts new file mode 100644 index 0000000000..b804f64226 --- /dev/null +++ b/packages/yarn-plugin/src/handlers/beforeWorkspacePacking.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Descriptor, Workspace, structUtils } from '@yarnpkg/core'; +import { getCurrentBackstageVersion, getPackageVersion } from '../util'; +import { PROTOCOL } from '../constants'; + +const getFinalDependencyType = ( + dependencyType: string, + descriptor: Descriptor, + workspace: Workspace, +) => { + if (dependencyType !== 'dependencies') { + return dependencyType; + } + + return workspace.manifest.ensureDependencyMeta( + structUtils.makeDescriptor(descriptor, 'unknown'), + ).optional + ? 'optionalDependencies' + : dependencyType; +}; + +export const beforeWorkspacePacking = async ( + workspace: Workspace, + rawManifest: any, +) => { + const backstageVersion = getCurrentBackstageVersion(); + + for (const dependencyType of ['dependencies', 'devDependencies'] as const) { + const entries = Array.from( + workspace.manifest.getForScope(dependencyType).values(), + ).filter(descriptor => descriptor.range.startsWith(PROTOCOL)); + + for (const descriptor of entries) { + const ident = structUtils.stringifyIdent(descriptor); + const range = structUtils.parseRange(descriptor.range); + + if (range.selector !== '^') { + throw new Error( + `Unexpected version range "${descriptor.range}" for dependency on "${ident}"`, + ); + } + + const finalDependencyType = getFinalDependencyType( + dependencyType, + descriptor, + workspace, + ); + + rawManifest[finalDependencyType][ident] = `^${await getPackageVersion( + structUtils.makeDescriptor( + descriptor, + `${PROTOCOL}${backstageVersion}`, + ), + )}`; + } + } +}; diff --git a/packages/yarn-plugin/src/index.ts b/packages/yarn-plugin/src/index.ts new file mode 100644 index 0000000000..6c1c805fd8 --- /dev/null +++ b/packages/yarn-plugin/src/index.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Yarn plugin for resolving package versions based on + * a Backstage version manifest. + * + * @packageDocumentation + */ + +import { Plugin } from '@yarnpkg/core'; +import { beforeWorkspacePacking } from './handlers/beforeWorkspacePacking'; +import { BackstageResolver } from './resolver/BackstageResolver'; + +/** + * @public + */ +const plugin: Plugin = { + hooks: { + beforeWorkspacePacking, + }, + resolvers: [BackstageResolver], +}; + +export default plugin; diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts new file mode 100644 index 0000000000..4cc0c1c9f2 --- /dev/null +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.test.ts @@ -0,0 +1,181 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { structUtils } from '@yarnpkg/core'; +import { npath, ppath } from '@yarnpkg/fslib'; +import { BackstageResolver } from './BackstageResolver'; +import { createMockDirectory } from '@backstage/backend-test-utils'; + +jest.mock('@backstage/release-manifests', () => ({ + getManifestByVersion: jest.fn().mockResolvedValue({ + releaseVersion: '1.23.45', + packages: [ + { + name: '@backstage/core', + version: '6.7.8', + }, + ], + }), +})); + +describe('BackstageResolver', () => { + const mockDir = createMockDirectory(); + let backstageResolver: BackstageResolver; + + beforeEach(() => { + jest + .spyOn(ppath, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + jest + .spyOn(process, 'cwd') + .mockReturnValue(npath.toPortablePath(mockDir.path)); + + mockDir.setContent({ + 'backstage.json': JSON.stringify({ + version: '1.23.45', + }), + 'package.json': JSON.stringify({ + workspaces: { + packages: ['packages/*'], + }, + }), + packages: { + a: { + 'package.json': JSON.stringify({ + name: 'a', + dependencies: { + '@backstage/core': 'backstage:^', + }, + }), + }, + }, + }); + + backstageResolver = new BackstageResolver(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe('supportsDescriptor', () => { + it.each([ + ['backstage:^'], + ['backstage:1.26.0-next.3'], + ['backstage:anything'], + ])('returns true for range "%s"', range => { + expect( + backstageResolver.supportsDescriptor( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + range, + ), + ), + ).toEqual(true); + }); + }); + + describe('bindDescriptor', () => { + describe('with range "backstage:^"', () => { + it('returns a descriptor basedwith a version range for the current Backstage version', () => { + expect( + backstageResolver.bindDescriptor( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:^', + ), + ), + ).toEqual( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + ); + }); + }); + + describe('with range "backstage:1.23.45"', () => { + it('throws an error', () => { + expect(() => + backstageResolver.bindDescriptor( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ), + ), + ).toThrow(/unsupported version range/i); + }); + }); + }); + + describe('getCandidates', () => { + it('returns an npm: descriptor based on the manifest for the appropriate backstage version', async () => { + const descriptor = structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:1.23.45', + ); + + await expect( + backstageResolver.getCandidates(descriptor), + ).resolves.toEqual([structUtils.makeLocator(descriptor, 'npm:6.7.8')]); + }); + + it('rejects descriptors not using the backstage: protocol', async () => { + await expect( + backstageResolver.getCandidates( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'npm:1.2.3', + ), + ), + ).rejects.toThrow(/unsupported version protocol/i); + }); + + it('rejects backstage: ranges with a ^ shorthand version', async () => { + await expect( + backstageResolver.getCandidates( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:^', + ), + ), + ).rejects.toThrow(/invalid backstage version/i); + }); + + it('rejects backstage: ranges with a * shorthand version', async () => { + await expect( + backstageResolver.getCandidates( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:*', + ), + ), + ).rejects.toThrow(/invalid backstage version/i); + }); + + it('rejects backstage: ranges with an invalid version specified', async () => { + await expect( + backstageResolver.getCandidates( + structUtils.makeDescriptor( + structUtils.makeIdent('backstage', 'core'), + 'backstage:latest', + ), + ), + ).rejects.toThrow(/invalid backstage version/i); + }); + }); +}); diff --git a/packages/yarn-plugin/src/resolver/BackstageResolver.ts b/packages/yarn-plugin/src/resolver/BackstageResolver.ts new file mode 100644 index 0000000000..33bba5df53 --- /dev/null +++ b/packages/yarn-plugin/src/resolver/BackstageResolver.ts @@ -0,0 +1,124 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + structUtils, + Descriptor, + Locator, + Package, + Resolver, +} from '@yarnpkg/core'; +import semver from 'semver'; +import { PROTOCOL } from '../constants'; +import { getCurrentBackstageVersion, getPackageVersion } from '../util'; + +export class BackstageResolver implements Resolver { + static protocol = PROTOCOL; + + supportsDescriptor = (descriptor: Descriptor) => + descriptor.range.startsWith(BackstageResolver.protocol); + + shouldPersistResolution = () => true; + + /** + * Called for each dependency present in the dependency list of a package + * definition. If it returns a new descriptor, this new descriptor will be + * used. + * + * In this plugin, we convert the specific range "backstage:^" to + * "backstage:". This new range will be the one + * stored in lockfile entries, ensuring that we re-resolve the package when + * the version in backstage.json changes. + */ + bindDescriptor(descriptor: Descriptor): Descriptor { + if (descriptor.range !== 'backstage:^') { + throw new Error( + `Unsupported version range "${ + descriptor.range + }" for package ${structUtils.stringifyIdent( + descriptor, + )}. The backstage protocol only supports the range "backstage:^".`, + ); + } + + return structUtils.makeDescriptor( + descriptor, + `${PROTOCOL}${getCurrentBackstageVersion()}`, + ); + } + + /** + * Given a descriptor, return the list of locators that potentially satisfy + * it. The implementation in this plugin converts a `backstage:` range with a + * concrete version into the appropriate concrete npm version for that + * backstage release. + */ + async getCandidates(descriptor: Descriptor): Promise { + const range = structUtils.parseRange(descriptor.range); + if (range.protocol !== BackstageResolver.protocol) { + throw new Error( + `Unsupported version protocol in version range "${ + descriptor.range + }" for package ${structUtils.stringifyIdent(descriptor)}`, + ); + } + + if (!semver.valid(range.selector)) { + throw new Error( + `Invalid Backstage version string when resolving version for ${structUtils.stringifyIdent( + descriptor, + )}`, + ); + } + + return [ + structUtils.makeLocator( + descriptor, + `npm:${await getPackageVersion(descriptor)}`, + ), + ]; + } + + /** + * This plugin does not need to support any locators itself, since the + * `getCandidates` method will always convert `backstage:` versions into + * `npm:` versions which can be handled as usual. + */ + supportsLocator = () => false; + + /** + * This resolver never transforms the packages that are actually depended on, + * only replaces versions. As such there's never a need to add additional + * dependencies. + */ + getResolutionDependencies = () => ({}); + + /** + * Candidate versions produced by this resolver always use the `npm:` + * protocol, so this function will never be called. + */ + async getSatisfying(): Promise<{ locators: Locator[]; sorted: boolean }> { + throw new Error('Unreachable'); + } + + /** + * Once transformed into locators (through getCandidates), the versions are + * resolved by the NpmSemverResolver + */ + async resolve(): Promise { + throw new Error(`Unreachable`); + } +} diff --git a/packages/yarn-plugin/src/util.ts b/packages/yarn-plugin/src/util.ts new file mode 100644 index 0000000000..8431e23cfa --- /dev/null +++ b/packages/yarn-plugin/src/util.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ppath, xfs } from '@yarnpkg/fslib'; +import { valid as semverValid } from 'semver'; +import { getManifestByVersion } from '@backstage/release-manifests'; +import { BACKSTAGE_JSON, findPaths } from '@backstage/cli-common'; +import { Descriptor, structUtils } from '@yarnpkg/core'; +import { PROTOCOL } from './constants'; + +export const getCurrentBackstageVersion = () => { + const workspaceRoot = ppath.resolve(findPaths(ppath.cwd()).targetRoot); + + const backstageJson = xfs.readJsonSync( + ppath.join(workspaceRoot, BACKSTAGE_JSON), + ); + + const backstageVersion = semverValid(backstageJson.version); + + if (backstageVersion === null) { + throw new Error('Valid version string not found in backstage.json'); + } + + return backstageVersion; +}; + +export const getPackageVersion = async (descriptor: Descriptor) => { + const ident = structUtils.stringifyIdent(descriptor); + const range = structUtils.parseRange(descriptor.range); + + if (range.protocol !== PROTOCOL) { + throw new Error(`Unexpected ${range.protocol} range when packing`); + } + + if (!semverValid(range.selector)) { + throw new Error(`Missing backstage version in range ${descriptor.range}`); + } + + const manifest = await getManifestByVersion({ + version: range.selector, + }); + + const manifestEntry = manifest.packages.find( + candidate => candidate.name === ident, + ); + + if (!manifestEntry) { + throw new Error( + `Package ${ident} not found in manifest for Backstage v${range.selector}. ` + + `This means the specified package is not included in this Backstage ` + + `release. This may imply the package has been replaced with an alternative - ` + + `please review the documentation for the package. If you need to continue ` + + `using this package, it will be necessary to switch to manually managing its ` + + `version.`, + ); + } + + return manifestEntry.version; +}; diff --git a/plugins/auth-backend-module-onelogin-provider/.eslintrc.js b/plugins/auth-backend-module-onelogin-provider/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/auth-backend-module-onelogin-provider/README.md b/plugins/auth-backend-module-onelogin-provider/README.md new file mode 100644 index 0000000000..2af119d25a --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/README.md @@ -0,0 +1,8 @@ +# Auth Module: OneLogin Provider + +This module provides an OneLogin auth provider implementation for `@backstage/plugin-auth-backend`. + +## Links + +- [Repository](https://github.com/backstage/backstage/tree/master/plugins/auth-backend-module-onelogin-provider) +- [Backstage Project Homepage](https://backstage.io) diff --git a/plugins/auth-backend-module-onelogin-provider/api-report.md b/plugins/auth-backend-module-onelogin-provider/api-report.md new file mode 100644 index 0000000000..8f94b27cab --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/api-report.md @@ -0,0 +1,30 @@ +## API Report File for "@backstage/plugin-auth-backend-module-onelogin-provider" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; +import { OAuthAuthenticator } from '@backstage/plugin-auth-node'; +import { OAuthAuthenticatorResult } from '@backstage/plugin-auth-node'; +import { PassportOAuthAuthenticatorHelper } from '@backstage/plugin-auth-node'; +import { PassportProfile } from '@backstage/plugin-auth-node'; +import { SignInResolverFactory } from '@backstage/plugin-auth-node'; + +// @public (undocumented) +const authModuleOneLoginProvider: () => BackendFeature; +export default authModuleOneLoginProvider; + +// @public (undocumented) +export const oneLoginAuthenticator: OAuthAuthenticator< + PassportOAuthAuthenticatorHelper, + PassportProfile +>; + +// @public +export namespace oneLoginSignInResolvers { + const usernameMatchingUserEntityName: SignInResolverFactory< + OAuthAuthenticatorResult, + unknown + >; +} +``` diff --git a/plugins/auth-backend-module-onelogin-provider/catalog-info.yaml b/plugins/auth-backend-module-onelogin-provider/catalog-info.yaml new file mode 100644 index 0000000000..6988215b59 --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-auth-backend-module-onelogin-provider + title: '@backstage/plugin-auth-backend-module-onelogin-provider' + description: The onelogin-provider backend module for the auth plugin. +spec: + lifecycle: experimental + type: backstage-backend-plugin-module + owner: maintainers diff --git a/plugins/auth-backend-module-onelogin-provider/config.d.ts b/plugins/auth-backend-module-onelogin-provider/config.d.ts new file mode 100644 index 0000000000..d4f1457461 --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/config.d.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export interface Config { + auth?: { + providers?: { + /** @visibility frontend */ + onelogin?: { + [authEnv: string]: { + clientId: string; + /** + * @visibility secret + */ + clientSecret: string; + issuer: string; + callbackUrl?: string; + }; + }; + }; + }; +} diff --git a/plugins/auth-backend-module-onelogin-provider/dev/index.ts b/plugins/auth-backend-module-onelogin-provider/dev/index.ts new file mode 100644 index 0000000000..d3c18c1d48 --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/dev/index.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createBackend } from '@backstage/backend-defaults'; + +const backend = createBackend(); + +backend.add(import('@backstage/plugin-auth-backend')); +backend.add(import('../src')); + +backend.start(); diff --git a/plugins/auth-backend-module-onelogin-provider/knip-report.md b/plugins/auth-backend-module-onelogin-provider/knip-report.md new file mode 100644 index 0000000000..a9ad7e156a --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/knip-report.md @@ -0,0 +1,19 @@ +# Knip report + +## Unused dependencies (4) + +| Name | Location | Severity | +| :---------------------------- | :----------- | :------- | +| @backstage/backend-plugin-api | package.json | error | +| @backstage/plugin-auth-node | package.json | error | +| passport | package.json | error | +| express | package.json | error | + +## Unused devDependencies (3) + +| Name | Location | Severity | +| :----------------------------- | :----------- | :------- | +| @backstage/plugin-auth-backend | package.json | error | +| @backstage/backend-test-utils | package.json | error | +| @backstage/backend-defaults | package.json | error | + diff --git a/plugins/auth-backend-module-onelogin-provider/package.json b/plugins/auth-backend-module-onelogin-provider/package.json new file mode 100644 index 0000000000..7012742eb2 --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/package.json @@ -0,0 +1,49 @@ +{ + "name": "@backstage/plugin-auth-backend-module-onelogin-provider", + "version": "0.0.0", + "description": "The onelogin-provider backend module for the auth plugin.", + "backstage": { + "role": "backend-plugin-module" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/auth-backend-module-onelogin-provider" + }, + "license": "Apache-2.0", + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist", + "config.d.ts" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "start": "backstage-cli package start", + "test": "backstage-cli package test" + }, + "dependencies": { + "@backstage/backend-plugin-api": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "express": "^4.18.2", + "passport": "^0.7.0", + "passport-onelogin-oauth": "^0.0.1" + }, + "devDependencies": { + "@backstage/backend-defaults": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@backstage/plugin-auth-backend": "workspace:^", + "supertest": "^6.3.3" + }, + "configSchema": "config.d.ts" +} diff --git a/plugins/auth-backend-module-onelogin-provider/src/authenticator.ts b/plugins/auth-backend-module-onelogin-provider/src/authenticator.ts new file mode 100644 index 0000000000..7b40ad7473 --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/src/authenticator.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Strategy as OneLoginStrategy } from 'passport-onelogin-oauth'; +import { + createOAuthAuthenticator, + PassportOAuthAuthenticatorHelper, + PassportOAuthDoneCallback, + PassportProfile, +} from '@backstage/plugin-auth-node'; + +/** @public */ +export const oneLoginAuthenticator = createOAuthAuthenticator({ + defaultProfileTransform: + PassportOAuthAuthenticatorHelper.defaultProfileTransform, + initialize({ callbackUrl, config }) { + const clientId = config.getString('clientId'); + const clientSecret = config.getString('clientSecret'); + const issuer = config.getString('issuer'); + + return PassportOAuthAuthenticatorHelper.from( + new OneLoginStrategy( + { + clientID: clientId, + clientSecret: clientSecret, + callbackURL: callbackUrl, + issuer, + passReqToCallback: false, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: PassportOAuthDoneCallback, + ) => { + done( + undefined, + { fullProfile, params, accessToken }, + { refreshToken }, + ); + }, + ), + ); + }, + + async start(input, helper) { + input.scope = 'openid'; + return helper.start(input, { + accessType: 'offline', + prompt: 'consent', + }); + }, + + async authenticate(input, helper) { + return helper.authenticate(input); + }, + + async refresh(input, helper) { + input.scope = 'openid'; + return helper.refresh(input); + }, +}); diff --git a/plugins/auth-backend-module-onelogin-provider/src/index.ts b/plugins/auth-backend-module-onelogin-provider/src/index.ts new file mode 100644 index 0000000000..82855d11a0 --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/src/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * The onelogin-provider backend module for the auth plugin. + * + * @packageDocumentation + */ + +export { oneLoginAuthenticator } from './authenticator'; +export { authModuleOneLoginProvider as default } from './module'; +export { oneLoginSignInResolvers } from './resolvers'; diff --git a/plugins/auth-backend-module-onelogin-provider/src/module.test.ts b/plugins/auth-backend-module-onelogin-provider/src/module.test.ts new file mode 100644 index 0000000000..c98dd7f71a --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/src/module.test.ts @@ -0,0 +1,79 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { mockServices, startTestBackend } from '@backstage/backend-test-utils'; +import { authModuleOneLoginProvider } from './module'; +import request from 'supertest'; +import { decodeOAuthState } from '@backstage/plugin-auth-node'; + +describe('authModuleOneLoginProvider', () => { + it('should start', async () => { + const { server } = await startTestBackend({ + features: [ + import('@backstage/plugin-auth-backend'), + authModuleOneLoginProvider, + mockServices.rootConfig.factory({ + data: { + app: { + baseUrl: 'http://localhost:3000', + }, + auth: { + providers: { + onelogin: { + development: { + clientId: 'my-client-id', + clientSecret: 'my-client-secret', + issuer: 'https://my-company.onelogin.com/oidc/2', + }, + }, + }, + }, + }, + }), + ], + }); + + const agent = request.agent(server); + + const res = await agent.get('/api/auth/onelogin/start?env=development'); + + expect(res.status).toEqual(302); + + const nonceCookie = agent.jar.getCookie('onelogin-nonce', { + domain: 'localhost', + path: '/api/auth/onelogin/handler', + script: false, + secure: false, + }); + expect(nonceCookie).toBeDefined(); + + const startUrl = new URL(res.get('location')); + expect(startUrl.origin).toBe('https://my-company.onelogin.com'); + expect(startUrl.pathname).toBe('/oidc/2/auth'); + expect(Object.fromEntries(startUrl.searchParams)).toEqual({ + response_type: 'code', + scope: 'openid', + client_id: 'my-client-id', + redirect_uri: `http://localhost:${server.port()}/api/auth/onelogin/handler/frame`, + state: expect.any(String), + }); + + expect(decodeOAuthState(startUrl.searchParams.get('state')!)).toEqual({ + env: 'development', + nonce: decodeURIComponent(nonceCookie.value), + }); + }); +}); diff --git a/plugins/auth-backend-module-onelogin-provider/src/module.ts b/plugins/auth-backend-module-onelogin-provider/src/module.ts new file mode 100644 index 0000000000..7d67424152 --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/src/module.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createBackendModule } from '@backstage/backend-plugin-api'; +import { + authProvidersExtensionPoint, + commonSignInResolvers, + createOAuthProviderFactory, +} from '@backstage/plugin-auth-node'; +import { oneLoginAuthenticator } from './authenticator'; +import { oneLoginSignInResolvers } from './resolvers'; + +/** @public */ +export const authModuleOneLoginProvider = createBackendModule({ + pluginId: 'auth', + moduleId: 'onelogin-provider', + register(reg) { + reg.registerInit({ + deps: { + providers: authProvidersExtensionPoint, + }, + async init({ providers }) { + providers.registerProvider({ + providerId: 'onelogin', + factory: createOAuthProviderFactory({ + authenticator: oneLoginAuthenticator, + signInResolverFactories: { + ...oneLoginSignInResolvers, + ...commonSignInResolvers, + }, + }), + }); + }, + }); + }, +}); diff --git a/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts new file mode 100644 index 0000000000..93bfd758cb --- /dev/null +++ b/plugins/auth-backend-module-onelogin-provider/src/resolvers.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createSignInResolverFactory, + OAuthAuthenticatorResult, + PassportProfile, + SignInInfo, +} from '@backstage/plugin-auth-node'; + +/** + * Available sign-in resolvers for the OneLogin auth provider. + * + * @public + */ +export namespace oneLoginSignInResolvers { + /** + * Looks up the user by matching their OneLogin username to the entity name. + */ + export const usernameMatchingUserEntityName = createSignInResolverFactory({ + create() { + return async ( + info: SignInInfo>, + ctx, + ) => { + const { result } = info; + + const id = result.fullProfile.username; + if (!id) { + throw new Error(`OneLogin user profile does not contain a username`); + } + + return ctx.signInWithCatalogUser({ entityRef: { name: id } }); + }; + }, + }); +} diff --git a/plugins/auth-backend/src/providers/onelogin/types.d.ts b/plugins/auth-backend-module-onelogin-provider/src/types.d.ts similarity index 81% rename from plugins/auth-backend/src/providers/onelogin/types.d.ts rename to plugins/auth-backend-module-onelogin-provider/src/types.d.ts index 6df10388af..9a05c4f2ec 100644 --- a/plugins/auth-backend/src/providers/onelogin/types.d.ts +++ b/plugins/auth-backend-module-onelogin-provider/src/types.d.ts @@ -13,8 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + declare module 'passport-onelogin-oauth' { + import { Request } from 'express'; + import { StrategyCreated } from 'passport'; + export class Strategy { constructor(options: any, verify: any); + authenticate(this: StrategyCreated, req: Request, options?: any): any; } } diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index f9f89a7f6d..c756e7b190 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -57,6 +57,7 @@ "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^", "@backstage/plugin-auth-backend-module-oidc-provider": "workspace:^", "@backstage/plugin-auth-backend-module-okta-provider": "workspace:^", + "@backstage/plugin-auth-backend-module-onelogin-provider": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/types": "workspace:^", diff --git a/plugins/auth-backend/src/providers/onelogin/provider.ts b/plugins/auth-backend/src/providers/onelogin/provider.ts index 0360a91a1c..ab5995da4c 100644 --- a/plugins/auth-backend/src/providers/onelogin/provider.ts +++ b/plugins/auth-backend/src/providers/onelogin/provider.ts @@ -14,156 +14,18 @@ * limitations under the License. */ -import { Strategy as OneLoginStrategy } from 'passport-onelogin-oauth'; -import express from 'express'; +import { oneLoginAuthenticator } from '@backstage/plugin-auth-backend-module-onelogin-provider'; import { - OAuthAdapter, - OAuthProviderOptions, - OAuthHandlers, - OAuthResponse, - OAuthEnvironmentHandler, - OAuthStartRequest, - encodeState, - OAuthRefreshRequest, - OAuthResult, -} from '../../lib/oauth'; -import passport from 'passport'; -import { - executeFrameHandlerStrategy, - executeRedirectStrategy, - executeRefreshTokenStrategy, - makeProfileInfo, - executeFetchUserProfileStrategy, - PassportDoneCallback, -} from '../../lib/passport'; -import { OAuthStartResponse, AuthHandler } from '../types'; -import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; -import { - AuthResolverContext, SignInResolver, + createOAuthProviderFactory, } from '@backstage/plugin-auth-node'; - -type PrivateInfo = { - refreshToken: string; -}; - -export type Options = OAuthProviderOptions & { - issuer: string; - signInResolver?: SignInResolver; - authHandler: AuthHandler; - resolverContext: AuthResolverContext; -}; - -export class OneLoginProvider implements OAuthHandlers { - private readonly _strategy: any; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; - private readonly resolverContext: AuthResolverContext; - - constructor(options: Options) { - this.signInResolver = options.signInResolver; - this.authHandler = options.authHandler; - this.resolverContext = options.resolverContext; - this._strategy = new OneLoginStrategy( - { - issuer: options.issuer, - clientID: options.clientId, - clientSecret: options.clientSecret, - callbackURL: options.callbackUrl, - passReqToCallback: false, - }, - ( - accessToken: any, - refreshToken: any, - params: any, - fullProfile: passport.Profile, - done: PassportDoneCallback, - ) => { - done( - undefined, - { - accessToken, - refreshToken, - params, - fullProfile, - }, - { - refreshToken, - }, - ); - }, - ); - } - async start(req: OAuthStartRequest): Promise { - return await executeRedirectStrategy(req, this._strategy, { - accessType: 'offline', - prompt: 'consent', - scope: 'openid', - state: encodeState(req.state), - }); - } - - async handler(req: express.Request) { - const { result, privateInfo } = await executeFrameHandlerStrategy< - OAuthResult, - PrivateInfo - >(req, this._strategy); - - return { - response: await this.handleResult(result), - refreshToken: privateInfo.refreshToken, - }; - } - - async refresh(req: OAuthRefreshRequest) { - const { accessToken, refreshToken, params } = - await executeRefreshTokenStrategy( - this._strategy, - req.refreshToken, - 'openid', - ); - - const fullProfile = await executeFetchUserProfileStrategy( - this._strategy, - accessToken, - ); - - return { - response: await this.handleResult({ - fullProfile, - params, - accessToken, - }), - refreshToken, - }; - } - - private async handleResult(result: OAuthResult) { - const { profile } = await this.authHandler(result, this.resolverContext); - - const response: OAuthResponse = { - providerInfo: { - idToken: result.params.id_token, - accessToken: result.accessToken, - scope: result.params.scope, - expiresInSeconds: result.params.expires_in, - }, - profile, - }; - - if (this.signInResolver) { - response.backstageIdentity = await this.signInResolver( - { - result, - profile, - }, - this.resolverContext, - ); - } - - return response; - } -} +import { + adaptLegacyOAuthHandler, + adaptLegacyOAuthSignInResolver, +} from '../../lib/legacy'; +import { OAuthResult } from '../../lib/oauth'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { AuthHandler } from '../types'; /** * Auth provider integration for OneLogin auth @@ -188,36 +50,10 @@ export const onelogin = createAuthProviderIntegration({ resolver: SignInResolver; }; }) { - return ({ providerId, globalConfig, config, resolverContext }) => - OAuthEnvironmentHandler.mapConfig(config, envConfig => { - const clientId = envConfig.getString('clientId'); - const clientSecret = envConfig.getString('clientSecret'); - const issuer = envConfig.getString('issuer'); - const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); - const callbackUrl = - customCallbackUrl || - `${globalConfig.baseUrl}/${providerId}/handler/frame`; - - const authHandler: AuthHandler = options?.authHandler - ? options.authHandler - : async ({ fullProfile, params }) => ({ - profile: makeProfileInfo(fullProfile, params.id_token), - }); - - const provider = new OneLoginProvider({ - clientId, - clientSecret, - callbackUrl, - issuer, - authHandler, - signInResolver: options?.signIn?.resolver, - resolverContext, - }); - - return OAuthAdapter.fromConfig(globalConfig, provider, { - providerId, - callbackUrl, - }); - }); + return createOAuthProviderFactory({ + authenticator: oneLoginAuthenticator, + profileTransform: adaptLegacyOAuthHandler(options?.authHandler), + signInResolver: adaptLegacyOAuthSignInResolver(options?.signIn?.resolver), + }); }, }); diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index 8f75e52289..205ffed089 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -115,6 +115,7 @@ export type EntityRelationsGraphProps = { mergeRelations?: boolean; kinds?: string[]; relations?: string[]; + entityFilter?: (entity: Entity) => boolean; direction?: Direction; onNodeClick?: (value: EntityNode, event: MouseEvent_2) => void; relationPairs?: RelationPairs; diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index 1956a4bdae..7c7f4c6063 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -71,6 +71,7 @@ export const CatalogGraphCard = ( direction = Direction.LEFT_RIGHT, kinds, relations, + entityFilter, height, className, rootEntityNames, @@ -140,6 +141,7 @@ export const CatalogGraphCard = ( mergeRelations={mergeRelations} direction={direction} relationPairs={relationPairs} + entityFilter={entityFilter} zoom={zoom} /> diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index c7f9360589..ad673b192a 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -121,7 +121,11 @@ export const CatalogGraphPage = ( }; } & Partial, ) => { - const { relationPairs = ALL_RELATION_PAIRS, initialState } = props; + const { + relationPairs = ALL_RELATION_PAIRS, + initialState, + entityFilter, + } = props; const navigate = useNavigate(); const classes = useStyles(); @@ -256,6 +260,7 @@ export const CatalogGraphPage = ( onNodeClick={onNodeClick} direction={direction} relationPairs={relationPairs} + entityFilter={entityFilter} className={classes.graph} zoom="enabled" curve={curve} diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 61e24ecac4..51de7cc47f 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -16,6 +16,7 @@ import { CompoundEntityRef, + Entity, stringifyEntityRef, } from '@backstage/catalog-model'; import { @@ -77,6 +78,7 @@ export type EntityRelationsGraphProps = { mergeRelations?: boolean; kinds?: string[]; relations?: string[]; + entityFilter?: (entity: Entity) => boolean; direction?: Direction; onNodeClick?: (value: EntityNode, event: MouseEvent) => void; relationPairs?: RelationPairs; @@ -101,6 +103,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { mergeRelations = true, kinds, relations, + entityFilter, direction = Direction.LEFT_RIGHT, onNodeClick, relationPairs = ALL_RELATION_PAIRS, @@ -130,6 +133,7 @@ export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { mergeRelations, kinds, relations, + entityFilter, onNodeClick, relationPairs, }); diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts index 420af5345c..b2d9083180 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.test.ts @@ -353,6 +353,28 @@ describe('useEntityRelationGraph', () => { }); }); + test('should filter by func', async () => { + const { result, rerender } = renderHook(() => + useEntityRelationGraph({ + rootEntityRefs: ['b:d/c'], + filter: { + entityFilter: e => e.metadata.name !== 'c2', + }, + }), + ); + + // Simulate rerendering as this is triggered automatically due to the mock + for (let i = 0; i < 5; ++i) { + rerender(); + } + + expect(result.current.entities).toEqual({ + 'b:d/c': expect.anything(), + 'b:d/c1': expect.anything(), + 'k:d/a1': expect.anything(), + }); + }); + test('should support multiple roots by kind', async () => { const { result, rerender } = renderHook(() => useEntityRelationGraph({ diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts index 957f35dc25..af2a491ada 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationGraph.ts @@ -16,6 +16,7 @@ import { Entity } from '@backstage/catalog-model'; import { useEffect } from 'react'; import { useEntityStore } from './useEntityStore'; +import { pickBy } from 'lodash'; /** * Discover the graph of entities connected by relations, starting from a set of @@ -24,13 +25,19 @@ import { useEntityStore } from './useEntityStore'; */ export function useEntityRelationGraph({ rootEntityRefs, - filter: { maxDepth = Number.POSITIVE_INFINITY, relations, kinds } = {}, + filter: { + maxDepth = Number.POSITIVE_INFINITY, + relations, + kinds, + entityFilter, + } = {}, }: { rootEntityRefs: string[]; filter?: { maxDepth?: number; relations?: string[]; kinds?: string[]; + entityFilter?: (entity: Entity) => boolean; }; }): { entities?: { [ref: string]: Entity }; @@ -60,6 +67,11 @@ export function useEntityRelationGraph({ processedEntityRefs.add(entityRef); if (entity && entity.relations) { + // If the entity is filtered out then no need to check any + // of its outgoing relationships to other entities + if (entityFilter && !entityFilter(entity)) { + continue; + } for (const rel of entity.relations) { if ( (!relations || relations.includes(rel.type)) && @@ -81,12 +93,23 @@ export function useEntityRelationGraph({ ++depth; } - requestEntities([...expectedEntities]); - }, [entities, rootEntityRefs, maxDepth, relations, kinds, requestEntities]); + }, [ + entities, + rootEntityRefs, + maxDepth, + relations, + kinds, + entityFilter, + requestEntities, + ]); + + const filteredEntities = entityFilter + ? pickBy(entities, (value, _key) => entityFilter(value)) + : entities; return { - entities, + entities: filteredEntities, loading, error, }; diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts index 74aff1b3d0..31398a137e 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/useEntityRelationNodesAndEdges.ts @@ -18,7 +18,7 @@ import useDebounce from 'react-use/esm/useDebounce'; import { RelationPairs, ALL_RELATION_PAIRS } from './relations'; import { EntityEdge, EntityNode } from './types'; import { useEntityRelationGraph } from './useEntityRelationGraph'; -import { DEFAULT_NAMESPACE } from '@backstage/catalog-model'; +import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model'; /** * Generate nodes and edges to render the entity graph. @@ -30,6 +30,7 @@ export function useEntityRelationNodesAndEdges({ mergeRelations = true, kinds, relations, + entityFilter, onNodeClick, relationPairs = ALL_RELATION_PAIRS, }: { @@ -39,6 +40,7 @@ export function useEntityRelationNodesAndEdges({ mergeRelations?: boolean; kinds?: string[]; relations?: string[]; + entityFilter?: (entity: Entity) => boolean; onNodeClick?: (value: EntityNode, event: MouseEvent) => void; relationPairs?: RelationPairs; }): { @@ -57,6 +59,7 @@ export function useEntityRelationNodesAndEdges({ maxDepth, kinds, relations, + entityFilter, }, }); diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx index d3c76a183d..4bd9f100f9 100644 --- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx +++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx @@ -20,7 +20,7 @@ import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import Autocomplete from '@material-ui/lab/Autocomplete'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState, ReactNode } from 'react'; import { useApi } from '@backstage/core-plugin-api'; import useAsync from 'react-use/esm/useAsync'; import { catalogApiRef } from '../../api'; @@ -152,6 +152,9 @@ export function EntityAutocompletePicker< {label} + PopperComponent={popperProps => ( +
{popperProps.children as ReactNode}
+ )} multiple disableCloseOnSelect options={availableOptions} diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 88ff3945d1..c6b738d18b 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -30,7 +30,7 @@ import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import Autocomplete from '@material-ui/lab/Autocomplete'; -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useMemo, useState, ReactNode } from 'react'; import { useEntityList } from '../../hooks/useEntityListProvider'; import { EntityOwnerFilter } from '../../filters'; import { useDebouncedEffect } from '@react-hookz/web'; @@ -178,6 +178,9 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { Owner ( +
{popperProps.children as ReactNode}
+ )} multiple disableCloseOnSelect loading={loading} diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx index f7de5d72b1..9995aba936 100644 --- a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx +++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.tsx @@ -24,7 +24,7 @@ import { makeStyles } from '@material-ui/core/styles'; import CheckBoxIcon from '@material-ui/icons/CheckBox'; import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import React, { useState } from 'react'; +import React, { useState, ReactNode } from 'react'; import { useEntityList } from '../../hooks'; import Autocomplete from '@material-ui/lab/Autocomplete'; @@ -73,6 +73,9 @@ export const EntityProcessingStatusPicker = () => { Processing Status + PopperComponent={popperProps => ( +
{popperProps.children as ReactNode}
+ )} multiple disableCloseOnSelect options={availableAdvancedItems} diff --git a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx index 38691b49d2..6692d5fbfe 100644 --- a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx @@ -29,7 +29,8 @@ type PaginatedCatalogTableProps = { * @internal */ export function PaginatedCatalogTable(props: PaginatedCatalogTableProps) { - const { columns, data, next, prev, title, isLoading, options } = props; + const { columns, data, next, prev, title, isLoading, options, ...restProps } = + props; return ( ); } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts index 0dcbd10f0f..fc688eee44 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts @@ -57,7 +57,7 @@ describe('debug:wait', () => { await expect(async () => { await action.handler(context); }).rejects.toThrow( - 'Waiting duration is longer than the maximum threshold of 0 hours, 0 minutes, 30 seconds', + 'Waiting duration is longer than the maximum threshold of 0 hours, 10 minutes, 0 seconds', ); }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts index ae0af4db5a..fd4a77754b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts @@ -21,7 +21,7 @@ import { examples } from './wait.examples'; const id = 'debug:wait'; -const MAX_WAIT_TIME_IN_ISO = 'T00:00:30'; +const MAX_WAIT_TIME_IN_ISO = 'T00:10:00'; /** * Waits for a certain period of time. diff --git a/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx index 24f8a21109..f2505ec148 100644 --- a/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx +++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import capitalize from 'lodash/capitalize'; import { Progress } from '@backstage/core-components'; import Box from '@material-ui/core/Box'; @@ -78,6 +78,9 @@ export const TemplateCategoryPicker = () => { Categories + PopperComponent={popperProps => ( +
{popperProps.children as ReactNode}
+ )} multiple id="categories-picker" options={availableTypes} diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx index 8016a6ae83..dcb52a3949 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx @@ -16,14 +16,18 @@ import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { + CatalogApi, + catalogApiRef, + entityPresentationApiRef, +} from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; - import { fireEvent, screen } from '@testing-library/react'; import React from 'react'; import { EntityPicker } from './EntityPicker'; import { EntityPickerProps } from './schema'; import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react'; +import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'scaffolder.backstage.io/v1beta3', @@ -50,6 +54,7 @@ describe('', () => { getLocationByRef: jest.fn(), removeEntityByUid: jest.fn(), } as any; + let Wrapper: React.ComponentType>; beforeEach(() => { @@ -59,7 +64,15 @@ describe('', () => { ]; Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} ); @@ -90,7 +103,12 @@ describe('', () => { ); expect(catalogApi.getEntities).toHaveBeenCalledWith({ - fields: ['metadata.name', 'metadata.namespace', 'kind'], + fields: [ + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + 'kind', + ], filter: undefined, }); }); diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index 69e1b5efad..176f6e9ce0 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -24,13 +24,16 @@ import { } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { + EntityDisplayName, + EntityRefPresentationSnapshot, catalogApiRef, - humanizeEntityRef, + entityPresentationApiRef, } from '@backstage/plugin-catalog-react'; import TextField from '@material-ui/core/TextField'; import FormControl from '@material-ui/core/FormControl'; import Autocomplete, { AutocompleteChangeReason, + createFilterOptions, } from '@material-ui/lab/Autocomplete'; import React, { useCallback, useEffect } from 'react'; import useAsync from 'react-use/esm/useAsync'; @@ -65,31 +68,55 @@ export const EntityPicker = (props: EntityPickerProps) => { uiSchema['ui:options']?.defaultNamespace || undefined; const catalogApi = useApi(catalogApiRef); + const entityPresentationApi = useApi(entityPresentationApiRef); const { value: entities, loading } = useAsync(async () => { - const fields = ['metadata.name', 'metadata.namespace', 'kind']; + const fields = [ + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + 'kind', + ]; const { items } = await catalogApi.getEntities( catalogFilter ? { filter: catalogFilter, fields } : { filter: undefined, fields }, ); - return items; + + const entityRefToPresentation = new Map< + string, + EntityRefPresentationSnapshot + >( + await Promise.all( + items.map(async item => { + const presentation = await entityPresentationApi.forEntity(item) + .promise; + return [stringifyEntityRef(item), presentation] as [ + string, + EntityRefPresentationSnapshot, + ]; + }), + ), + ); + + return { catalogEntities: items, entityRefToPresentation }; }); + const allowArbitraryValues = uiSchema['ui:options']?.allowArbitraryValues ?? true; const getLabel = useCallback( - (ref: string) => { + (freeSoloValue: string) => { try { - return humanizeEntityRef( - parseEntityRef(ref, { defaultKind, defaultNamespace }), - { - defaultKind, - defaultNamespace, - }, - ); + // Will throw if defaultKind or defaultNamespace are not set + const parsedRef = parseEntityRef(freeSoloValue, { + defaultKind, + defaultNamespace, + }); + + return stringifyEntityRef(parsedRef); } catch (err) { - return ref; + return freeSoloValue; } }, [defaultKind, defaultNamespace], @@ -129,12 +156,12 @@ export const EntityPicker = (props: EntityPickerProps) => { // Since free solo can be enabled, attempt to parse as a full entity ref first, then fall // back to the given value. const selectedEntity = - entities?.find(e => stringifyEntityRef(e) === formData) ?? + entities?.catalogEntities.find(e => stringifyEntityRef(e) === formData) ?? (allowArbitraryValues && formData ? getLabel(formData) : ''); useEffect(() => { - if (entities?.length === 1 && selectedEntity === '') { - onChange(stringifyEntityRef(entities[0])); + if (entities?.catalogEntities.length === 1 && selectedEntity === '') { + onChange(stringifyEntityRef(entities.catalogEntities[0])); } }, [entities, onChange, selectedEntity]); @@ -145,17 +172,18 @@ export const EntityPicker = (props: EntityPickerProps) => { error={rawErrors?.length > 0 && !formData} > // option can be a string due to freeSolo. typeof option === 'string' ? option - : humanizeEntityRef(option, { defaultKind, defaultNamespace })! + : entities?.entityRefToPresentation.get(stringifyEntityRef(option)) + ?.entityRef! } autoSelect freeSolo={allowArbitraryValues} @@ -171,6 +199,12 @@ export const EntityPicker = (props: EntityPickerProps) => { InputProps={params.InputProps} /> )} + renderOption={option => } + filterOptions={createFilterOptions({ + stringify: option => + entities?.entityRefToPresentation.get(stringifyEntityRef(option)) + ?.primaryTitle!, + })} /> ); diff --git a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx index 57ca8e53ab..79105e05c3 100644 --- a/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx +++ b/plugins/scaffolder/src/components/fields/OwnerPicker/OwnerPicker.test.tsx @@ -16,11 +16,16 @@ import { type EntityFilterQuery } from '@backstage/catalog-client'; import { Entity } from '@backstage/catalog-model'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { + CatalogApi, + catalogApiRef, + entityPresentationApiRef, +} from '@backstage/plugin-catalog-react'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; import { ScaffolderRJSFFieldProps as FieldProps } from '@backstage/plugin-scaffolder-react'; import React from 'react'; import { OwnerPicker } from './OwnerPicker'; +import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog'; const makeEntity = (kind: string, namespace: string, name: string): Entity => ({ apiVersion: 'backstage.io/v1beta1', @@ -64,7 +69,15 @@ describe('', () => { ]; Wrapper = ({ children }: { children?: React.ReactNode }) => ( - + {children} ); @@ -99,7 +112,12 @@ describe('', () => { filter: { kind: ['Group', 'User'], }, - fields: ['metadata.name', 'metadata.namespace', 'kind'], + fields: [ + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + 'kind', + ], }), ); }); @@ -132,7 +150,12 @@ describe('', () => { filter: { kind: ['User'], }, - fields: ['metadata.name', 'metadata.namespace', 'kind'], + fields: [ + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + 'kind', + ], }), ); }); diff --git a/plugins/user-settings-backend/README.md b/plugins/user-settings-backend/README.md index bbd69cb18e..a1fb504415 100644 --- a/plugins/user-settings-backend/README.md +++ b/plugins/user-settings-backend/README.md @@ -6,13 +6,26 @@ authorization token. ## Setup backend -1. Install the backend plugin: +Install the backend plugin ```bash # From your Backstage root directory yarn --cwd packages/backend add @backstage/plugin-user-settings-backend ``` +### New backend + +Add the plugin to your backend in `packages/backend/src/index.ts`: + +```ts +backend.add(import('@backstage/plugin-user-settings-backend/alpha')); +``` + +To get real-time updates of the user settings across different user sessions, you must also install +the `@backstage/plugin-signals-backend` plugin. + +### Old backend + 1. Configure the routes by adding a new `userSettings.ts` file in `packages/backend/src/plugins/`: @@ -29,7 +42,7 @@ export default async function createPlugin(env: PluginEnvironment) { } ``` -3. Add the new routes to your backend by modifying `packages/backend/src/index.ts`: +2. Add the new routes to your backend by modifying `packages/backend/src/index.ts`: ```diff // packages/backend/src/index.ts @@ -58,6 +71,7 @@ To make use of the user settings backend, replace the `WebStorage` with the + storageApiRef, } from '@backstage/core-plugin-api'; +import { UserSettingsStorage } from '@backstage/plugin-user-settings'; ++import { signalApiRef } from '@backstage/plugin-signals-react'; export const apis: AnyApiFactory[] = [ + createApiFactory({ @@ -66,7 +80,8 @@ To make use of the user settings backend, replace the `WebStorage` with the + discoveryApi: discoveryApiRef, + errorApi: errorApiRef, + fetchApi: fetchApiRef, -+ identityApi: identityApiRef ++ identityApi: identityApiRef, ++ signalApi: signalApiRef, // Optional + }, + factory: deps => UserSettingsStorage.create(deps), + }), diff --git a/plugins/user-settings-backend/api-report.md b/plugins/user-settings-backend/api-report.md index 1bc37bbec7..f8bf571fa7 100644 --- a/plugins/user-settings-backend/api-report.md +++ b/plugins/user-settings-backend/api-report.md @@ -6,6 +6,7 @@ import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; +import { SignalsService } from '@backstage/plugin-signals-node'; // @public export function createRouter(options: RouterOptions): Promise; @@ -16,6 +17,8 @@ export interface RouterOptions { database: PluginDatabaseManager; // (undocumented) identity: IdentityApi; + // (undocumented) + signals?: SignalsService; } // (No @packageDocumentation comment for this package) diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index f3d764a7eb..332c7eada1 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -51,6 +51,8 @@ "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-signals-node": "workspace:^", + "@backstage/plugin-user-settings-common": "workspace:^", "@backstage/types": "workspace:^", "@types/express": "^4.17.6", "express": "^4.17.1", diff --git a/plugins/user-settings-backend/src/alpha.ts b/plugins/user-settings-backend/src/alpha.ts index a070396ca0..86180c8699 100644 --- a/plugins/user-settings-backend/src/alpha.ts +++ b/plugins/user-settings-backend/src/alpha.ts @@ -15,10 +15,11 @@ */ import { - createBackendPlugin, coreServices, + createBackendPlugin, } from '@backstage/backend-plugin-api'; import { createRouter } from './service/router'; +import { signalsServiceRef } from '@backstage/plugin-signals-node'; /** * The user settings backend plugin. @@ -33,9 +34,10 @@ export default createBackendPlugin({ database: coreServices.database, identity: coreServices.identity, httpRouter: coreServices.httpRouter, + signals: signalsServiceRef, }, - async init({ database, identity, httpRouter }) { - httpRouter.use(await createRouter({ database, identity })); + async init({ database, identity, httpRouter, signals }) { + httpRouter.use(await createRouter({ database, identity, signals })); }, }); }, diff --git a/plugins/user-settings-backend/src/service/router.test.ts b/plugins/user-settings-backend/src/service/router.test.ts index 64cdfcc465..27375c6333 100644 --- a/plugins/user-settings-backend/src/service/router.test.ts +++ b/plugins/user-settings-backend/src/service/router.test.ts @@ -22,6 +22,7 @@ import express from 'express'; import request from 'supertest'; import { UserSettingsStore } from '../database/UserSettingsStore'; import { createRouterInternal } from './router'; +import { SignalsService } from '@backstage/plugin-signals-node'; describe('createRouter', () => { const userSettingsStore: jest.Mocked = { @@ -36,6 +37,9 @@ describe('createRouter', () => { const identityApi: jest.Mocked> = { getIdentity: getIdentityMock, }; + const signalService: jest.Mocked = { + publish: jest.fn(), + }; let app: express.Express; @@ -43,6 +47,7 @@ describe('createRouter', () => { const router = await createRouterInternal({ userSettingsStore, identity: identityApi as IdentityApi, + signals: signalService as SignalsService, }); app = express().use(router); @@ -118,6 +123,11 @@ describe('createRouter', () => { bucket: 'my-bucket', key: 'my-key', }); + expect(signalService.publish).toHaveBeenCalledWith({ + recipients: { type: 'user', entityRef: 'user-1' }, + channel: `user-settings`, + message: { type: 'key-deleted', key: 'my-key' }, + }); }); it('returns an error if the Authorization header is missing', async () => { @@ -167,6 +177,11 @@ describe('createRouter', () => { bucket: 'my-bucket', key: 'my-key', }); + expect(signalService.publish).toHaveBeenCalledWith({ + recipients: { type: 'user', entityRef: 'user-1' }, + channel: `user-settings`, + message: { type: 'key-changed', key: 'my-key' }, + }); }); it('returns an error if the value is not given', async () => { diff --git a/plugins/user-settings-backend/src/service/router.ts b/plugins/user-settings-backend/src/service/router.ts index ebbb6716a3..b38e536354 100644 --- a/plugins/user-settings-backend/src/service/router.ts +++ b/plugins/user-settings-backend/src/service/router.ts @@ -21,6 +21,8 @@ import express, { Request } from 'express'; import Router from 'express-promise-router'; import { DatabaseUserSettingsStore } from '../database/DatabaseUserSettingsStore'; import { UserSettingsStore } from '../database/UserSettingsStore'; +import { SignalsService } from '@backstage/plugin-signals-node'; +import { UserSettingsSignal } from '@backstage/plugin-user-settings-common'; /** * @public @@ -28,6 +30,7 @@ import { UserSettingsStore } from '../database/UserSettingsStore'; export interface RouterOptions { database: PluginDatabaseManager; identity: IdentityApi; + signals?: SignalsService; } /** @@ -45,12 +48,14 @@ export async function createRouter( return await createRouterInternal({ userSettingsStore, identity: options.identity, + signals: options.signals, }); } export async function createRouterInternal(options: { identity: IdentityApi; userSettingsStore: UserSettingsStore; + signals?: SignalsService; }): Promise { const router = Router(); router.use(express.json()); @@ -104,6 +109,14 @@ export async function createRouterInternal(options: { key, }); + if (options.signals) { + await options.signals.publish({ + recipients: { type: 'user', entityRef: userEntityRef }, + channel: `user-settings`, + message: { type: 'key-changed', key }, + }); + } + res.json(setting); }); @@ -113,6 +126,13 @@ export async function createRouterInternal(options: { const { bucket, key } = req.params; await options.userSettingsStore.delete({ userEntityRef, bucket, key }); + if (options.signals) { + await options.signals.publish({ + recipients: { type: 'user', entityRef: userEntityRef }, + channel: 'user-settings', + message: { type: 'key-deleted', key }, + }); + } res.status(204).end(); }); diff --git a/plugins/user-settings-common/.eslintrc.js b/plugins/user-settings-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/user-settings-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/user-settings-common/README.md b/plugins/user-settings-common/README.md new file mode 100644 index 0000000000..0b8039a5cd --- /dev/null +++ b/plugins/user-settings-common/README.md @@ -0,0 +1,5 @@ +# @backstage/plugin-user-settings-common + +Welcome to the common package for the user-settings plugin! + +_This plugin was created through the Backstage CLI_ diff --git a/plugins/user-settings-common/api-report.md b/plugins/user-settings-common/api-report.md new file mode 100644 index 0000000000..b69f4a4190 --- /dev/null +++ b/plugins/user-settings-common/api-report.md @@ -0,0 +1,13 @@ +## API Report File for "@backstage/plugin-user-settings-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +export type UserSettingsSignal = { + type: 'key-changed' | 'key-deleted'; + key: string; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/user-settings-common/catalog-info.yaml b/plugins/user-settings-common/catalog-info.yaml new file mode 100644 index 0000000000..ebbd0eec1e --- /dev/null +++ b/plugins/user-settings-common/catalog-info.yaml @@ -0,0 +1,10 @@ +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: backstage-plugin-user-settings-common + title: '@backstage/plugin-user-settings-common' + description: The Backstage common plugin to manage user settings +spec: + lifecycle: experimental + type: backstage-common-library + owner: maintainers diff --git a/plugins/user-settings-common/package.json b/plugins/user-settings-common/package.json new file mode 100644 index 0000000000..1575712a05 --- /dev/null +++ b/plugins/user-settings-common/package.json @@ -0,0 +1,37 @@ +{ + "name": "@backstage/plugin-user-settings-common", + "version": "0.0.0", + "description": "Common functionalities for the user-settings plugin", + "backstage": { + "role": "common-library" + }, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/user-settings-common" + }, + "license": "Apache-2.0", + "sideEffects": false, + "main": "src/index.ts", + "types": "src/index.ts", + "files": [ + "dist" + ], + "scripts": { + "build": "backstage-cli package build", + "clean": "backstage-cli package clean", + "lint": "backstage-cli package lint", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack", + "test": "backstage-cli package test" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + } +} diff --git a/plugins/user-settings-common/src/index.ts b/plugins/user-settings-common/src/index.ts new file mode 100644 index 0000000000..5d542de408 --- /dev/null +++ b/plugins/user-settings-common/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; diff --git a/plugins/user-settings-common/src/setupTests.ts b/plugins/user-settings-common/src/setupTests.ts new file mode 100644 index 0000000000..c7ce5c0988 --- /dev/null +++ b/plugins/user-settings-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/plugins/user-settings-common/src/types.ts b/plugins/user-settings-common/src/types.ts new file mode 100644 index 0000000000..6a49a72c93 --- /dev/null +++ b/plugins/user-settings-common/src/types.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @public */ +export type UserSettingsSignal = { + type: 'key-changed' | 'key-deleted'; + key: string; +}; diff --git a/plugins/user-settings/api-report.md b/plugins/user-settings/api-report.md index d485320fe8..cee35724bb 100644 --- a/plugins/user-settings/api-report.md +++ b/plugins/user-settings/api-report.md @@ -22,6 +22,7 @@ import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { SessionApi } from '@backstage/core-plugin-api'; +import { SignalApi } from '@backstage/plugin-signals-react'; import { StorageApi } from '@backstage/core-plugin-api'; import { StorageValueSnapshot } from '@backstage/core-plugin-api'; import { TabProps } from '@material-ui/core/Tab'; @@ -133,6 +134,7 @@ export class UserSettingsStorage implements StorageApi { discoveryApi: DiscoveryApi; errorApi: ErrorApi; identityApi: IdentityApi; + signalApi?: SignalApi; namespace?: string; }): UserSettingsStorage; // (undocumented) diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index bf636fd19b..ed9affbfe7 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -56,6 +56,8 @@ "@backstage/errors": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-signals-react": "workspace:^", + "@backstage/plugin-user-settings-common": "workspace:^", "@backstage/theme": "workspace:^", "@backstage/types": "workspace:^", "@material-ui/core": "^4.12.2", diff --git a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts index ef4d3ead5d..6b109f5ecd 100644 --- a/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts +++ b/plugins/user-settings/src/apis/StorageApi/UserSettingsStorage.ts @@ -25,7 +25,9 @@ import { } from '@backstage/core-plugin-api'; import { ResponseError } from '@backstage/errors'; import { JsonValue, Observable } from '@backstage/types'; +import { SignalApi, SignalSubscriber } from '@backstage/plugin-signals-react'; import ObservableImpl from 'zen-observable'; +import { UserSettingsSignal } from '@backstage/plugin-user-settings-common'; const JSON_HEADERS = { 'Content-Type': 'application/json; charset=utf-8', @@ -57,6 +59,7 @@ export class UserSettingsStorage implements StorageApi { private readonly errorApi: ErrorApi, private readonly identityApi: IdentityApi, private readonly fallback: WebStorage, + private readonly signalApi?: SignalApi, ) {} static create(options: { @@ -64,6 +67,7 @@ export class UserSettingsStorage implements StorageApi { discoveryApi: DiscoveryApi; errorApi: ErrorApi; identityApi: IdentityApi; + signalApi?: SignalApi; namespace?: string; }): UserSettingsStorage { return new UserSettingsStorage( @@ -76,6 +80,7 @@ export class UserSettingsStorage implements StorageApi { namespace: options.namespace, errorApi: options.errorApi, }), + options.signalApi, ); } @@ -145,15 +150,33 @@ export class UserSettingsStorage implements StorageApi { this.observables.set( key, new ObservableImpl>(subscriber => { + let signalSubscription: SignalSubscriber | undefined; this.subscribers.add(subscriber); - // TODO(freben): Introduce server polling or similar, to ensure that different devices update when values change - Promise.resolve() - .then(() => this.get(key)) - .then(snapshot => subscriber.next(snapshot)) - .catch(error => this.errorApi.post(error)); + const updateSnapshot = () => { + Promise.resolve() + .then(() => this.get(key)) + .then(snapshot => subscriber.next(snapshot)) + .catch(error => this.errorApi.post(error)); + }; + + if (this.signalApi) { + signalSubscription = this.signalApi.subscribe( + `user-settings`, + (msg: UserSettingsSignal) => { + if (msg.key === key) { + updateSnapshot(); + } + }, + ); + } + + updateSnapshot(); return () => { + if (signalSubscription) { + signalSubscription.unsubscribe(); + } this.subscribers.delete(subscriber); }; }).filter(({ key: messageKey }) => messageKey === key), diff --git a/yarn.lock b/yarn.lock index f6552b886d..ee4185b5b4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19,6 +19,157 @@ __metadata: languageName: node linkType: hard +"@algolia/cache-browser-local-storage@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/cache-browser-local-storage@npm:4.23.2" + dependencies: + "@algolia/cache-common": 4.23.2 + checksum: 3b6b09666ba38f3675927f8193928e168becbb77fd3f0b27d1f7a94540be81c9837950c9e82a08502f48cff36938602ff8067575e6c12aea3869525ea3dcf69a + languageName: node + linkType: hard + +"@algolia/cache-common@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/cache-common@npm:4.23.2" + checksum: 45cbf8feafbd13219982c178c3173c22f836e7d1b4bfc87ce346e7d6a565d45c822b3ad301afec120a1131f91f31f9c078ae897b917885277a952a9cc67515e2 + languageName: node + linkType: hard + +"@algolia/cache-in-memory@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/cache-in-memory@npm:4.23.2" + dependencies: + "@algolia/cache-common": 4.23.2 + checksum: a89ed4320e94825effd647124fdfef18a2beee3942efa7fe865d071be04de096a8b3835cd0d872ba349681770ce9c8cf12720b9f74b0124db67b715f64c6a8fa + languageName: node + linkType: hard + +"@algolia/client-account@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-account@npm:4.23.2" + dependencies: + "@algolia/client-common": 4.23.2 + "@algolia/client-search": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: fa180f2c9c25e455d3094627586fc880c1b88c5bb0eba7ce663792d416d887f35108202e2a7279abd4a3df7c75bf0ef7fe63c21243fe7324970b350e654a0471 + languageName: node + linkType: hard + +"@algolia/client-analytics@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-analytics@npm:4.23.2" + dependencies: + "@algolia/client-common": 4.23.2 + "@algolia/client-search": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: 51302bf7dbdfd97a88e4a181649d09a848f8b8a167ef55a0f951f00e3091f01c27aa8541698ef08fe029d36558447e983361bef880522c1ccc18e852db2a905c + languageName: node + linkType: hard + +"@algolia/client-common@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-common@npm:4.23.2" + dependencies: + "@algolia/requester-common": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: 0d36e794e682794d8831a2a92050a8fdd4f9a50e80debd41f608b4401c61fc9c96ec68b23385c474b34cc6bc2dc9468e418c07274878507e8fabdf73dd534322 + languageName: node + linkType: hard + +"@algolia/client-personalization@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-personalization@npm:4.23.2" + dependencies: + "@algolia/client-common": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: b8fc0fe3922f8db8b13572d3eb4f472defa277261e5cbf6b01d1a59ff470b88380f1b569c3abb25e43ec1abdcfd276c199ffd336464904cdfa65a346e309853e + languageName: node + linkType: hard + +"@algolia/client-search@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/client-search@npm:4.23.2" + dependencies: + "@algolia/client-common": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: e8eee05b362c84b33a7f4ba5e3af5274a3b4dde9cf20d4c755dce0f31715495f982574c84cc98f6b67d92229a06f9d5f9588b4d8659413131d4a589a3bd9a3f4 + languageName: node + linkType: hard + +"@algolia/logger-common@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/logger-common@npm:4.23.2" + checksum: da3c48adce896c91dd7a9770f8e6378394d4c7f0cd6f50710d340e411be0c8c52cd731488822ebec2926d2cbba7d0674dead23255f43bdf3f811f10a68c80f5e + languageName: node + linkType: hard + +"@algolia/logger-console@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/logger-console@npm:4.23.2" + dependencies: + "@algolia/logger-common": 4.23.2 + checksum: d3c82c5a6a15399621898bf8206a636be0c7edab59c22fd48c2590d77804365266a87e721b6ceee9ff97a8e5a7cd1b9353cf56dd7b3ac31b5ff36e7634decd13 + languageName: node + linkType: hard + +"@algolia/recommend@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/recommend@npm:4.23.2" + dependencies: + "@algolia/cache-browser-local-storage": 4.23.2 + "@algolia/cache-common": 4.23.2 + "@algolia/cache-in-memory": 4.23.2 + "@algolia/client-common": 4.23.2 + "@algolia/client-search": 4.23.2 + "@algolia/logger-common": 4.23.2 + "@algolia/logger-console": 4.23.2 + "@algolia/requester-browser-xhr": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/requester-node-http": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: fa2b81656ac9aa557e06b57b3acde558d29da84ce45fddbe083145ec22cc540653f24436df2ecd24ffcfaf7c6115862833e76f6e745dad64b19a5a4a8aa36330 + languageName: node + linkType: hard + +"@algolia/requester-browser-xhr@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/requester-browser-xhr@npm:4.23.2" + dependencies: + "@algolia/requester-common": 4.23.2 + checksum: a16bdcebac7febd82052fab2bb9caef6c3e84a94092c0b70f2ea3aefff825099f6c6aa02bfeaacff6c5157e3bb89bfcfd690746e34aeb988272d64c4e31fb560 + languageName: node + linkType: hard + +"@algolia/requester-common@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/requester-common@npm:4.23.2" + checksum: a57da6b4a675b2176aa66d3148939e023f8fb2eb1e0a8830e4e07d2ecf4edae3416ee8614dd5bae4b8a1fc40fc3a418db24180a6a9cc64b227312e00512fc456 + languageName: node + linkType: hard + +"@algolia/requester-node-http@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/requester-node-http@npm:4.23.2" + dependencies: + "@algolia/requester-common": 4.23.2 + checksum: 3085543774fbdf77f043d22ec9528815408e103a9c628f84c702338f16bcbd1cf818d70c7fcb3a11c4542e5b264c56a7e30840c56fd895c5645f72ba16808fc8 + languageName: node + linkType: hard + +"@algolia/transporter@npm:4.23.2": + version: 4.23.2 + resolution: "@algolia/transporter@npm:4.23.2" + dependencies: + "@algolia/cache-common": 4.23.2 + "@algolia/logger-common": 4.23.2 + "@algolia/requester-common": 4.23.2 + checksum: 8ac57c003e6c5ab39c0cd873c325746c90e6bdbe0857c4157b551678cc4efed48e9423317dfced722d094810649d1ae44210c98122d6c1bece35713adffd2b24 + languageName: node + linkType: hard + "@ampproject/remapping@npm:^2.2.0": version: 2.2.1 resolution: "@ampproject/remapping@npm:2.2.1" @@ -105,6 +256,15 @@ __metadata: languageName: node linkType: hard +"@arcanis/slice-ansi@npm:^1.1.1": + version: 1.1.1 + resolution: "@arcanis/slice-ansi@npm:1.1.1" + dependencies: + grapheme-splitter: ^1.0.4 + checksum: 14ed60cb45750d386c64229ac7bab20e10eedc193503fa4decff764162d329d6d3363ed2cd3debec833186ee54affe4f824f6e8eff531295117fd1ebda200270 + languageName: node + linkType: hard + "@ardatan/sync-fetch@npm:^0.0.1": version: 0.0.1 resolution: "@ardatan/sync-fetch@npm:0.0.1" @@ -4827,6 +4987,23 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-auth-backend-module-onelogin-provider@workspace:^, @backstage/plugin-auth-backend-module-onelogin-provider@workspace:plugins/auth-backend-module-onelogin-provider": + version: 0.0.0-use.local + resolution: "@backstage/plugin-auth-backend-module-onelogin-provider@workspace:plugins/auth-backend-module-onelogin-provider" + dependencies: + "@backstage/backend-defaults": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/plugin-auth-backend": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + express: ^4.18.2 + passport: ^0.7.0 + passport-onelogin-oauth: ^0.0.1 + supertest: ^6.3.3 + languageName: unknown + linkType: soft + "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider": version: 0.0.0-use.local resolution: "@backstage/plugin-auth-backend-module-pinniped-provider@workspace:plugins/auth-backend-module-pinniped-provider" @@ -4898,6 +5075,7 @@ __metadata: "@backstage/plugin-auth-backend-module-oauth2-proxy-provider": "workspace:^" "@backstage/plugin-auth-backend-module-oidc-provider": "workspace:^" "@backstage/plugin-auth-backend-module-okta-provider": "workspace:^" + "@backstage/plugin-auth-backend-module-onelogin-provider": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/types": "workspace:^" @@ -7660,6 +7838,8 @@ __metadata: "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-signals-node": "workspace:^" + "@backstage/plugin-user-settings-common": "workspace:^" "@backstage/types": "workspace:^" "@types/express": ^4.17.6 "@types/supertest": ^2.0.8 @@ -7672,6 +7852,14 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-user-settings-common@workspace:^, @backstage/plugin-user-settings-common@workspace:plugins/user-settings-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-user-settings-common@workspace:plugins/user-settings-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/plugin-user-settings@workspace:^, @backstage/plugin-user-settings@workspace:plugins/user-settings": version: 0.0.0-use.local resolution: "@backstage/plugin-user-settings@workspace:plugins/user-settings" @@ -7686,6 +7874,8 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-signals-react": "workspace:^" + "@backstage/plugin-user-settings-common": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" @@ -17182,6 +17372,13 @@ __metadata: languageName: node linkType: hard +"@types/emscripten@npm:^1.39.6": + version: 1.39.10 + resolution: "@types/emscripten@npm:1.39.10" + checksum: 1721da76593f9194e0b7c90a581e2d31c23bd4eb28f93030cd1dc58216cdf1e692c045274f2eedaed29c652c25c9a4dff2e503b11bd1258d07095c009a1956b1 + languageName: node + linkType: hard + "@types/es-aggregate-error@npm:^1.0.2": version: 1.0.2 resolution: "@types/es-aggregate-error@npm:1.0.2" @@ -17885,7 +18082,7 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^18.17.8": +"@types/node@npm:^18.17.15, @types/node@npm:^18.17.8": version: 18.19.8 resolution: "@types/node@npm:18.19.8" dependencies: @@ -18308,7 +18505,7 @@ __metadata: languageName: node linkType: hard -"@types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": +"@types/semver@npm:^7.1.0, @types/semver@npm:^7.3.12, @types/semver@npm:^7.5.0": version: 7.5.8 resolution: "@types/semver@npm:7.5.8" checksum: ea6f5276f5b84c55921785a3a27a3cd37afee0111dfe2bcb3e03c31819c197c782598f17f0b150a69d453c9584cd14c4c4d7b9a55d2c5e6cacd4d66fdb3b3663 @@ -18543,6 +18740,13 @@ __metadata: languageName: node linkType: hard +"@types/treeify@npm:^1.0.0": + version: 1.0.3 + resolution: "@types/treeify@npm:1.0.3" + checksum: 777e579b30a916a781e7cbad2b7a76bc5473ff7bfe7167dd6de47f80f4386df5bf3d0dc34170afb75d52e75f6ed61cc109abf2324e093c1f9ecd4e79fec58d0c + languageName: node + linkType: hard + "@types/triple-beam@npm:^1.3.2": version: 1.3.2 resolution: "@types/triple-beam@npm:1.3.2" @@ -18698,6 +18902,13 @@ __metadata: languageName: node linkType: hard +"@types/yoga-layout@npm:1.9.2": + version: 1.9.2 + resolution: "@types/yoga-layout@npm:1.9.2" + checksum: dbc3d6ab997d50fe1fcca5dd6822982c8fe586145ab648e0e97c3bc4ebc93d0b40c9edd75febaba374d61f60c1379b639f6be652965c776a901bf1068f2eac87 + languageName: node + linkType: hard + "@types/yup@npm:^0.32.0": version: 0.32.0 resolution: "@types/yup@npm:0.32.0" @@ -19382,6 +19593,144 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/builder@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/builder@npm:4.0.0" + dependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + "@yarnpkg/fslib": ^3.0.0 + chalk: ^3.0.0 + clipanion: ^4.0.0-rc.2 + esbuild: "npm:esbuild-wasm@^0.15.15" + semver: ^7.1.2 + tslib: ^2.4.0 + bin: + builder: ./lib/cli.js + checksum: 2bc2901e410e5951820c2d6fc5365ab15373b89e9f0aabce96cd6385019983d8911d495e95a4f3b927a1e18bc4b8de2e43158d9319e9a56de44d6e1ae4af9897 + languageName: node + linkType: hard + +"@yarnpkg/cli@npm:^4.0.0": + version: 4.1.1 + resolution: "@yarnpkg/cli@npm:4.1.1" + dependencies: + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/libzip": ^3.0.1 + "@yarnpkg/parsers": ^3.0.0 + "@yarnpkg/plugin-compat": ^4.0.3 + "@yarnpkg/plugin-constraints": ^4.0.2 + "@yarnpkg/plugin-dlx": ^4.0.0 + "@yarnpkg/plugin-essentials": ^4.1.1 + "@yarnpkg/plugin-exec": ^3.0.0 + "@yarnpkg/plugin-file": ^3.0.0 + "@yarnpkg/plugin-git": ^3.0.0 + "@yarnpkg/plugin-github": ^3.0.0 + "@yarnpkg/plugin-http": ^3.0.1 + "@yarnpkg/plugin-init": ^4.0.1 + "@yarnpkg/plugin-interactive-tools": ^4.0.0 + "@yarnpkg/plugin-link": ^3.0.0 + "@yarnpkg/plugin-nm": ^4.0.2 + "@yarnpkg/plugin-npm": ^3.0.1 + "@yarnpkg/plugin-npm-cli": ^4.0.2 + "@yarnpkg/plugin-pack": ^4.0.0 + "@yarnpkg/plugin-patch": ^4.0.1 + "@yarnpkg/plugin-pnp": ^4.0.2 + "@yarnpkg/plugin-pnpm": ^2.0.0 + "@yarnpkg/plugin-stage": ^4.0.0 + "@yarnpkg/plugin-typescript": ^4.0.0 + "@yarnpkg/plugin-version": ^4.0.1 + "@yarnpkg/plugin-workspace-tools": ^4.1.0 + "@yarnpkg/shell": ^4.0.0 + ci-info: ^3.2.0 + clipanion: ^4.0.0-rc.2 + semver: ^7.1.2 + tslib: ^2.4.0 + typanion: ^3.14.0 + peerDependencies: + "@yarnpkg/core": ^4.0.3 + checksum: e16567629195a88b36f744bc6d5b32d7ec5aad7ae37f00607c9ea0175028804ca75cd568a02ae8e28fce431b58fd268a593a13114ad892c2d8ed8f55d2d6768d + languageName: node + linkType: hard + +"@yarnpkg/core@npm:^4.0.0, @yarnpkg/core@npm:^4.0.3": + version: 4.0.3 + resolution: "@yarnpkg/core@npm:4.0.3" + dependencies: + "@arcanis/slice-ansi": ^1.1.1 + "@types/semver": ^7.1.0 + "@types/treeify": ^1.0.0 + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/libzip": ^3.0.1 + "@yarnpkg/parsers": ^3.0.0 + "@yarnpkg/shell": ^4.0.0 + camelcase: ^5.3.1 + chalk: ^3.0.0 + ci-info: ^3.2.0 + clipanion: ^4.0.0-rc.2 + cross-spawn: 7.0.3 + diff: ^5.1.0 + dotenv: ^16.3.1 + fast-glob: ^3.2.2 + got: ^11.7.0 + lodash: ^4.17.15 + micromatch: ^4.0.2 + p-limit: ^2.2.0 + semver: ^7.1.2 + strip-ansi: ^6.0.0 + tar: ^6.0.5 + tinylogic: ^2.0.0 + treeify: ^1.1.0 + tslib: ^2.4.0 + tunnel: ^0.0.6 + checksum: 811956ab94e9f4b3601f304312baf816322df124cf85754ef3985110c90935b1b761c8266a06c2c5633f6edc39c8a4ceffeff50f08f036d7adf19612ce3761a3 + languageName: node + linkType: hard + +"@yarnpkg/extensions@npm:^2.0.1": + version: 2.0.1 + resolution: "@yarnpkg/extensions@npm:2.0.1" + peerDependencies: + "@yarnpkg/core": ^4.0.3 + checksum: e578718513822644b0857adece75dd7a7eea5cd4106ccd5cf37bfa11d8ad04051a0f4c339d0077fe6ec7f9a769d5e782f3034f4219e2a4bc3d54087aacbcdaf6 + languageName: node + linkType: hard + +"@yarnpkg/fslib@npm:^3.0.0, @yarnpkg/fslib@npm:^3.0.1, @yarnpkg/fslib@npm:^3.0.2": + version: 3.0.2 + resolution: "@yarnpkg/fslib@npm:3.0.2" + dependencies: + tslib: ^2.4.0 + checksum: b0795df777e43eb1c2bd8aa1d6c45f88b8a1d3922ee8bf264cdca916cc6f9455551a9499761a9c1f585aa576350d84adcc315e497fc031cdbbbc1a5c71942e1b + languageName: node + linkType: hard + +"@yarnpkg/libui@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/libui@npm:3.0.0" + dependencies: + tslib: ^2.4.0 + peerDependencies: + ink: ^3.0.8 + react: ^16.8.4 + checksum: 66a0f117ee35736012aa9fbaca68e62ef0c4b1731cff3f851aabb901366aa37102186cdf2917ee73ad8c99e54f78864b9274ef49b7b90552451a8d726c2d08ae + languageName: node + linkType: hard + +"@yarnpkg/libzip@npm:^3.0.0, @yarnpkg/libzip@npm:^3.0.1": + version: 3.0.1 + resolution: "@yarnpkg/libzip@npm:3.0.1" + dependencies: + "@types/emscripten": ^1.39.6 + "@yarnpkg/fslib": ^3.0.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/fslib": ^3.0.2 + checksum: 932b1fc35bb60ca57d033aab7a940f7b1d7bf52a68bfff6c755fe26827c0f1b373fda33e373afabbae98de3ed31ae4f0e9ee2ef24588cd074b75fb814117827d + languageName: node + linkType: hard + "@yarnpkg/lockfile@npm:^1.1.0": version: 1.1.0 resolution: "@yarnpkg/lockfile@npm:1.1.0" @@ -19389,6 +19738,17 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/nm@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/nm@npm:4.0.2" + dependencies: + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/pnp": ^4.0.2 + checksum: 4a9d1a62d33230c84c3396e84e6b8a25e6b6c3ab9f9f36815b7ecf7581f5bf59abc1996f9ccf583627c177a841d09ffa31ad9cdd48ba3d500263bf12ad1ff281 + languageName: node + linkType: hard + "@yarnpkg/parsers@npm:^3.0.0": version: 3.0.0 resolution: "@yarnpkg/parsers@npm:3.0.0" @@ -19399,6 +19759,414 @@ __metadata: languageName: node linkType: hard +"@yarnpkg/plugin-compat@npm:^4.0.3": + version: 4.0.3 + resolution: "@yarnpkg/plugin-compat@npm:4.0.3" + dependencies: + "@yarnpkg/extensions": ^2.0.1 + peerDependencies: + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-patch": ^4.0.1 + checksum: 0704e4f5d2fd85cf93cbeae88ada3aaec67f390037e166e8dfa04ba1d02780ef169c3d174c2e46b1a8ada5ba3c35dfd5082ff026b638c6ca7b405a6aa7e9453b + languageName: node + linkType: hard + +"@yarnpkg/plugin-constraints@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/plugin-constraints@npm:4.0.2" + dependencies: + "@yarnpkg/fslib": ^3.0.1 + clipanion: ^4.0.0-rc.2 + lodash: ^4.17.15 + tau-prolog: ^0.2.66 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.2 + "@yarnpkg/core": ^4.0.2 + checksum: 9d357906336141d7fc8d6a2a7cf6e62ec4d6c8f8f06b3856286f7473dff2f0a03d1b21c28335b13540528a0cfcb9206062f20d2deeca78d3f3e02f41a1a0150e + languageName: node + linkType: hard + +"@yarnpkg/plugin-dlx@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-dlx@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + checksum: 239d8c8db728183e570735f4bd42c92d65eace219a38a3e7d895680030daf0f4a66004e88b7c3d57575b42b0b39801d31dd60b8d0d127bf713e645433c847ea4 + languageName: node + linkType: hard + +"@yarnpkg/plugin-essentials@npm:^4.1.1": + version: 4.1.1 + resolution: "@yarnpkg/plugin-essentials@npm:4.1.1" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/parsers": ^3.0.0 + ci-info: ^3.2.0 + clipanion: ^4.0.0-rc.2 + enquirer: ^2.3.6 + lodash: ^4.17.15 + micromatch: ^4.0.2 + semver: ^7.1.2 + tslib: ^2.4.0 + typanion: ^3.14.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.1 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-git": ^3.0.0 + checksum: 0992309971e90f05b795b04396668b9949db7e532035f4ce395378e698b5145048f1386a451db1978f59f8523636000fde2defa342ff1b3c38457714151119d4 + languageName: node + linkType: hard + +"@yarnpkg/plugin-exec@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-exec@npm:3.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + checksum: 66ae5c6b48607a0fa7095bcea4304c384628b537cf02440d2ccc35847aeaeac07d09b7b215870000c048d0ff421a30771c5c57592ae2276c3ca630f7fd57fb2a + languageName: node + linkType: hard + +"@yarnpkg/plugin-file@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-file@npm:3.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + "@yarnpkg/libzip": ^3.0.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + checksum: 149e57f555666b77eaf8e4035629f28bfbb026d20dc97720b7b8ee01781639ac29db5e27212a6b4980dd67097da4baa6297786c964f1e0321137489cdcb1fd31 + languageName: node + linkType: hard + +"@yarnpkg/plugin-git@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-git@npm:3.0.0" + dependencies: + "@types/semver": ^7.1.0 + "@yarnpkg/fslib": ^3.0.0 + clipanion: ^4.0.0-rc.2 + git-url-parse: ^13.1.0 + lodash: ^4.17.15 + semver: ^7.1.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + checksum: d75ed9d339905e81d5ffb8d0cac5aa2cf2e106d322c4dc64acac0e53a00e6c7cd5fd931d7373b1d944d11fd4f4685188e5f445fce34394a627be77eade39b769 + languageName: node + linkType: hard + +"@yarnpkg/plugin-github@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-github@npm:3.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + "@yarnpkg/plugin-git": ^3.0.0 + checksum: a8aa25ac79dfb547960446ea00e1afa65f14c5032a926b250f20d8f21e1a8b212aab90e7ae2830655c2a2257b9f989a8222dbdabcbc46be16796fa36e7aa7abe + languageName: node + linkType: hard + +"@yarnpkg/plugin-http@npm:^3.0.1": + version: 3.0.1 + resolution: "@yarnpkg/plugin-http@npm:3.0.1" + dependencies: + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.2 + checksum: 928d22cf37ff90501fbbc48ddff162c63e254100fe5f01c46b979458514f14d892f577047e90ced997e4c938edbe4b65b6580f9f5c0390b6b28bce1f0b2f4804 + languageName: node + linkType: hard + +"@yarnpkg/plugin-init@npm:^4.0.1": + version: 4.0.1 + resolution: "@yarnpkg/plugin-init@npm:4.0.1" + dependencies: + "@yarnpkg/fslib": ^3.0.1 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.2 + "@yarnpkg/core": ^4.0.2 + checksum: d419f4e021f74b249be87b768ce10256407872d3a2af92dc51e92f3b5f779f2447af45483d76641651bac6e474f5c3fd275e4764422848bfaeb19ce7e50d97f3 + languageName: node + linkType: hard + +"@yarnpkg/plugin-interactive-tools@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-interactive-tools@npm:4.0.0" + dependencies: + "@yarnpkg/libui": ^3.0.0 + algoliasearch: ^4.2.0 + clipanion: ^4.0.0-rc.2 + diff: ^5.1.0 + ink: ^3.0.8 + ink-text-input: ^4.0.1 + react: ^16.13.1 + semver: ^7.1.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + "@yarnpkg/plugin-essentials": ^4.0.0 + checksum: 43a50edf1e21582d0fd758077c787228e6145fa235e726bb4b8e2bdbcbd2ec0118ec63bc2ab6d3342ad2c06dc713bc871fa33bbb17de75d0c0c437daad2a666b + languageName: node + linkType: hard + +"@yarnpkg/plugin-link@npm:^3.0.0": + version: 3.0.0 + resolution: "@yarnpkg/plugin-link@npm:3.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.0 + checksum: 48bdb0e7ac8f9544999237d90f8b64163c1959d9e6f6a67d14799ef1c6533d5f30c7fa896129ed7b7ac693a9ce111646878ec4f505a39f5494df5db34cc372c4 + languageName: node + linkType: hard + +"@yarnpkg/plugin-nm@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/plugin-nm@npm:4.0.2" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/libzip": ^3.0.1 + "@yarnpkg/nm": ^4.0.2 + "@yarnpkg/parsers": ^3.0.0 + "@yarnpkg/plugin-pnp": ^4.0.2 + "@yarnpkg/pnp": ^4.0.2 + "@zkochan/cmd-shim": ^5.1.0 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.0 + "@yarnpkg/core": ^4.0.3 + checksum: d7fad715a126dee644d3cb9a64fb6aac6ac1e179f08d72853b5247460a1033d81e7070f35bff196b16b0e0816c7a4e1e211f9a596c5af07ff6f84fff4a23cd61 + languageName: node + linkType: hard + +"@yarnpkg/plugin-npm-cli@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/plugin-npm-cli@npm:4.0.2" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + clipanion: ^4.0.0-rc.2 + enquirer: ^2.3.6 + micromatch: ^4.0.2 + semver: ^7.1.2 + tslib: ^2.4.0 + typanion: ^3.14.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.0 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-npm": ^3.0.1 + "@yarnpkg/plugin-pack": ^4.0.0 + checksum: af338409d242045830276e78740b011afefb0950699cfb04b895b6962c9ba6a2a11e72eddbc04b3a568d5b0cb153ae68e22bbecfd34eedd5559681c18ac7e0c0 + languageName: node + linkType: hard + +"@yarnpkg/plugin-npm@npm:^3.0.1": + version: 3.0.1 + resolution: "@yarnpkg/plugin-npm@npm:3.0.1" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + enquirer: ^2.3.6 + lodash: ^4.17.15 + semver: ^7.1.2 + ssri: ^6.0.1 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-pack": ^4.0.0 + checksum: 5b415fe84dfc306731c858c99523515766fcfbf75eb8fe17e2d0d2bd4fde525004df6477d71b418f4cc46f711b557feba4854d6216d49a2d1436ff4096c96dac + languageName: node + linkType: hard + +"@yarnpkg/plugin-pack@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-pack@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + clipanion: ^4.0.0-rc.2 + micromatch: ^4.0.2 + tar-stream: ^2.0.1 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + checksum: 566cd532e83f135d5d4848ed7d392c9136dda1bc38470bf26cbfdf615ed0a3410e9c01373ae42ce747c906734f02b25c75df636f425974a3394232c730f9e250 + languageName: node + linkType: hard + +"@yarnpkg/plugin-patch@npm:^4.0.1": + version: 4.0.1 + resolution: "@yarnpkg/plugin-patch@npm:4.0.1" + dependencies: + "@yarnpkg/fslib": ^3.0.1 + "@yarnpkg/libzip": ^3.0.0 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.2 + "@yarnpkg/core": ^4.0.2 + checksum: c534b80facd573ddc6888d7a9b739a309295a600c105415ce6caf9b7ab5f2ca6983ba932fb35acca7de7d17e58fc4396cf28d051624007c97decc3c10c389d57 + languageName: node + linkType: hard + +"@yarnpkg/plugin-pnp@npm:^4.0.0, @yarnpkg/plugin-pnp@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/plugin-pnp@npm:4.0.2" + dependencies: + "@yarnpkg/fslib": ^3.0.1 + "@yarnpkg/plugin-stage": ^4.0.0 + "@yarnpkg/pnp": ^4.0.1 + clipanion: ^4.0.0-rc.2 + micromatch: ^4.0.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.2 + "@yarnpkg/core": ^4.0.2 + checksum: 29656bef3ee4c151172f64475a782efde77df3df996c397c34aa633deb9f9e3b562c18c6ed32b8555598adde062b6cd598bc630c28f4648498abf84652a76d6b + languageName: node + linkType: hard + +"@yarnpkg/plugin-pnpm@npm:^2.0.0": + version: 2.0.0 + resolution: "@yarnpkg/plugin-pnpm@npm:2.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + "@yarnpkg/plugin-pnp": ^4.0.0 + "@yarnpkg/plugin-stage": ^4.0.0 + clipanion: ^4.0.0-rc.2 + p-limit: ^2.2.0 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + checksum: 5ec39a8f431eda3a8877e286a9ab88c719b3451d1a5009acaaf2370321288d40d8dc34495162eb89573aa8e00bddc6b6563bebfce459f9efe24589cbe5f9d4e9 + languageName: node + linkType: hard + +"@yarnpkg/plugin-stage@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-stage@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + clipanion: ^4.0.0-rc.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + checksum: b315ef24433151161cc931bf3ad72db08f7a2a611ee4a83da1fa96278384aee6d62c250314bcaabd41d0522b328ad4094a13bcf73d14babb6b03cc2ea7656761 + languageName: node + linkType: hard + +"@yarnpkg/plugin-typescript@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/plugin-typescript@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + "@yarnpkg/plugin-pack": ^4.0.0 + algoliasearch: ^4.2.0 + semver: ^7.1.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.0.0 + "@yarnpkg/core": ^4.0.0 + "@yarnpkg/plugin-essentials": ^4.0.0 + checksum: 7ce40990039149acd8e62c6e150f0201638beb13cc78675a734b9024db22a6d6914dce090e805dc476795972897a849c154aa0371262dd8dec9743ac2df53944 + languageName: node + linkType: hard + +"@yarnpkg/plugin-version@npm:^4.0.1": + version: 4.0.1 + resolution: "@yarnpkg/plugin-version@npm:4.0.1" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + "@yarnpkg/libui": ^3.0.0 + "@yarnpkg/parsers": ^3.0.0 + clipanion: ^4.0.0-rc.2 + ink: ^3.0.8 + lodash: ^4.17.15 + react: ^16.13.1 + semver: ^7.1.2 + tslib: ^2.4.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.0 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-git": ^3.0.0 + checksum: d155f675f2298f50c34f864d1598a9de8a10d6168909aa77904941f08212d3d23de0cfb5e6ca80f8ab738a3f5593a0d8046f1f5f7e8c6b3f3d4b2541b6e5625b + languageName: node + linkType: hard + +"@yarnpkg/plugin-workspace-tools@npm:^4.1.0": + version: 4.1.0 + resolution: "@yarnpkg/plugin-workspace-tools@npm:4.1.0" + dependencies: + "@yarnpkg/fslib": ^3.0.2 + clipanion: ^4.0.0-rc.2 + micromatch: ^4.0.2 + p-limit: ^2.2.0 + tslib: ^2.4.0 + typanion: ^3.14.0 + peerDependencies: + "@yarnpkg/cli": ^4.1.0 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/plugin-git": ^3.0.0 + checksum: 8ee82796e1f86267827bf9fd25b07e63914732817fdcf8debe9d60e8e1ead87f79c8fb7b9bc60a78e097a1920579fb044b8f37aa28a3b5e7902893fc7e712957 + languageName: node + linkType: hard + +"@yarnpkg/pnp@npm:^4.0.1, @yarnpkg/pnp@npm:^4.0.2": + version: 4.0.2 + resolution: "@yarnpkg/pnp@npm:4.0.2" + dependencies: + "@types/node": ^18.17.15 + "@yarnpkg/fslib": ^3.0.2 + checksum: 7efd3702e83495b22452f79e56fc753f71e9f57613acaa775f64ec026b585cc8d3d6e2be6afeed9f6fbaa6034814eb25b70bc8a451c1f860100143d6e0e59c75 + languageName: node + linkType: hard + +"@yarnpkg/shell@npm:^4.0.0": + version: 4.0.0 + resolution: "@yarnpkg/shell@npm:4.0.0" + dependencies: + "@yarnpkg/fslib": ^3.0.0 + "@yarnpkg/parsers": ^3.0.0 + chalk: ^3.0.0 + clipanion: ^4.0.0-rc.2 + cross-spawn: 7.0.3 + fast-glob: ^3.2.2 + micromatch: ^4.0.2 + tslib: ^2.4.0 + bin: + shell: ./lib/cli.js + checksum: 8497e278b1d3d0ffe324a3b9c878ca7165bbbe4d182f5ecb02f1bfaaf4dd18c8aaa54c33ee17bb37eb09173816dc4617b70c3fe0925f5fb99749687e2650b7a2 + languageName: node + linkType: hard + +"@zkochan/cmd-shim@npm:^5.1.0": + version: 5.4.1 + resolution: "@zkochan/cmd-shim@npm:5.4.1" + dependencies: + cmd-extension: ^1.0.2 + graceful-fs: ^4.2.10 + is-windows: ^1.0.2 + checksum: 418b4b3a5fb36960f9a96adb6ba3946be0494ab06be1959c8b2805fe37f7ebca9d185108138e30c7ff193eac74eebd10a361eca949a608c9e16afbffd6a48482 + languageName: node + linkType: hard + "@zkochan/retry@npm:^0.2.0": version: 0.2.0 resolution: "@zkochan/retry@npm:0.2.0" @@ -19685,6 +20453,29 @@ __metadata: languageName: node linkType: hard +"algoliasearch@npm:^4.2.0": + version: 4.23.2 + resolution: "algoliasearch@npm:4.23.2" + dependencies: + "@algolia/cache-browser-local-storage": 4.23.2 + "@algolia/cache-common": 4.23.2 + "@algolia/cache-in-memory": 4.23.2 + "@algolia/client-account": 4.23.2 + "@algolia/client-analytics": 4.23.2 + "@algolia/client-common": 4.23.2 + "@algolia/client-personalization": 4.23.2 + "@algolia/client-search": 4.23.2 + "@algolia/logger-common": 4.23.2 + "@algolia/logger-console": 4.23.2 + "@algolia/recommend": 4.23.2 + "@algolia/requester-browser-xhr": 4.23.2 + "@algolia/requester-common": 4.23.2 + "@algolia/requester-node-http": 4.23.2 + "@algolia/transporter": 4.23.2 + checksum: 25a3303a5aff34a049c9d115d140650b4178b4cf2d78b23f4e8bdae7caa5e1a61d22c888a12d162ac22cfeba99e2579b4c17b03ccd33f5b5446c9db8f10ff712 + languageName: node + linkType: hard + "alphanum-sort@npm:^1.0.2": version: 1.0.2 resolution: "alphanum-sort@npm:1.0.2" @@ -20343,6 +21134,13 @@ __metadata: languageName: node linkType: hard +"auto-bind@npm:4.0.0": + version: 4.0.0 + resolution: "auto-bind@npm:4.0.0" + checksum: 00cad71cce5742faccb7dd65c1b55ebc4f45add4b0c9a1547b10b05bab22813230133b0c892c67ba3eb969a4524710c5e43cc45c72898ec84e56f3a596e7a04f + languageName: node + linkType: hard + "autolinker@npm:^3.11.0": version: 3.16.2 resolution: "autolinker@npm:3.16.2" @@ -21823,7 +22621,7 @@ __metadata: languageName: node linkType: hard -"cli-boxes@npm:^2.2.1": +"cli-boxes@npm:^2.2.0, cli-boxes@npm:^2.2.1": version: 2.2.1 resolution: "cli-boxes@npm:2.2.1" checksum: be79f8ec23a558b49e01311b39a1ea01243ecee30539c880cf14bf518a12e223ef40c57ead0cb44f509bffdffc5c129c746cd50d863ab879385370112af4f585 @@ -21889,6 +22687,16 @@ __metadata: languageName: node linkType: hard +"cli-truncate@npm:^2.1.0": + version: 2.1.0 + resolution: "cli-truncate@npm:2.1.0" + dependencies: + slice-ansi: ^3.0.0 + string-width: ^4.2.0 + checksum: bf1e4e6195392dc718bf9cd71f317b6300dc4a9191d052f31046b8773230ece4fa09458813bf0e3455a5e68c0690d2ea2c197d14a8b85a7b5e01c97f4b5feb5d + languageName: node + linkType: hard + "cli-truncate@npm:^4.0.0": version: 4.0.0 resolution: "cli-truncate@npm:4.0.0" @@ -21920,6 +22728,17 @@ __metadata: languageName: node linkType: hard +"clipanion@npm:^4.0.0-rc.2": + version: 4.0.0-rc.3 + resolution: "clipanion@npm:4.0.0-rc.3" + dependencies: + typanion: ^3.8.0 + peerDependencies: + typanion: "*" + checksum: a5e6201e5a7fdb93dbe5f61d158d3e3d23e3164c38a7c679c7dcc599e10ee339d249c33fd8729ba8ffd0a376206ed14aac2e5472624ecaf79041fac3baa73b9b + languageName: node + linkType: hard + "cliui@npm:7.0.4, cliui@npm:^7.0.2": version: 7.0.4 resolution: "cliui@npm:7.0.4" @@ -22033,6 +22852,13 @@ __metadata: languageName: node linkType: hard +"cmd-extension@npm:^1.0.2": + version: 1.0.2 + resolution: "cmd-extension@npm:1.0.2" + checksum: 4cbcdd53196a3c1db3484f67aa49ed83c0e6069713f60193a94d747cb84050e8e64d688673aa5159cf0184e054cb806ceb6119e45744f721cbd3a09a3e7038cb + languageName: node + linkType: hard + "cmd-shim@npm:^4.0.1": version: 4.1.0 resolution: "cmd-shim@npm:4.1.0" @@ -22056,6 +22882,15 @@ __metadata: languageName: node linkType: hard +"code-excerpt@npm:^3.0.0": + version: 3.0.0 + resolution: "code-excerpt@npm:3.0.0" + dependencies: + convert-to-spaces: ^1.0.1 + checksum: fa3a8ed15967076a43a4093b0c824cf0ada15d9aab12ea3c028851b72a69b56495aac1eadf18c3b6ae4baf0a95bb1e1faa9dbeeb0a2b2b5ae058da23328e9dd8 + languageName: node + linkType: hard + "codemirror-graphql@npm:^2.0.10": version: 2.0.11 resolution: "codemirror-graphql@npm:2.0.11" @@ -22261,6 +23096,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:7.2.0, commander@npm:^7.2.0": + version: 7.2.0 + resolution: "commander@npm:7.2.0" + checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc + languageName: node + linkType: hard + "commander@npm:8.3.0, commander@npm:^8.3.0": version: 8.3.0 resolution: "commander@npm:8.3.0" @@ -22303,13 +23145,6 @@ __metadata: languageName: node linkType: hard -"commander@npm:^7.2.0": - version: 7.2.0 - resolution: "commander@npm:7.2.0" - checksum: 53501cbeee61d5157546c0bef0fedb6cdfc763a882136284bed9a07225f09a14b82d2a84e7637edfd1a679fb35ed9502fd58ef1d091e6287f60d790147f68ddc - languageName: node - linkType: hard - "common-ancestor-path@npm:^1.0.1": version: 1.0.1 resolution: "common-ancestor-path@npm:1.0.1" @@ -22632,6 +23467,13 @@ __metadata: languageName: node linkType: hard +"convert-to-spaces@npm:^1.0.1": + version: 1.0.2 + resolution: "convert-to-spaces@npm:1.0.2" + checksum: e73f2ae39eb2b184f0796138eaab9c088b03b94937377d31be5b2282aef6a6ccce6b46f51bd99b3b7dfc70f516e2a6b16c0dd911883bfadf8d1073f462480224 + languageName: node + linkType: hard + "cookie-parser@npm:^1.4.5, cookie-parser@npm:^1.4.6": version: 1.4.6 resolution: "cookie-parser@npm:1.4.6" @@ -22966,6 +23808,17 @@ __metadata: languageName: node linkType: hard +"cross-spawn@npm:7.0.3, cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": + version: 7.0.3 + resolution: "cross-spawn@npm:7.0.3" + dependencies: + path-key: ^3.1.0 + shebang-command: ^2.0.0 + which: ^2.0.1 + checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 + languageName: node + linkType: hard + "cross-spawn@npm:^5.1.0": version: 5.1.0 resolution: "cross-spawn@npm:5.1.0" @@ -22990,17 +23843,6 @@ __metadata: languageName: node linkType: hard -"cross-spawn@npm:^7.0.0, cross-spawn@npm:^7.0.1, cross-spawn@npm:^7.0.2, cross-spawn@npm:^7.0.3": - version: 7.0.3 - resolution: "cross-spawn@npm:7.0.3" - dependencies: - path-key: ^3.1.0 - shebang-command: ^2.0.0 - which: ^2.0.1 - checksum: 671cc7c7288c3a8406f3c69a3ae2fc85555c04169e9d611def9a675635472614f1c0ed0ef80955d5b6d4e724f6ced67f0ad1bb006c2ea643488fcfef994d7f52 - languageName: node - linkType: hard - "crosspath@npm:^2.0.0": version: 2.0.0 resolution: "crosspath@npm:2.0.0" @@ -24324,10 +25166,10 @@ __metadata: languageName: node linkType: hard -"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3": - version: 16.3.1 - resolution: "dotenv@npm:16.3.1" - checksum: 15d75e7279018f4bafd0ee9706593dd14455ddb71b3bcba9c52574460b7ccaf67d5cf8b2c08a5af1a9da6db36c956a04a1192b101ee102a3e0cf8817bbcf3dfd +"dotenv@npm:^16.0.0, dotenv@npm:^16.0.3, dotenv@npm:^16.3.1": + version: 16.4.5 + resolution: "dotenv@npm:16.4.5" + checksum: 301a12c3d44fd49888b74eb9ccf9f07a1f5df43f489e7fcb89647a2edcd84c42d6bc349dc8df099cd18f07c35c7b04685c1a4f3e6a6a9e6b30f8d48c15b7f49c languageName: node linkType: hard @@ -24619,12 +25461,13 @@ __metadata: languageName: node linkType: hard -"enquirer@npm:^2.3.0": - version: 2.3.6 - resolution: "enquirer@npm:2.3.6" +"enquirer@npm:^2.3.0, enquirer@npm:^2.3.6": + version: 2.4.1 + resolution: "enquirer@npm:2.4.1" dependencies: ansi-colors: ^4.1.1 - checksum: 1c0911e14a6f8d26721c91e01db06092a5f7675159f0261d69c403396a385afd13dd76825e7678f66daffa930cfaa8d45f506fb35f818a2788463d022af1b884 + strip-ansi: ^6.0.1 + checksum: f080f11a74209647dbf347a7c6a83c8a47ae1ebf1e75073a808bc1088eb780aa54075bfecd1bcdb3e3c724520edb8e6ee05da031529436b421b71066fcc48cb5 languageName: node linkType: hard @@ -25079,6 +25922,15 @@ __metadata: languageName: node linkType: hard +"esbuild@npm:esbuild-wasm@^0.15.15": + version: 0.15.18 + resolution: "esbuild-wasm@npm:0.15.18" + bin: + esbuild: bin/esbuild + checksum: 8e809acd497ee2170f0bb6bd7296d0b04f5ddfa79fb4ee1fb8431523822805a07b4a4e671efa06af6d4f078a876b10c7504f8d170808c8033c8a086f3075cb61 + languageName: node + linkType: hard + "escalade@npm:^3.1.1": version: 3.1.1 resolution: "escalade@npm:3.1.1" @@ -26180,7 +27032,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.3.2, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": +"fast-glob@npm:3.3.2, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.12, fast-glob@npm:^3.2.2, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -26387,6 +27239,13 @@ __metadata: languageName: node linkType: hard +"figgy-pudding@npm:^3.5.1": + version: 3.5.2 + resolution: "figgy-pudding@npm:3.5.2" + checksum: 4090bd66193693dcda605e44d6b8715d8fb5c92a67acd57826e55cf816a342f550d57e5638f822b39366e1b2fdb244e99b3068a37213aa1d6c1bf602b8fde5ae + languageName: node + linkType: hard + "figures@npm:^3.0.0, figures@npm:^3.2.0": version: 3.2.0 resolution: "figures@npm:3.2.0" @@ -27439,6 +28298,13 @@ __metadata: languageName: node linkType: hard +"globalyzer@npm:0.1.0": + version: 0.1.0 + resolution: "globalyzer@npm:0.1.0" + checksum: 419a0f95ba542534fac0842964d31b3dc2936a479b2b1a8a62bad7e8b61054faa9b0a06ad9f2e12593396b9b2621cac93358d9b3071d33723fb1778608d358a1 + languageName: node + linkType: hard + "globby@npm:^11.0.0, globby@npm:^11.0.1, globby@npm:^11.0.3, globby@npm:^11.0.4, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" @@ -27466,6 +28332,13 @@ __metadata: languageName: node linkType: hard +"globrex@npm:^0.1.2": + version: 0.1.2 + resolution: "globrex@npm:0.1.2" + checksum: adca162494a176ce9ecf4dd232f7b802956bb1966b37f60c15e49d2e7d961b66c60826366dc2649093cad5a0d69970cfa8875bd1695b5a1a2f33dcd2aa88da3c + languageName: node + linkType: hard + "goober@npm:^2.0.33": version: 2.1.14 resolution: "goober@npm:2.1.14" @@ -27524,9 +28397,9 @@ __metadata: languageName: node linkType: hard -"got@npm:^11.8.3": - version: 11.8.5 - resolution: "got@npm:11.8.5" +"got@npm:^11.7.0, got@npm:^11.8.3": + version: 11.8.6 + resolution: "got@npm:11.8.6" dependencies: "@sindresorhus/is": ^4.0.0 "@szmarczak/http-timer": ^4.0.5 @@ -27539,7 +28412,7 @@ __metadata: lowercase-keys: ^2.0.0 p-cancelable: ^2.0.0 responselike: ^2.0.0 - checksum: 2de8a1bbda4e9b6b2b72b2d2100bc055a59adc1740529e631f61feb44a8b9a1f9f8590941ed9da9df0090b6d6d0ed8ffee94cd9ac086ec3409b392b33440f7d2 + checksum: bbc783578a8d5030c8164ef7f57ce41b5ad7db2ed13371e1944bef157eeca5a7475530e07c0aaa71610d7085474d0d96222c9f4268d41db333a17e39b463f45d languageName: node linkType: hard @@ -28672,6 +29545,56 @@ __metadata: languageName: node linkType: hard +"ink-text-input@npm:^4.0.1": + version: 4.0.3 + resolution: "ink-text-input@npm:4.0.3" + dependencies: + chalk: ^4.1.0 + type-fest: ^0.15.1 + peerDependencies: + ink: ^3.0.0-3 + react: ^16.5.2 || ^17.0.0 + checksum: 2d309ec8ca386010d467822e317389e3c60b764fd04091df063a45c31f43104fd9f4a4e71a928a2c3c3cca461a9b8a526e90439616760f0f3726507132abbac5 + languageName: node + linkType: hard + +"ink@npm:^3.0.8": + version: 3.2.0 + resolution: "ink@npm:3.2.0" + dependencies: + ansi-escapes: ^4.2.1 + auto-bind: 4.0.0 + chalk: ^4.1.0 + cli-boxes: ^2.2.0 + cli-cursor: ^3.1.0 + cli-truncate: ^2.1.0 + code-excerpt: ^3.0.0 + indent-string: ^4.0.0 + is-ci: ^2.0.0 + lodash: ^4.17.20 + patch-console: ^1.0.0 + react-devtools-core: ^4.19.1 + react-reconciler: ^0.26.2 + scheduler: ^0.20.2 + signal-exit: ^3.0.2 + slice-ansi: ^3.0.0 + stack-utils: ^2.0.2 + string-width: ^4.2.2 + type-fest: ^0.12.0 + widest-line: ^3.1.0 + wrap-ansi: ^6.2.0 + ws: ^7.5.5 + yoga-layout-prebuilt: ^1.9.6 + peerDependencies: + "@types/react": ">=16.8.0" + react: ">=16.8.0" + peerDependenciesMeta: + "@types/react": + optional: true + checksum: 35f1b733b94bf12cc0bf7acb4d3fcba9d961ede15cee9c64a7325606b74cee78e1009eaffbac127f4d7d28e758d8259dea8d0850bfacb991b8d93632f41d3fa2 + languageName: node + linkType: hard + "inline-style-parser@npm:0.1.1": version: 0.1.1 resolution: "inline-style-parser@npm:0.1.1" @@ -29519,7 +30442,7 @@ __metadata: languageName: node linkType: hard -"is-windows@npm:^1.0.0": +"is-windows@npm:^1.0.0, is-windows@npm:^1.0.2": version: 1.0.2 resolution: "is-windows@npm:1.0.2" checksum: 438b7e52656fe3b9b293b180defb4e448088e7023a523ec21a91a80b9ff8cdb3377ddb5b6e60f7c7de4fa8b63ab56e121b6705fe081b3cf1b828b0a380009ad7 @@ -34125,6 +35048,13 @@ __metadata: languageName: node linkType: hard +"node-watch@npm:0.7.3": + version: 0.7.3 + resolution: "node-watch@npm:0.7.3" + checksum: c745482f720613415153b9065383b77d21f2ef60ceabf64f779c1452b1dcbb8d08c71f650b93f3c1d84524371321f92a15fda468745872cacffec8289741d51a + languageName: node + linkType: hard + "nodemailer@npm:^6.9.13": version: 6.9.13 resolution: "nodemailer@npm:6.9.13" @@ -35462,6 +36392,13 @@ __metadata: languageName: node linkType: hard +"patch-console@npm:^1.0.0": + version: 1.0.0 + resolution: "patch-console@npm:1.0.0" + checksum: 8cd738aa470f2e9463fca35da6a19403384ac555004f698ddd3dfdb69135ab60fe9bd2edd1dbdd8c09d92c0a2190fd0f7337fe48123013baf8ffec8532885a3a + languageName: node + linkType: hard + "patch-package@npm:^8.0.0": version: 8.0.0 resolution: "patch-package@npm:8.0.0" @@ -37109,6 +38046,19 @@ __metadata: languageName: node linkType: hard +"qunit@npm:^2.8.0": + version: 2.20.1 + resolution: "qunit@npm:2.20.1" + dependencies: + commander: 7.2.0 + node-watch: 0.7.3 + tiny-glob: 0.2.9 + bin: + qunit: bin/qunit.js + checksum: ee0db04239ff7ae1ff9b67f15ab31e8c4f1e41bb5d6a41f34b19b17f836bc9414e43897516a617d08840eaa2bd71580c456e7e448d81c388dfb58198f0c26de7 + languageName: node + linkType: hard + "raf-schd@npm:^4.0.2": version: 4.0.2 resolution: "raf-schd@npm:4.0.2" @@ -37353,6 +38303,16 @@ __metadata: languageName: node linkType: hard +"react-devtools-core@npm:^4.19.1": + version: 4.28.5 + resolution: "react-devtools-core@npm:4.28.5" + dependencies: + shell-quote: ^1.6.1 + ws: ^7 + checksum: d8e4b32ffcfe1ada5c9f7decffd04afc4707a3d6261953a92b8aed1c8abe15cd57d6eb4ce711f842180a2f5c60d2947209e3c1202f7ea29303ee150c55da59e0 + languageName: node + linkType: hard + "react-dom@npm:^16.13.1": version: 16.14.0 resolution: "react-dom@npm:16.14.0" @@ -37585,6 +38545,19 @@ __metadata: languageName: node linkType: hard +"react-reconciler@npm:^0.26.2": + version: 0.26.2 + resolution: "react-reconciler@npm:0.26.2" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + scheduler: ^0.20.2 + peerDependencies: + react: ^17.0.2 + checksum: 2ebceace56f547f51eaf142becefef9cca980eae4f42d90ee5a966f54a375f5082d78b71b00c40bbd9bca69e0e0f698c7d4e81cc7373437caa19831fddc1d01b + languageName: node + linkType: hard + "react-redux@npm:^7.1.1": version: 7.2.5 resolution: "react-redux@npm:7.2.5" @@ -37930,6 +38903,17 @@ __metadata: languageName: node linkType: hard +"react@npm:^16.13.1": + version: 16.14.0 + resolution: "react@npm:16.14.0" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + prop-types: ^15.6.2 + checksum: 8484f3ecb13414526f2a7412190575fc134da785c02695eb92bb6028c930bfe1c238d7be2a125088fec663cc7cda0a3623373c46807cf2c281f49c34b79881ac + languageName: node + linkType: hard + "react@npm:^18.0.2": version: 18.2.0 resolution: "react@npm:18.2.0" @@ -38088,6 +39072,13 @@ __metadata: languageName: node linkType: hard +"readline-sync@npm:1.4.9": + version: 1.4.9 + resolution: "readline-sync@npm:1.4.9" + checksum: cedd48f422bb4c8736cbd44332bea9d3367302d0e2a63c064b97e909bbcd203099d483cee4fff4e78614a1c3c1d5a62f1c63bf2c013483072ec1ff1c8a2c5129 + languageName: node + linkType: hard + "recast@npm:^0.20.3": version: 0.20.4 resolution: "recast@npm:0.20.4" @@ -39230,6 +40221,16 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.20.2": + version: 0.20.2 + resolution: "scheduler@npm:0.20.2" + dependencies: + loose-envify: ^1.1.0 + object-assign: ^4.1.1 + checksum: c4b35cf967c8f0d3e65753252d0f260271f81a81e427241295c5a7b783abf4ea9e905f22f815ab66676f5313be0a25f47be582254db8f9241b259213e999b8fc + languageName: node + linkType: hard + "scheduler@npm:^0.23.0": version: 0.23.0 resolution: "scheduler@npm:0.23.0" @@ -39372,7 +40373,7 @@ __metadata: languageName: node linkType: hard -"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.4.0, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": +"semver@npm:^7.0.0, semver@npm:^7.1.1, semver@npm:^7.1.2, semver@npm:^7.1.3, semver@npm:^7.3.2, semver@npm:^7.3.4, semver@npm:^7.3.5, semver@npm:^7.3.7, semver@npm:^7.3.8, semver@npm:^7.4.0, semver@npm:^7.5.2, semver@npm:^7.5.3, semver@npm:^7.5.4, semver@npm:^7.6.0": version: 7.6.0 resolution: "semver@npm:7.6.0" dependencies: @@ -39625,7 +40626,7 @@ __metadata: languageName: node linkType: hard -"shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1": +"shell-quote@npm:^1.6.1, shell-quote@npm:^1.7.3, shell-quote@npm:^1.8.1": version: 1.8.1 resolution: "shell-quote@npm:1.8.1" checksum: 5f01201f4ef504d4c6a9d0d283fa17075f6770bfbe4c5850b074974c68062f37929ca61700d95ad2ac8822e14e8c4b990ca0e6e9272e64befd74ce5e19f0736b @@ -39798,6 +40799,17 @@ __metadata: languageName: node linkType: hard +"slice-ansi@npm:^3.0.0": + version: 3.0.0 + resolution: "slice-ansi@npm:3.0.0" + dependencies: + ansi-styles: ^4.0.0 + astral-regex: ^2.0.0 + is-fullwidth-code-point: ^3.0.0 + checksum: 5ec6d022d12e016347e9e3e98a7eb2a592213a43a65f1b61b74d2c78288da0aded781f665807a9f3876b9daa9ad94f64f77d7633a0458876c3a4fdc4eb223f24 + languageName: node + linkType: hard + "slice-ansi@npm:^4.0.0": version: 4.0.0 resolution: "slice-ansi@npm:4.0.0" @@ -40251,6 +41263,15 @@ __metadata: languageName: node linkType: hard +"ssri@npm:^6.0.1": + version: 6.0.2 + resolution: "ssri@npm:6.0.2" + dependencies: + figgy-pudding: ^3.5.1 + checksum: 7c2e5d442f6252559c8987b7114bcf389fe5614bf65de09ba3e6f9a57b9b65b2967de348fcc3acccff9c069adb168140dd2c5fc2f6f4a779e604a27ef1f7d551 + languageName: node + linkType: hard + "ssri@npm:^8.0.0, ssri@npm:^8.0.1": version: 8.0.1 resolution: "ssri@npm:8.0.1" @@ -40292,12 +41313,12 @@ __metadata: languageName: node linkType: hard -"stack-utils@npm:^2.0.3": - version: 2.0.5 - resolution: "stack-utils@npm:2.0.5" +"stack-utils@npm:^2.0.2, stack-utils@npm:^2.0.3": + version: 2.0.6 + resolution: "stack-utils@npm:2.0.6" dependencies: escape-string-regexp: ^2.0.0 - checksum: 76b69da0f5b48a34a0f93c98ee2a96544d2c4ca2557f7eef5ddb961d3bdc33870b46f498a84a7c4f4ffb781df639840e7ebf6639164ed4da5e1aeb659615b9c7 + checksum: 052bf4d25bbf5f78e06c1d5e67de2e088b06871fa04107ca8d3f0e9d9263326e2942c8bedee3545795fc77d787d443a538345eef74db2f8e35db3558c6f91ff7 languageName: node linkType: hard @@ -41171,7 +42192,7 @@ __metadata: languageName: node linkType: hard -"tar-stream@npm:^2.0.0, tar-stream@npm:^2.1.4, tar-stream@npm:^2.2.0": +"tar-stream@npm:^2.0.0, tar-stream@npm:^2.0.1, tar-stream@npm:^2.1.4, tar-stream@npm:^2.2.0": version: 2.2.0 resolution: "tar-stream@npm:2.2.0" dependencies: @@ -41195,7 +42216,7 @@ __metadata: languageName: node linkType: hard -"tar@npm:^6.0.2, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": +"tar@npm:^6.0.2, tar@npm:^6.0.5, tar@npm:^6.1.0, tar@npm:^6.1.11, tar@npm:^6.1.12, tar@npm:^6.1.2": version: 6.2.1 resolution: "tar@npm:6.2.1" dependencies: @@ -41216,6 +42237,16 @@ __metadata: languageName: node linkType: hard +"tau-prolog@npm:^0.2.66": + version: 0.2.81 + resolution: "tau-prolog@npm:0.2.81" + dependencies: + qunit: ^2.8.0 + readline-sync: 1.4.9 + checksum: 9771c020fe37e821f9e5321ef26f70728f885077c5885d4e8b06fd9409205b1d0ad9fd9ac02d66e56e062ce8e8bbc07a9bf8c3791b5023bfe72ed04c598dd3ef + languageName: node + linkType: hard + "tdigest@npm:^0.1.1": version: 0.1.1 resolution: "tdigest@npm:0.1.1" @@ -41480,6 +42511,16 @@ __metadata: languageName: node linkType: hard +"tiny-glob@npm:0.2.9": + version: 0.2.9 + resolution: "tiny-glob@npm:0.2.9" + dependencies: + globalyzer: 0.1.0 + globrex: ^0.1.2 + checksum: aea5801eb6663ddf77ebb74900b8f8bd9dfcfc9b6a1cc8018cb7421590c00bf446109ff45e4b64a98e6c95ddb1255a337a5d488fb6311930e2a95334151ec9c6 + languageName: node + linkType: hard + "tiny-invariant@npm:^1.0.6": version: 1.3.1 resolution: "tiny-invariant@npm:1.3.1" @@ -41494,6 +42535,13 @@ __metadata: languageName: node linkType: hard +"tinylogic@npm:^2.0.0": + version: 2.0.0 + resolution: "tinylogic@npm:2.0.0" + checksum: b966cbb41241a048095fb9e685d5e2020475fdea2c65b4ae51e5dee48964860a4505d987503c004b8a76e96b64c7da2f49954dd36c691d559c315d878ce7da29 + languageName: node + linkType: hard + "tmp@npm:^0.0.33": version: 0.0.33 resolution: "tmp@npm:0.0.33" @@ -41717,6 +42765,13 @@ __metadata: languageName: node linkType: hard +"treeify@npm:^1.1.0": + version: 1.1.0 + resolution: "treeify@npm:1.1.0" + checksum: aa00dded220c1dd052573bd6fc2c52862f09870851a284f0d3650d72bf913ba9b4f6b824f4f1ab81899bae29375f4266b07fe47cbf82343a1efa13cc09ce87af + languageName: node + linkType: hard + "treeverse@npm:^1.0.4": version: 1.0.4 resolution: "treeverse@npm:1.0.4" @@ -41958,6 +43013,13 @@ __metadata: languageName: node linkType: hard +"typanion@npm:^3.14.0, typanion@npm:^3.8.0": + version: 3.14.0 + resolution: "typanion@npm:3.14.0" + checksum: fc0590d02c13c659eb1689e8adf7777e6c00dc911377e44cd36fe1b1271cfaca71547149f12cdc275058c0de5562a14e5273adbae66d47e6e0320e36007f5912 + languageName: node + linkType: hard + "type-check@npm:^0.4.0, type-check@npm:~0.4.0": version: 0.4.0 resolution: "type-check@npm:0.4.0" @@ -41983,6 +43045,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.12.0": + version: 0.12.0 + resolution: "type-fest@npm:0.12.0" + checksum: 407d6c1a6fcc907f6124c37e977ba4966205014787a32a27579da6e47c3b1bd210b68cc1c7764d904c8aa55fb4efa6945586f9b4fae742c63ed026a4559da07d + languageName: node + linkType: hard + "type-fest@npm:^0.13.1": version: 0.13.1 resolution: "type-fest@npm:0.13.1" @@ -41990,6 +43059,13 @@ __metadata: languageName: node linkType: hard +"type-fest@npm:^0.15.1": + version: 0.15.1 + resolution: "type-fest@npm:0.15.1" + checksum: a1a0cdbd7f802d9784324f185df055739e97424ecb60914e9025574a4bc07e4a063c152e4510ebf5989de8a263220de1f6b5cf1b05f0b333dbd5b21d9b4a271b + languageName: node + linkType: hard + "type-fest@npm:^0.20.2": version: 0.20.2 resolution: "type-fest@npm:0.20.2" @@ -43848,7 +44924,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^7.4.6": +"ws@npm:^7, ws@npm:^7.4.6, ws@npm:^7.5.5": version: 7.5.9 resolution: "ws@npm:7.5.9" peerDependencies: @@ -44151,6 +45227,22 @@ __metadata: languageName: node linkType: hard +"yarn-plugin-backstage@workspace:packages/yarn-plugin": + version: 0.0.0-use.local + resolution: "yarn-plugin-backstage@workspace:packages/yarn-plugin" + dependencies: + "@backstage/backend-test-utils": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/cli-common": "workspace:^" + "@backstage/release-manifests": "workspace:^" + "@yarnpkg/builder": ^4.0.0 + "@yarnpkg/core": ^4.0.3 + "@yarnpkg/fslib": ^3.0.2 + nodemon: ^3.0.1 + semver: ^7.6.0 + languageName: unknown + linkType: soft + "yauzl@npm:^3.0.0": version: 3.1.3 resolution: "yauzl@npm:3.1.3" @@ -44239,6 +45331,15 @@ __metadata: languageName: node linkType: hard +"yoga-layout-prebuilt@npm:^1.9.6": + version: 1.10.0 + resolution: "yoga-layout-prebuilt@npm:1.10.0" + dependencies: + "@types/yoga-layout": 1.9.2 + checksum: 6954c7c7b04c585a1c974391bea4734611adb85702b5e9131549a1d3dc5b94e69bcfea34121cdaeb5e702663bf290fcce5374910128e54d1031503a57c062865 + languageName: node + linkType: hard + "yup@npm:*, yup@npm:^1.0.0": version: 1.4.0 resolution: "yup@npm:1.4.0"