diff --git a/.changeset/beige-mangos-knock.md b/.changeset/beige-mangos-knock.md deleted file mode 100644 index d8723c3f87..0000000000 --- a/.changeset/beige-mangos-knock.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-github-actions': patch ---- - -Show empty state only when workflow API call has completed diff --git a/.changeset/blue-queens-sniff.md b/.changeset/blue-queens-sniff.md new file mode 100644 index 0000000000..9a26507f92 --- /dev/null +++ b/.changeset/blue-queens-sniff.md @@ -0,0 +1,7 @@ +--- +'@backstage/app-defaults': patch +'@backstage/core-app-api': patch +'@backstage/core-plugin-api': patch +--- + +Add `FetchApi` and related `fetchApiRef` which implement fetch, with an added Backstage token header when available. diff --git a/.changeset/brave-impalas-switch.md b/.changeset/brave-impalas-switch.md new file mode 100644 index 0000000000..729a4ec96a --- /dev/null +++ b/.changeset/brave-impalas-switch.md @@ -0,0 +1,22 @@ +--- +'@backstage/backend-common': patch +--- + +Fixed bug in backend-common to allow passing of remote option in order to enable passing remote url in --config option. The remote option should be passed along with reloadIntervalSeconds from packages/backend/src/index.ts (Updated the file as well) + +These changes are needed in `packages/backend/src/index.ts` if remote URLs are desired to be passed in --config option and read and watch remote files for config. + +```diff +@@ -86,7 +86,11 @@ async function main() { + const config = await loadBackendConfig({ + argv: process.argv, + logger, ++ remote: { ++ reloadIntervalSeconds: 60 * 10 // Check remote config changes every 10 minutes. Change to your desired interval in seconds ++ } + }); ++ + const createEnv = makeCreateEnv(config); + + const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); +``` diff --git a/.changeset/cuddly-cooks-enjoy.md b/.changeset/cuddly-cooks-enjoy.md new file mode 100644 index 0000000000..d39844e7a9 --- /dev/null +++ b/.changeset/cuddly-cooks-enjoy.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +In case remote.reloadIntervalSeconds is passed, it must be a valid positive value diff --git a/.changeset/cuddly-suns-sit.md b/.changeset/cuddly-suns-sit.md new file mode 100644 index 0000000000..f1fe16add6 --- /dev/null +++ b/.changeset/cuddly-suns-sit.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-user-settings': patch +--- + +Fix undefined identity bug in UserSettingsProfileCard caused by using deprecated methods of the IdentityApi diff --git a/.changeset/eleven-baboons-sparkle.md b/.changeset/eleven-baboons-sparkle.md new file mode 100644 index 0000000000..e8c28af482 --- /dev/null +++ b/.changeset/eleven-baboons-sparkle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +added useOwnedEntities hook to get the list of entities of the logged-in user diff --git a/.changeset/funny-chefs-guess.md b/.changeset/funny-chefs-guess.md new file mode 100644 index 0000000000..66824892f2 --- /dev/null +++ b/.changeset/funny-chefs-guess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Fixed bug in OwnershipCard component where text wasn't correctly pluralized diff --git a/.changeset/gentle-humans-hope.md b/.changeset/gentle-humans-hope.md new file mode 100644 index 0000000000..b8c337cec9 --- /dev/null +++ b/.changeset/gentle-humans-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend-module-rails': patch +--- + +Add new options to rails new (force and skipTests) diff --git a/.changeset/gold-seas-wave.md b/.changeset/gold-seas-wave.md new file mode 100644 index 0000000000..ec0c6451b7 --- /dev/null +++ b/.changeset/gold-seas-wave.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-common': minor +--- + +Create catalog-common and add catalog permissions. diff --git a/.changeset/itchy-grapes-think.md b/.changeset/itchy-grapes-think.md new file mode 100644 index 0000000000..c453b24922 --- /dev/null +++ b/.changeset/itchy-grapes-think.md @@ -0,0 +1,25 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Deprecated the `CatalogClientWrapper` class. + +The default implementation of `catalogApiRef` that this plugin exposes, is now powered by the new `fetchApiRef`. The default implementation of _that_ API, in turn, has the ability to inject the user's Backstage token in requests in a similar manner to what the deprecated `CatalogClientWrapper` used to do. The latter has therefore been taken out of the default catalog API implementation. + +If you use a custom `fetchApiRef` implementation that does NOT issue tokens, or use a custom `catalogApiRef` implementation which does NOT use the default `fetchApiRef`, you can still for some time wrap your catalog API in this class to get back the old behavior: + +```ts +// Add this to your packages/app/src/plugins.ts if you want to get back the old +// catalog client behavior: +createApiFactory({ + api: catalogApiRef, + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new CatalogClientWrapper({ + client: new CatalogClient({ discoveryApi }), + identityApi, + }), +}), +``` + +But do consider migrating to making use of the `fetchApiRef` as soon as convenient, since the wrapper class will be removed in a future release. diff --git a/.changeset/lovely-drinks-kiss.md b/.changeset/lovely-drinks-kiss.md new file mode 100644 index 0000000000..f343888920 --- /dev/null +++ b/.changeset/lovely-drinks-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': patch +--- + +Add the ability to supply a custom `fetchApi`. In the default frontend app setup, this will use the `fetchApiRef` implementation. diff --git a/.changeset/neat-stingrays-decide.md b/.changeset/neat-stingrays-decide.md new file mode 100644 index 0000000000..f339d7ded6 --- /dev/null +++ b/.changeset/neat-stingrays-decide.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': minor +--- + +Removed deprecated `Keyboard` class which has been superseded by `@testing-library/user-event#userEvent` diff --git a/.changeset/perfect-apricots-raise.md b/.changeset/perfect-apricots-raise.md new file mode 100644 index 0000000000..d7a40e6343 --- /dev/null +++ b/.changeset/perfect-apricots-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': minor +--- + +Removed deprecated option `configPaths` as it has been superseded by `configTargets` diff --git a/.changeset/perfect-buses-collect.md b/.changeset/perfect-buses-collect.md new file mode 100644 index 0000000000..e0df504087 --- /dev/null +++ b/.changeset/perfect-buses-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Added OwnedEntityPicker field which displays Owned Entities in options diff --git a/.changeset/rare-ladybugs-invite.md b/.changeset/rare-ladybugs-invite.md new file mode 100644 index 0000000000..4153363b9c --- /dev/null +++ b/.changeset/rare-ladybugs-invite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Fixed the fallback identity population to correctly generate an entity reference for `userEntityRef` if no token is provided. diff --git a/.changeset/silver-books-clean.md b/.changeset/silver-books-clean.md new file mode 100644 index 0000000000..9685cc2119 --- /dev/null +++ b/.changeset/silver-books-clean.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-client': patch +--- + +Add support for passing paging parameters to the getEntities call of the catalog client diff --git a/.changeset/sixty-pandas-switch.md b/.changeset/sixty-pandas-switch.md new file mode 100644 index 0000000000..91e767e154 --- /dev/null +++ b/.changeset/sixty-pandas-switch.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-search-backend': minor +--- + +Search result location filtering + +This change introduces a filter for search results based on their location protocol. The intention is to filter out unsafe or +malicious values before they can be consumed by the frontend. By default locations must be http/https URLs (or paths). diff --git a/.changeset/ten-candles-call.md b/.changeset/ten-candles-call.md new file mode 100644 index 0000000000..8907da06bd --- /dev/null +++ b/.changeset/ten-candles-call.md @@ -0,0 +1,9 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-scaffolder': patch +--- + +In @backstage/plugin-scaffolder - When user will have one option available in hostUrl or owner - autoselect and select component should be readonly. + +in @backstage/core-components - Select component has extended API with few more props: native : boolean, disabled: boolean. native - if set to true - Select component will use native browser select picker (not rendered by Material UI lib ). +disabled - if set to true - action on component will not be possible. diff --git a/.changeset/twenty-tigers-smash.md b/.changeset/twenty-tigers-smash.md new file mode 100644 index 0000000000..3ad694fbba --- /dev/null +++ b/.changeset/twenty-tigers-smash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins-backend': patch +--- + +feature: add crumbIssuer option to Jenkins (optional) configuration, improve the UI to show a notification after executing the action re-build diff --git a/.changeset/twenty-tigers-ymash.md b/.changeset/twenty-tigers-ymash.md new file mode 100644 index 0000000000..c3e03ba75d --- /dev/null +++ b/.changeset/twenty-tigers-ymash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +feature: add crumbIssuer option to Jenkins (optional) configuration, improve the UI to show a notification after executing the action re-build diff --git a/.changeset/weak-berries-heal.md b/.changeset/weak-berries-heal.md deleted file mode 100644 index 4160d078fc..0000000000 --- a/.changeset/weak-berries-heal.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -'@backstage/plugin-apache-airflow': minor ---- - -Introduces a new plugin for the Apache Airflow workflow management platform. -This implementation has been tested with the Apache Airflow v2 API, -authenticating with basic authentication through the Backstage proxy plugin. - -Supported functionality includes: - -- Information card of version information of the Airflow instance -- Information card of instance health for the meta-database and scheduler -- Table of DAGs with meta information and status, along with a link to view - details in the Airflow UI diff --git a/.changeset/young-dodos-bake.md b/.changeset/young-dodos-bake.md new file mode 100644 index 0000000000..f620c6a1a4 --- /dev/null +++ b/.changeset/young-dodos-bake.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-plugin-api': minor +--- + +Removed deprecated types `ApiRefType` and `ApiRefsToTypes` diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..5f513b3db8 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,11 @@ +catalog: + - plugins/catalog/**/* + - plugins/catalog-*/**/* + - packages/catalog-*/**/* +scaffolder: + - plugins/scaffolder/**/* + - plugins/scaffolder-*/**/* +search: + - plugins/search/**/* + - plugins/search-*/**/* + - packages/search-*/**/* diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 4ec62923d8..5233992cf9 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -12,6 +12,7 @@ Atlassian automations autoscaling Autoscaling +autoselect Avro backrub Bigtable @@ -53,6 +54,7 @@ configs const cookiecutter cron +cronjobs css Datadog dataflow diff --git a/.github/workflows/changeset.yml b/.github/workflows/changeset.yml index 70afd15eee..a649904a01 100644 --- a/.github/workflows/changeset.yml +++ b/.github/workflows/changeset.yml @@ -10,7 +10,7 @@ jobs: name: Create Changeset PR runs-on: ubuntu-latest steps: - - uses: actions/checkout@v1 + - uses: actions/checkout@v2 - name: Install Dependencies run: yarn --frozen-lockfile - name: Create Release Pull Request diff --git a/.github/workflows/fossa.yml b/.github/workflows/fossa.yml index 9d054ff6df..3d0832f958 100644 --- a/.github/workflows/fossa.yml +++ b/.github/workflows/fossa.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v2.3.4 + uses: actions/checkout@v2 - name: Install Fossa run: "curl -H 'Cache-Control: no-cache' https://raw.githubusercontent.com/fossas/fossa-cli/master/install.sh | bash" diff --git a/.github/workflows/label.yml b/.github/workflows/label.yml new file mode 100644 index 0000000000..dc7127ab0d --- /dev/null +++ b/.github/workflows/label.yml @@ -0,0 +1,12 @@ +name: 'Pull Request Labeler' +on: + - pull_request_target + +jobs: + triage: + runs-on: ubuntu-latest + steps: + - uses: actions/labeler@v3 + with: + repo-token: '${{ secrets.GITHUB_TOKEN }}' + sync-labels: true diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index 96a2a20109..5cbf4e32ef 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -24,7 +24,7 @@ jobs: recent activity from the author. It will be closed if no further activity occurs. If you are the author and the PR has been closed, feel free to re-open the PR and continue the contribution! days-before-pr-stale: 7 - days-before-pr-close: 3 + days-before-pr-close: 5 exempt-pr-labels: reviewer-approved,awaiting-review stale-pr-label: stale operations-per-run: 100 diff --git a/README.md b/README.md index 1078631926..3e91cfe7c3 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ # [Backstage](https://backstage.io) +> 🎄 The maintainers will be taking a break over the holidays from week beginning 20th Dec. The Repository and Discord may be quieter than usual. We will be back next year, rested and restored, on Jan. 3. 🎄 + +> 🎅 Happy holidays and a Happy New Year to all, and especially those that have made this year special for Backstage! 🎅 + [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![CNCF Status](https://img.shields.io/badge/cncf%20status-sandbox-blue.svg)](https://www.cncf.io/projects) [![Main CI Build](https://github.com/backstage/backstage/workflows/Main%20Master%20Build/badge.svg)](https://github.com/backstage/backstage/actions?query=workflow%3A%22Main+Master+Build%22) diff --git a/docs/api/deprecations.md b/docs/api/deprecations.md index aca7e42d96..04c3da92bc 100644 --- a/docs/api/deprecations.md +++ b/docs/api/deprecations.md @@ -61,3 +61,125 @@ breaking change to make `theme` optional. This means that if you currently construct the themes that you pass on to `createApp` using `AppTheme` as an intermediate type, you will need to work around this in some way, for example by passing the themes to `createApp` more directly. + +### Generic Auth API Refs + +`Released 2021-12-16 in @backstage/core-plugin-api v0.3.1` + +There are four auth Utility API references in `@backstage/core-plugin-api` that +were too generic to be useful. The APIs in question are `auth0AuthApiRef`, +`oauth2ApiRef`, `oidcAuthApiRef`, and `samlAuthApiRef`. The issue with these +APIs was that they had no actual contract of what the backing auth provider was. +This made it more or less impossible to use these providers in open source +plugins in any meaningful way. We also did not want to keep these Utility API +references around just as helpers either, instead opting to remove them and let +integrators define their own APIs that are more specific to their auth provider. +This is also falls in line with a long-term goal to unify all auth providers to +not have separate frontend implementations. + +If you're currently using one of these API references for either Sign-In or +access delegation within an app, there are a couple of steps you need to take to +migrate to your own custom API. + +First, you'll need to define a new Utility API reference. If you're only using +the API for sign-in, you can put the definition in `packages/app/src/apis.ts`. +However, if you need to access your auth API inside plugins you you'll need to +export it from a common package. If you don't already have one we recommended +creating `@internal/apis` and from there export the API reference. + +```ts +// `ProfileInfoApi & BackstageIdentityApi & SessionApi` are required for sign-in +// Include `OAuthApi & OpenIdConnectApi` only if applicable +export const acmeAuthApiRef: ApiRef< + OAuthApi & + OpenIdConnectApi & + ProfileInfoApi & + BackstageIdentityApi & + SessionApi +> = createApiRef({ + id: 'internal.auth.acme', +}); +``` + +Next you'll want to wire up the API inside `packages/app/src/apis.ts`, which +varies depending on which API you're replacing. If you for example are replacing +the `oauth2ApiRef`, the factory might look like this: + +```ts +// oauth2 +createApiFactory({ + api: acmeAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + configApi: configApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi, configApi }) => + OAuth2.create({ + discoveryApi, + oauthRequestApi, + environment: configApi.getOptionalString('auth.environment'), + }), +}); +``` + +Provider specific factory implementations, copy the code you need into the +factory method depending on which apiRef you previously used. + +```ts +// samlAuthApiRef +SamlAuth.create({ + discoveryApi, + environment: configApi.getOptionalString('auth.environment'), +}); + +// oidcAuthApiRef +OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'oidc', + title: 'Your Identity Provider', + icon: () => null, + }, + environment: configApi.getOptionalString('auth.environment'), +}); + +// auth0AuthApiRef +OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider: { + id: 'auth0', + title: 'Auth0', + icon: () => null, + }, + defaultScopes: ['openid', 'email', 'profile'], + environment: configApi.getOptionalString('auth.environment'), +}); +``` + +Finally, for the provider to show up in your settings menu, you also need to +update the settings route in `packages/app/src/App.tsx` to pass the +`acmeAuthApiRef` to the `UserSettingsPage`. This replaces all existing provider +items, so you might want to add back any of the default ones that you are using +from the +[DefaultProviderSettings](https://github.com/backstage/backstage/blob/a3ec122170e0205fd3f9c307b98b1c5e4f55bf5f/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx#L35). + +```tsx + + } + /> + } +/> +``` diff --git a/docs/auth/microsoft/provider.md b/docs/auth/microsoft/provider.md index 1e24235f1a..ef79127223 100644 --- a/docs/auth/microsoft/provider.md +++ b/docs/auth/microsoft/provider.md @@ -48,6 +48,6 @@ The Microsoft provider is a structure with three configuration keys: ## Adding the provider to the Backstage frontend -To add the provider to the frontend, add the `microsoftAuthApi` reference and +To add the provider to the frontend, add the `microsoftAuthApiRef` reference and `SignInPage` component as shown in [Adding the provider to the sign-in page](../index.md#adding-the-provider-to-the-sign-in-page). diff --git a/docs/conf/writing.md b/docs/conf/writing.md index 7945d6c980..a4da1e3df0 100644 --- a/docs/conf/writing.md +++ b/docs/conf/writing.md @@ -67,7 +67,7 @@ production build. ## Configuration Files -It is possible to have multiple configuration files (bundled and/or remote), +It is possible to have multiple configuration files (bundled and/or remote\*), both to support different environments, but also to define configuration that is local to specific packages. The configuration files to load are selected using a `--config ` flag, and it is possible to load any number of @@ -77,6 +77,9 @@ root when running the backend, you would use `--config ../../my-config.yaml`, and for config file on a config server you would use `--config https://some.domain.io/app-config.yaml` +**Note**: In case URLs are passed, it is also needed to set the remote option in +the loadBackendConfig call. + If no `config` flags are specified, the default behavior is to load `app-config.yaml` and, if it exists, `app-config.local.yaml` from the repo root. In the provided project setup, `app-config.local.yaml` is `.gitignore`'d, making diff --git a/docs/features/kubernetes/configuration.md b/docs/features/kubernetes/configuration.md index 07a12f54a0..32ab830322 100644 --- a/docs/features/kubernetes/configuration.md +++ b/docs/features/kubernetes/configuration.md @@ -264,6 +264,12 @@ following objects: - horizontalpodautoscalers - ingresses +The following RBAC permissions are required on the batch API group for the +following objects: + +- jobs +- cronjobs + ## Surfacing your Kubernetes components as part of an entity There are two ways to surface your Kubernetes components as part of an entity. diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index 1331c73b72..8342c74096 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -90,6 +90,18 @@ within your instance. The configuration options are documented in the The underlying functionality is using official ElasticSearch client version 7.x, meaning that ElasticSearch version 7 is the only one confirmed to be supported. +Should you need to create your own bespoke search experiences that require more +than just a query translator (such as faceted search or Relay pagination), you +can access the configuration of the search engine in order to create new elastic +search clients. The version of the client need not be the same as one used +internally by the elastic search engine plugin. For example: + +```typescript +import { Client } from '@elastic/elastic-search'; + +const client = searchEngine.newClient(options => new Client(options)); +``` + ## Example configurations ### AWS diff --git a/docs/features/software-templates/writing-custom-field-extensions.md b/docs/features/software-templates/writing-custom-field-extensions.md index ebb7af4398..fab2e1e2cb 100644 --- a/docs/features/software-templates/writing-custom-field-extensions.md +++ b/docs/features/software-templates/writing-custom-field-extensions.md @@ -26,7 +26,7 @@ You can create your own Field Extension by using the `API` like below: ```tsx -//packages/app/scaffolder/MyCustomExtension/MyCustomExtension.tsx +//packages/app/src/scaffolder/MyCustomExtension/MyCustomExtension.tsx import React from 'react'; import { FieldProps, FieldValidation } from '@rjsf/core'; import FormControl from '@material-ui/core/FormControl'; @@ -68,7 +68,7 @@ export const myCustomValidation = ( ``` ```tsx -// packages/app/scaffolder/MyCustomExtension/extensions.ts +// packages/app/src/scaffolder/MyCustomExtension/extensions.ts /* This is where the magic happens and creates the custom field extension. @@ -94,7 +94,7 @@ export const MyCustomFieldExtension = plugin.provide( ``` ```tsx -// packages/app/scaffolder/MyCustomExtension/index.ts +// packages/app/src/scaffolder/MyCustomExtension/index.ts export { MyCustomFieldExtension } from './extension'; ``` @@ -102,7 +102,7 @@ export { MyCustomFieldExtension } from './extension'; Once all these files are in place, you then need to provide your custom extension to the `scaffolder` plugin. -You do this in `packages/app/App.tsx`. You need to provide the +You do this in `packages/app/src/App.tsx`. You need to provide the `customFieldExtensions` as children to the `ScaffolderPage`. ```tsx @@ -118,7 +118,7 @@ const routes = ( Should look something like this instead: ```tsx -import { MyCustomFieldExtension } from './scafffolder/MyCustomExtension'; +import { MyCustomFieldExtension } from './scaffolder/MyCustomExtension'; const routes = ( ... diff --git a/docs/features/techdocs/cli.md b/docs/features/techdocs/cli.md index 5548adf7f0..30cb8314a0 100644 --- a/docs/features/techdocs/cli.md +++ b/docs/features/techdocs/cli.md @@ -1,4 +1,9 @@ -# TechDocs CLI +--- +id: cli +title: TechDocs CLI +# prettier-ignore +description: TechDocs CLI - a utility command line interface for managing TechDocs sites in Backstage. +--- Utility command line interface for managing TechDocs sites in [Backstage](https://github.com/backstage/backstage). diff --git a/package.json b/package.json index 2d63f92d3a..57cf603090 100644 --- a/package.json +++ b/package.json @@ -55,9 +55,9 @@ }, "version": "1.0.0", "dependencies": { - "@microsoft/api-documenter": "^7.13.68", - "@microsoft/api-extractor": "^7.18.7", - "@microsoft/api-extractor-model": "^7.13.5", + "@microsoft/api-documenter": "^7.13.77", + "@microsoft/api-extractor": "^7.19.2", + "@microsoft/api-extractor-model": "^7.15.1", "@microsoft/tsdoc": "^0.13.2" }, "devDependencies": { @@ -78,11 +78,12 @@ "prettier": "^2.2.1", "shx": "^0.3.2", "ts-node": "^10.4.0", + "typescript": "~4.3.5", "yarn-lock-check": "^1.0.5" }, "prettier": "@spotify/prettier-config", "lint-staged": { - "*.{js,jsx,ts,tsx}": [ + "*.{js,jsx,ts,tsx,mjs,cjs}": [ "eslint --fix", "prettier --write" ], diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index d3c76d52bd..29aa62bdab 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -34,6 +34,8 @@ import { OneLoginAuth, UnhandledErrorForwarder, AtlassianAuth, + createFetchApi, + FetchMiddlewares, } from '@backstage/core-app-api'; import { @@ -42,6 +44,8 @@ import { analyticsApiRef, errorApiRef, discoveryApiRef, + fetchApiRef, + identityApiRef, oauthRequestApiRef, googleAuthApiRef, githubAuthApiRef, @@ -92,6 +96,27 @@ export const apis = [ deps: { errorApi: errorApiRef }, factory: ({ errorApi }) => WebStorage.create({ errorApi }), }), + createApiFactory({ + api: fetchApiRef, + deps: { + configApi: configApiRef, + identityApi: identityApiRef, + discoveryApi: discoveryApiRef, + }, + factory: ({ configApi, identityApi, discoveryApi }) => { + return createFetchApi({ + middleware: [ + FetchMiddlewares.resolvePluginProtocol({ + discoveryApi, + }), + FetchMiddlewares.injectIdentityAuth({ + identityApi, + config: configApi, + }), + ], + }); + }, + }), createApiFactory({ api: oauthRequestApiRef, deps: {}, diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 3f3ac75853..d1055a54bf 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,23 @@ # example-app +## 0.2.57 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-github-actions@0.4.27 + - @backstage/core-app-api@0.2.1 + - @backstage/plugin-kubernetes@0.5.1 + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + - @backstage/plugin-org@0.3.31 + - @backstage/plugin-azure-devops@0.1.7 + - @backstage/cli@0.10.2 + - @backstage/catalog-model@0.9.8 + - @backstage/plugin-techdocs@0.12.10 + - @backstage/plugin-catalog-react@0.6.7 + - @backstage/plugin-apache-airflow@0.1.0 + ## 0.2.56 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 99b90952e3..a9e8e8ddc8 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,39 +1,39 @@ { "name": "example-app", - "version": "0.2.56", + "version": "0.2.57", "private": true, "bundled": true, "dependencies": { "@backstage/app-defaults": "^0.1.2", - "@backstage/catalog-model": "^0.9.7", - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/catalog-model": "^0.9.8", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/integration-react": "^0.1.15", "@backstage/plugin-api-docs": "^0.6.18", - "@backstage/plugin-azure-devops": "^0.1.6", - "@backstage/plugin-apache-airflow": "^0.0.0", + "@backstage/plugin-azure-devops": "^0.1.7", + "@backstage/plugin-apache-airflow": "^0.1.0", "@backstage/plugin-badges": "^0.2.16", "@backstage/plugin-catalog": "^0.7.4", "@backstage/plugin-catalog-graph": "^0.2.3", "@backstage/plugin-catalog-import": "^0.7.5", - "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-catalog-react": "^0.6.7", "@backstage/plugin-circleci": "^0.2.31", "@backstage/plugin-cloudbuild": "^0.2.29", "@backstage/plugin-code-coverage": "^0.1.19", "@backstage/plugin-cost-insights": "^0.11.13", "@backstage/plugin-explore": "^0.3.22", "@backstage/plugin-gcp-projects": "^0.3.10", - "@backstage/plugin-github-actions": "^0.4.26", + "@backstage/plugin-github-actions": "^0.4.27", "@backstage/plugin-graphiql": "^0.2.24", "@backstage/plugin-home": "^0.4.7", "@backstage/plugin-jenkins": "^0.5.14", "@backstage/plugin-kafka": "^0.2.22", - "@backstage/plugin-kubernetes": "^0.5.0", + "@backstage/plugin-kubernetes": "^0.5.1", "@backstage/plugin-lighthouse": "^0.2.31", "@backstage/plugin-newrelic": "^0.3.10", - "@backstage/plugin-org": "^0.3.30", + "@backstage/plugin-org": "^0.3.31", "@backstage/plugin-pagerduty": "0.3.19", "@backstage/plugin-rollbar": "^0.3.20", "@backstage/plugin-scaffolder": "^0.11.14", @@ -41,7 +41,7 @@ "@backstage/plugin-sentry": "^0.3.30", "@backstage/plugin-shortcuts": "^0.1.15", "@backstage/plugin-tech-radar": "^0.4.13", - "@backstage/plugin-techdocs": "^0.12.9", + "@backstage/plugin-techdocs": "^0.12.10", "@backstage/plugin-todo": "^0.1.16", "@backstage/plugin-user-settings": "^0.3.13", "@backstage/search-common": "^0.2.0", diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 9810f49386..a7de64e89a 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -126,6 +126,8 @@ import { } from '@roadiehq/backstage-plugin-travis-ci'; import React, { ReactNode, useMemo, useState } from 'react'; +const customEntityFilterKind = ['Component', 'API', 'System']; + const EntityLayoutWrapper = (props: { children?: ReactNode }) => { const [badgesDialogOpen, setBadgesDialogOpen] = useState(false); @@ -523,7 +525,10 @@ const userPage = ( - + @@ -539,7 +544,10 @@ const groupPage = ( - + diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index ad3fb33365..4bd21df721 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/backend-common +## 0.9.14 + +### Patch Changes + +- fe24bc9a32: Each plugin now saves to a separate sqlite database file when `connection.filename` is provided in the sqlite config. + Any existing sqlite database files will be ignored. + ## 0.9.13 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index d20f568e98..cbdfba3c38 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -21,6 +21,7 @@ import { GitLabIntegration } from '@backstage/integration'; import { isChildPath } from '@backstage/cli-common'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; +import { LoadConfigOptionsRemote } from '@backstage/config-loader'; import { Logger as Logger_2 } from 'winston'; import { MergeResult } from 'isomorphic-git'; import { PushResult } from 'isomorphic-git'; @@ -338,6 +339,7 @@ export function isDatabaseConflictError(e: unknown): boolean; // @public export function loadBackendConfig(options: { logger: Logger_2; + remote?: LoadConfigOptionsRemote; argv: string[]; }): Promise; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 130a3fcaa5..91b8f46255 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.9.13", + "version": "0.9.14", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -81,7 +81,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", "@backstage/test-utils": "^0.1.24", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", @@ -94,7 +94,7 @@ "@types/recursive-readdir": "^2.2.0", "@types/stoppable": "^1.1.0", "@types/supertest": "^2.0.8", - "@types/tar": "^4.0.3", + "@types/tar": "^6.1.1", "@types/unzipper": "^0.10.3", "@types/webpack-env": "^1.15.2", "aws-sdk-mock": "^5.2.1", diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 941ad26e65..399898cfc4 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -23,6 +23,7 @@ import { loadConfig, ConfigSchema, ConfigTarget, + LoadConfigOptionsRemote, } from '@backstage/config-loader'; import { AppConfig, Config, ConfigReader } from '@backstage/config'; import { JsonValue } from '@backstage/types'; @@ -178,6 +179,7 @@ let currentCancelFunc: () => void; export async function loadBackendConfig(options: { logger: Logger; // process.argv or any other overrides + remote?: LoadConfigOptionsRemote; argv: string[]; }): Promise { const args = parseArgs(options.argv); @@ -202,8 +204,8 @@ export async function loadBackendConfig(options: { const config = new ObservableConfigProxy(options.logger); const { appConfigs } = await loadConfig({ configRoot: paths.targetRoot, - configPaths: [], configTargets: configTargets, + remote: options.remote, watch: { onChange(newConfigs) { options.logger.info( diff --git a/packages/backend-common/src/database/DatabaseManager.test.ts b/packages/backend-common/src/database/DatabaseManager.test.ts index bd2e7b9f71..a48b1584aa 100644 --- a/packages/backend-common/src/database/DatabaseManager.test.ts +++ b/packages/backend-common/src/database/DatabaseManager.test.ts @@ -15,6 +15,7 @@ */ import { ConfigReader } from '@backstage/config'; import { omit } from 'lodash'; +import path from 'path'; import { createDatabaseClient, ensureDatabaseExists, @@ -170,28 +171,6 @@ describe('DatabaseManager', () => { ); }); - it('uses top level sqlite database filename if plugin config is not present', async () => { - const testManager = DatabaseManager.fromConfig( - new ConfigReader({ - backend: { - database: { - client: 'sqlite3', - connection: 'some-file-path', - }, - }, - }), - ); - - await testManager.forPlugin('pluginwithoutconfig').getClient(); - const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); - const [_, overrides] = mockCalls[0]; - - expect(overrides).toHaveProperty( - 'connection.filename', - expect.stringContaining('some-file-path'), - ); - }); - it('provides an inmemory sqlite database if top level is also inmemory and plugin config is not present', async () => { const testManager = DatabaseManager.fromConfig( new ConfigReader({ @@ -214,6 +193,110 @@ describe('DatabaseManager', () => { ); }); + it('throws if top level sqlite filename is provided', async () => { + const testManager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: 'some-file-path', + }, + }, + }), + ); + + await expect( + testManager.forPlugin('pluginwithoutconfig').getClient(), + ).rejects.toBeInstanceOf(Error); + }); + + it('creates plugin-specific sqlite files when plugin config is not present', async () => { + const testManager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: { + directory: 'sqlite-files', + }, + }, + }, + }), + ); + + await testManager.forPlugin('pluginwithoutconfig').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_, overrides] = mockCalls[0]; + + expect(overrides).toHaveProperty( + 'connection.filename', + path.join('sqlite-files', 'pluginwithoutconfig.sqlite'), + ); + }); + + it('uses sqlite directory from top level config and filename from plugin config', async () => { + const testManager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: { + directory: 'sqlite-files', + }, + plugin: { + test: { + connection: { + filename: 'other.sqlite', + }, + }, + }, + }, + }, + }), + ); + + await testManager.forPlugin('test').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_, overrides] = mockCalls[0]; + + expect(overrides).toHaveProperty( + 'connection.filename', + path.join('sqlite-files', 'other.sqlite'), + ); + }); + + it('uses sqlite directory and filename from plugin config', async () => { + const testManager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: { + directory: 'sqlite-files', + }, + plugin: { + test: { + connection: { + directory: 'custom-sqlite-files', + filename: 'other.sqlite', + }, + }, + }, + }, + }, + }), + ); + + await testManager.forPlugin('test').getClient(); + const mockCalls = mocked(createDatabaseClient).mock.calls.splice(-1); + const [_, overrides] = mockCalls[0]; + + expect(overrides).toHaveProperty( + 'connection.filename', + path.join('custom-sqlite-files', 'other.sqlite'), + ); + }); + it('connects to a plugin database using a specific database name', async () => { // testdbname.connection.database is set in config await manager.forPlugin('testdbname').getClient(); @@ -282,7 +365,10 @@ describe('DatabaseManager', () => { expect(baseConfig.get().client).toEqual('sqlite3'); // sqlite3 uses 'filename' instead of 'database' - expect(overrides).toHaveProperty('connection.filename'); + expect(overrides).toHaveProperty( + 'connection.filename', + 'plugin_with_different_client', + ); }); it('provides database client specific base from plugin connection string when client set under plugin', async () => { diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 8e23a219ec..5959c60c83 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -28,6 +28,7 @@ import { normalizeConnection, } from './connection'; import { PluginDatabaseManager } from './types'; +import path from 'path'; /** * Provides a config lookup path for a plugin's config block. @@ -114,10 +115,18 @@ export class DatabaseManager { const connection = this.getConnectionConfig(pluginId); if (this.getClientType(pluginId).client === 'sqlite3') { - // sqlite database name should fallback to ':memory:' as a special case - return ( - (connection as Knex.Sqlite3ConnectionConfig)?.filename ?? ':memory:' - ); + const sqliteFilename: string | undefined = ( + connection as Knex.Sqlite3ConnectionConfig + ).filename; + + if (sqliteFilename === ':memory:') { + return sqliteFilename; + } + + const sqliteDirectory = + (connection as { directory?: string }).directory ?? '.'; + + return path.join(sqliteDirectory, sqliteFilename ?? `${pluginId}.sqlite`); } const databaseName = (connection as Knex.ConnectionConfig)?.database; @@ -205,6 +214,17 @@ export class DatabaseManager { this.config.get('connection'), this.config.getString('client'), ); + + if ( + client === 'sqlite3' && + 'filename' in baseConnection && + baseConnection.filename !== ':memory:' + ) { + throw new Error( + '`connection.filename` is not supported for the base sqlite connection. Prefer `connection.directory` or provide a filename for the plugin connection instead.', + ); + } + // Databases cannot be shared unless the `pluginDivisionMode` is set to `schema`. The // `database` property from the base connection is omitted unless `pluginDivisionMode` // is set to `schema`. SQLite3's `filename` property is an exception as this is used as a diff --git a/packages/backend-common/src/database/connectors/sqlite3.test.ts b/packages/backend-common/src/database/connectors/sqlite3.test.ts index b9da19d247..cfc9f61527 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.test.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.test.ts @@ -73,28 +73,6 @@ describe('sqlite3', () => { }); }); - it('builds a persistent connection per database', () => { - expect( - buildSqliteDatabaseConfig( - createConfig({ - filename: path.join('path', 'to', 'foo'), - }), - { - connection: { - database: 'my-database', - }, - }, - ), - ).toEqual({ - client: 'sqlite3', - connection: { - filename: path.join('path', 'to', 'foo', 'my-database.sqlite'), - database: 'my-database', - }, - useNullAsDefault: true, - }); - }); - it('replaces the connection with an override', () => { expect( buildSqliteDatabaseConfig(createConfig(':memory:'), { diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index 3dfecd7e6b..41f80ed293 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -86,19 +86,6 @@ export function buildSqliteDatabaseConfig( overrides, ); - // If we don't create an in-memory database, interpret the connection string - // as a directory that contains multiple sqlite files based on the database - // name. - const database = (config.connection as Knex.ConnectionConfig).database; - const sqliteConnection = config.connection as Knex.Sqlite3ConnectionConfig; - - if (database && sqliteConnection.filename !== ':memory:') { - sqliteConnection.filename = path.join( - sqliteConnection.filename, - `${database}.sqlite`, - ); - } - return config; } diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index fa8632e2ef..9c7b00b61f 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,19 @@ # example-backend +## 0.2.57 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-backend-module-elasticsearch@0.0.7 + - @backstage/plugin-catalog-backend@0.19.2 + - @backstage/plugin-scaffolder-backend@0.15.17 + - @backstage/backend-common@0.9.14 + - @backstage/plugin-azure-devops-backend@0.2.5 + - @backstage/plugin-auth-backend@0.5.1 + - @backstage/catalog-model@0.9.8 + - example-app@0.2.57 + ## 0.2.56 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index ae61bc7a45..80ae3b0998 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.56", + "version": "0.2.57", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -24,16 +24,16 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.9.13", + "@backstage/backend-common": "^0.9.14", "@backstage/catalog-client": "^0.5.2", - "@backstage/catalog-model": "^0.9.7", + "@backstage/catalog-model": "^0.9.8", "@backstage/config": "^0.1.10", "@backstage/integration": "^0.6.10", "@backstage/plugin-app-backend": "^0.3.19", - "@backstage/plugin-auth-backend": "^0.5.0", - "@backstage/plugin-azure-devops-backend": "^0.2.4", + "@backstage/plugin-auth-backend": "^0.5.1", + "@backstage/plugin-azure-devops-backend": "^0.2.5", "@backstage/plugin-badges-backend": "^0.1.13", - "@backstage/plugin-catalog-backend": "^0.19.1", + "@backstage/plugin-catalog-backend": "^0.19.2", "@backstage/plugin-code-coverage-backend": "^0.1.16", "@backstage/plugin-graphql-backend": "^0.1.9", "@backstage/plugin-jenkins-backend": "^0.1.9", @@ -41,11 +41,11 @@ "@backstage/plugin-kafka-backend": "^0.2.12", "@backstage/plugin-proxy-backend": "^0.2.14", "@backstage/plugin-rollbar-backend": "^0.1.16", - "@backstage/plugin-scaffolder-backend": "^0.15.16", + "@backstage/plugin-scaffolder-backend": "^0.15.17", "@backstage/plugin-scaffolder-backend-module-rails": "^0.2.0", "@backstage/plugin-search-backend": "^0.2.8", "@backstage/plugin-search-backend-node": "^0.4.2", - "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.6", + "@backstage/plugin-search-backend-module-elasticsearch": "^0.0.7", "@backstage/plugin-search-backend-module-pg": "^0.2.2", "@backstage/plugin-techdocs-backend": "^0.12.0", "@backstage/plugin-tech-insights-backend": "^0.1.3", @@ -56,7 +56,7 @@ "@octokit/rest": "^18.5.3", "azure-devops-node-api": "^11.0.1", "dockerode": "^3.3.1", - "example-app": "^0.2.56", + "example-app": "^0.2.57", "express": "^4.17.1", "express-promise-router": "^4.1.0", "express-prom-bundle": "^6.3.6", @@ -68,7 +68,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", "@types/dockerode": "^3.3.0", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 4426ed7767..7a79c278ff 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -89,6 +89,7 @@ async function main() { argv: process.argv, logger, }); + const createEnv = makeCreateEnv(config); const healthcheckEnv = useHotMemoize(module, () => createEnv('healthcheck')); diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index e16ca740f6..f3921993d3 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -71,7 +71,7 @@ export interface CatalogApi { // @public export class CatalogClient implements CatalogApi { - constructor(options: { discoveryApi: DiscoveryApi }); + constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }); addLocation( { type, target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions, @@ -121,6 +121,9 @@ export type CatalogEntitiesRequest = { | Record | undefined; fields?: string[] | undefined; + offset?: number; + limit?: number; + after?: string; }; // @public @@ -155,4 +158,9 @@ export type DiscoveryApi = { // @public export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = 'backstage.io/catalog-processing'; + +// @public +export type FetchApi = { + fetch: typeof fetch; +}; ``` diff --git a/packages/catalog-client/src/CatalogClient.test.ts b/packages/catalog-client/src/CatalogClient.test.ts index d2db98bb25..1331f8e874 100644 --- a/packages/catalog-client/src/CatalogClient.test.ts +++ b/packages/catalog-client/src/CatalogClient.test.ts @@ -175,6 +175,24 @@ describe('CatalogClient', () => { { apiVersion: '2' }, ]); }); + + it('builds paging parameters properly', async () => { + expect.assertions(2); + + server.use( + rest.get(`${mockBaseUrl}/entities`, (req, res, ctx) => { + expect(req.url.search).toBe('?offset=1&limit=2&after=%3D'); + return res(ctx.json([])); + }), + ); + + const response = await client.getEntities( + { offset: 1, limit: 2, after: '=' }, + { token }, + ); + + expect(response.items).toEqual([]); + }); }); describe('getLocationById', () => { diff --git a/packages/catalog-client/src/CatalogClient.ts b/packages/catalog-client/src/CatalogClient.ts index 29d01fbe5c..a446d249c0 100644 --- a/packages/catalog-client/src/CatalogClient.ts +++ b/packages/catalog-client/src/CatalogClient.ts @@ -25,7 +25,7 @@ import { stringifyLocationReference, } from '@backstage/catalog-model'; import { ResponseError } from '@backstage/errors'; -import fetch from 'cross-fetch'; +import crossFetch from 'cross-fetch'; import { CATALOG_FILTER_EXISTS, AddLocationRequest, @@ -38,6 +38,7 @@ import { CatalogEntityAncestorsResponse, } from './types/api'; import { DiscoveryApi } from './types/discovery'; +import { FetchApi } from './types/fetch'; /** * A frontend and backend compatible client for communicating with the Backstage Catalog. @@ -46,9 +47,11 @@ import { DiscoveryApi } from './types/discovery'; * */ export class CatalogClient implements CatalogApi { private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; - constructor(options: { discoveryApi: DiscoveryApi }) { + constructor(options: { discoveryApi: DiscoveryApi; fetchApi?: FetchApi }) { this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi || { fetch: crossFetch }; } /** @@ -110,7 +113,7 @@ export class CatalogClient implements CatalogApi { request?: CatalogEntitiesRequest, options?: CatalogRequestOptions, ): Promise> { - const { filter = [], fields = [] } = request ?? {}; + const { filter = [], fields = [], offset, limit, after } = request ?? {}; const filterItems = [filter].flat(); const params: string[] = []; @@ -141,6 +144,16 @@ export class CatalogClient implements CatalogApi { params.push(`fields=${fields.map(encodeURIComponent).join(',')}`); } + if (offset !== undefined) { + params.push(`offset=${offset}`); + } + if (limit !== undefined) { + params.push(`limit=${limit}`); + } + if (after !== undefined) { + params.push(`after=${encodeURIComponent(after)}`); + } + const query = params.length ? `?${params.join('&')}` : ''; const entities: Entity[] = await this.requestRequired( 'GET', @@ -206,7 +219,7 @@ export class CatalogClient implements CatalogApi { * @public */ async refreshEntity(entityRef: string, options?: CatalogRequestOptions) { - const response = await fetch( + const response = await this.fetchApi.fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/refresh`, { headers: { @@ -237,7 +250,7 @@ export class CatalogClient implements CatalogApi { { type = 'url', target, dryRun, presence }: AddLocationRequest, options?: CatalogRequestOptions, ): Promise { - const response = await fetch( + const response = await this.fetchApi.fetch( `${await this.discoveryApi.getBaseUrl('catalog')}/locations${ dryRun ? '?dryRun=true' : '' }`, @@ -376,7 +389,7 @@ export class CatalogClient implements CatalogApi { const headers: Record = options?.token ? { Authorization: `Bearer ${options.token}` } : {}; - const response = await fetch(url, { method, headers }); + const response = await this.fetchApi.fetch(url, { method, headers }); if (!response.ok) { throw await ResponseError.fromResponse(response); @@ -392,7 +405,7 @@ export class CatalogClient implements CatalogApi { const headers: Record = options?.token ? { Authorization: `Bearer ${options.token}` } : {}; - const response = await fetch(url, { method, headers }); + const response = await this.fetchApi.fetch(url, { method, headers }); if (!response.ok) { throw await ResponseError.fromResponse(response); @@ -410,7 +423,7 @@ export class CatalogClient implements CatalogApi { const headers: Record = options?.token ? { Authorization: `Bearer ${options.token}` } : {}; - const response = await fetch(url, { method, headers }); + const response = await this.fetchApi.fetch(url, { method, headers }); if (!response.ok) { if (response.status === 404) { diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts index 6344caff7c..3a09e14d71 100644 --- a/packages/catalog-client/src/types/api.ts +++ b/packages/catalog-client/src/types/api.ts @@ -29,11 +29,81 @@ export const CATALOG_FILTER_EXISTS = Symbol('CATALOG_FILTER_EXISTS'); * @public */ export type CatalogEntitiesRequest = { + /** + * If given, return only entities that match the given patterns. + * + * @remarks + * + * If multiple filter sets are given as an array, then there is effectively an + * OR between each filter set. + * + * Within one filter set, there is effectively an AND between the various + * keys. + * + * Within one key, if there are more than one value, then there is effectively + * an OR between them. + * + * Example: For an input of + * + * ``` + * [ + * { kind: ['API', 'Component'] }, + * { 'metadata.name': 'a', 'metadata.namespace': 'b' } + * ] + * ``` + * + * This effectively means + * + * ``` + * (kind = EITHER 'API' OR 'Component') + * OR + * (metadata.name = 'a' AND metadata.namespace = 'b' ) + * ``` + * + * Each key is a dot separated path in each object. + * + * As a value you can also pass in the symbol `CATALOG_FILTER_EXISTS` + * (exported from this package), which means that you assert on the existence + * of that key, no matter what its value is. + */ filter?: | Record[] | Record | undefined; + /** + * If given, return only the parts of each entity that match those dot + * separated paths in each object. + * + * @remarks + * + * Example: For an input of `['kind', 'metadata.annotations']`, then response + * objects will be shaped like + * + * ``` + * { + * "kind": "Component", + * "metadata": { + * "annotations": { + * "foo": "bar" + * } + * } + * } + * ``` + */ fields?: string[] | undefined; + /** + * If given, skips over the first N items in the result set. + */ + offset?: number; + /** + * If given, returns at most N items from the result set. + */ + limit?: number; + /** + * If given, skips over all items before that cursor as returned by a previous + * request. + */ + after?: string; }; /** diff --git a/packages/catalog-client/src/types/discovery.ts b/packages/catalog-client/src/types/discovery.ts index 19ee5ed19c..5fa14372e3 100644 --- a/packages/catalog-client/src/types/discovery.ts +++ b/packages/catalog-client/src/types/discovery.ts @@ -15,7 +15,7 @@ */ /** - * This is a copy of the core DiscoveryApi, to avoid importing core. + * This is a copy of the DiscoveryApi, to avoid importing core-plugin-api. * * @public */ diff --git a/packages/catalog-client/src/types/fetch.ts b/packages/catalog-client/src/types/fetch.ts new file mode 100644 index 0000000000..59de6a68f1 --- /dev/null +++ b/packages/catalog-client/src/types/fetch.ts @@ -0,0 +1,24 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * This is a copy of FetchApi, to avoid importing core-plugin-api. + * + * @public + */ +export type FetchApi = { + fetch: typeof fetch; +}; diff --git a/packages/catalog-client/src/types/index.ts b/packages/catalog-client/src/types/index.ts index 3552a72e3c..f3c6fab4e9 100644 --- a/packages/catalog-client/src/types/index.ts +++ b/packages/catalog-client/src/types/index.ts @@ -25,5 +25,6 @@ export type { CatalogEntityAncestorsResponse, } from './api'; export type { DiscoveryApi } from './discovery'; +export type { FetchApi } from './fetch'; export { CATALOG_FILTER_EXISTS } from './api'; export { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from './status'; diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index efd6513cdd..8c0a65c20a 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/catalog-model +## 0.9.8 + +### Patch Changes + +- ad7338bb48: Added an optional `presence` field to Location spec, which describes whether the target of a location is required to exist or not. It defaults to `'required'`, which is the current behaviour of the catalog. + ## 0.9.7 ### Patch Changes diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 52f36493b5..9c0c33fa0e 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -325,6 +325,7 @@ interface LocationEntityV1alpha1 extends Entity { type?: string; target?: string; targets?: string[]; + presence?: 'required' | 'optional'; }; } export { LocationEntityV1alpha1 as LocationEntity }; diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 60331d9dc8..6fdc89bdfb 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "0.9.7", + "version": "0.9.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,7 +42,7 @@ "yup": "^0.32.9" }, "devDependencies": { - "@backstage/cli": "^0.10.0", + "@backstage/cli": "^0.10.2", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", "yaml": "^1.9.2" diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts index 7efec38395..6e78c1b96e 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.test.ts @@ -102,4 +102,23 @@ describe('LocationV1alpha1Validator', () => { (entity as any).spec.targets = 7; await expect(validator.check(entity)).rejects.toThrow(/targets/); }); + + it('accepts good presence', async () => { + (entity as any).spec.presence = 'required'; + await expect(validator.check(entity)).resolves.toBe(true); + (entity as any).spec.presence = 'optional'; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects empty presence', async () => { + (entity as any).spec.presence = ''; + await expect(validator.check(entity)).rejects.toThrow(/presence/); + }); + + it('rejects wrong presence', async () => { + (entity as any).spec.presence = 7; + await expect(validator.check(entity)).rejects.toThrow(/presence/); + (entity as any).spec.presence = 'nope'; + await expect(validator.check(entity)).rejects.toThrow(/presence/); + }); }); diff --git a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts index 1872e43526..93a218aa2d 100644 --- a/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts +++ b/packages/catalog-model/src/kinds/LocationEntityV1alpha1.ts @@ -30,6 +30,7 @@ export interface LocationEntityV1alpha1 extends Entity { type?: string; target?: string; targets?: string[]; + presence?: 'required' | 'optional'; }; } diff --git a/packages/catalog-model/src/schema/kinds/Location.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/Location.v1alpha1.schema.json index d633d30229..b70df9e70e 100644 --- a/packages/catalog-model/src/schema/kinds/Location.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/Location.v1alpha1.schema.json @@ -59,6 +59,13 @@ ], "minLength": 1 } + }, + "presence": { + "type": "string", + "description": "Whether the presence of the location target is required and it should be considered an error if it can not be found", + "default": "required", + "examples": ["required"], + "enum": ["required", "optional"] } } } diff --git a/packages/cli/.snyk b/packages/cli/.snyk index 612b4b0525..3ad43bd3de 100644 --- a/packages/cli/.snyk +++ b/packages/cli/.snyk @@ -67,4 +67,16 @@ ignore: reason: Prototype pollution is not an effective attack against a CLI as it already executes arbitrary code expires: 2022-03-06T17:18:55.019Z created: 2021-09-06T17:18:55.027Z + + 'snyk:lic:npm:rollup-plugin-dts:LGPL-3.0': + - '*': + reason: Backstage itself does not redistribute this dependency in minified form + expires: 2031-09-06T17:18:55.027Z + created: 2021-09-06T17:18:55.027Z + + 'snyk:lic:npm:axe-core:MPL-2.0': + - '*': + reason: Backstage itself does not redistribute this dependency in minified form + expires: 2031-09-06T17:18:55.027Z + created: 2021-09-06T17:18:55.027Z patch: {} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 0c30e8b251..5d51ea890b 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/cli +## 0.10.2 + +### Patch Changes + +- 25dfc2d483: Add support for `.cjs` and `.mjs` extensions in local and dependency modules. + ## 0.10.1 ### Patch Changes diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 1d33372c60..978c598584 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -96,20 +96,22 @@ async function getProjectConfig(targetPath, displayName) { rootDir: path.resolve(targetPath, 'src'), coverageDirectory: path.resolve(targetPath, 'coverage'), coverageProvider: 'v8', - collectCoverageFrom: ['**/*.{js,jsx,ts,tsx}', '!**/*.d.ts'], + collectCoverageFrom: ['**/*.{js,jsx,ts,tsx,mjs,cjs}', '!**/*.d.ts'], moduleNameMapper: { '\\.(css|less|scss|sss|styl)$': require.resolve('jest-css-modules'), }, transform: { - '\\.(js|jsx|ts|tsx)$': require.resolve('./jestSucraseTransform.js'), + '\\.(js|jsx|ts|tsx|mjs|cjs)$': require.resolve( + './jestSucraseTransform.js', + ), '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg|eot|woff|woff2|ttf)$': require.resolve('./jestFileTransform.js'), '\\.(yaml)$': require.resolve('jest-transform-yaml'), }, // A bit more opinionated - testMatch: ['**/?(*.)test.{js,jsx,mjs,ts,tsx}'], + testMatch: ['**/?(*.)test.{js,jsx,ts,tsx,mjs,cjs}'], transformIgnorePatterns: [`/node_modules/(?:${transformIgnorePattern})/`], }; diff --git a/packages/cli/package.json b/packages/cli/package.json index 984c1c70d1..4b59d46827 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.10.1", + "version": "0.10.2", "private": false, "publishConfig": { "access": "public" @@ -116,11 +116,11 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.9.13", + "@backstage/backend-common": "^0.9.14", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", - "@backstage/core-app-api": "^0.2.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@backstage/theme": "^0.2.14", @@ -128,13 +128,13 @@ "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", "@types/http-proxy": "^1.17.4", - "@types/inquirer": "^7.3.1", + "@types/inquirer": "^8.1.3", "@types/mock-fs": "^4.13.0", "@types/node": "^14.14.32", "@types/recursive-readdir": "^2.2.0", "@types/rollup-plugin-peer-deps-external": "^2.2.0", "@types/rollup-plugin-postcss": "^2.0.0", - "@types/tar": "^4.0.3", + "@types/tar": "^6.1.1", "@types/terser-webpack-plugin": "^5.0.4", "@types/webpack": "^5.28.0", "@types/webpack-dev-server": "^3.11.5", diff --git a/packages/cli/src/commands/lint.ts b/packages/cli/src/commands/lint.ts index 41b454ffff..5c3e0a88e3 100644 --- a/packages/cli/src/commands/lint.ts +++ b/packages/cli/src/commands/lint.ts @@ -20,7 +20,7 @@ import { paths } from '../lib/paths'; export default async (cmd: Command, cmdArgs: string[]) => { const args = [ - '--ext=js,jsx,ts,tsx', + '--ext=js,jsx,ts,tsx,mjs,cjs', '--max-warnings=0', `--format=${cmd.format}`, ...(cmdArgs ?? [paths.targetDir]), diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 863728b8f8..d1d26f383d 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -61,7 +61,7 @@ export const transforms = (options: TransformOptions): Transforms => { }, }, { - test: /\.(jsx?|mjs)$/, + test: /\.(jsx?|mjs|cjs)$/, exclude: /node_modules/, loader: require.resolve('@sucrase/webpack-loader'), options: { @@ -71,7 +71,7 @@ export const transforms = (options: TransformOptions): Transforms => { }, }, { - test: /\.m?js/, + test: /\.(js|mjs|cjs)/, resolve: { fullySpecified: false, }, diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts index d343a554f8..f85db276b2 100644 --- a/packages/cli/src/lib/config.ts +++ b/packages/cli/src/lib/config.ts @@ -59,7 +59,6 @@ export async function loadCliConfig(options: Options) { ? async name => process.env[name] || 'x' : undefined, configRoot: paths.targetRoot, - configPaths: [], configTargets: configTargets, }); diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index d7e402270a..803579a963 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/codemods +## 0.1.26 + +### Patch Changes + +- Updated dependencies + - @backstage/core-app-api@0.2.1 + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + ## 0.1.25 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index 32bf6be518..8261e30547 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.25", + "version": "0.1.26", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index e0fab50e5b..76c6468556 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -45,7 +45,6 @@ export function loadConfig( // @public export type LoadConfigOptions = { configRoot: string; - configPaths: string[]; configTargets: ConfigTarget[]; env?: string; experimentalEnvFunc?: (name: string) => Promise; @@ -103,8 +102,4 @@ export type TransformFunc = ( visibility: ConfigVisibility; }, ) => T | undefined; - -// Warnings were encountered during analysis: -// -// src/loader.d.ts:33:5 - (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/config-loader" does not have an export "configTargets" ``` diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index b5c986eeb9..97c3037244 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -118,7 +118,6 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: [], configTargets: [], env: 'production', }), @@ -146,7 +145,6 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: [], configTargets: [{ url: configUrl }], env: 'production', remote: { @@ -173,8 +171,10 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: ['/root/app-config2.yaml'], - configTargets: [{ path: '/root/app-config.yaml' }], + configTargets: [ + { path: '/root/app-config.yaml' }, + { path: '/root/app-config2.yaml' }, + ], env: 'production', }), ).resolves.toEqual({ @@ -207,7 +207,6 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: ['/root/app-config.yaml'], configTargets: [{ path: '/root/app-config.yaml' }], env: 'production', }), @@ -231,7 +230,6 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: [], configTargets: [ { path: '/root/app-config.yaml' }, { path: '/root/app-config.development.yaml' }, @@ -274,7 +272,6 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: [], configTargets: [{ path: '/root/app-config.substitute.yaml' }], env: 'development', }), @@ -302,7 +299,6 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: [], configTargets: [], watch: { onChange: onChange.resolve, @@ -353,7 +349,6 @@ describe('loadConfig', () => { await expect( loadConfig({ configRoot: '/root', - configPaths: [], configTargets: [{ url: configUrl }], watch: { onChange: onChange.resolve, @@ -401,7 +396,6 @@ describe('loadConfig', () => { await loadConfig({ configRoot: '/root', - configPaths: [], configTargets: [], watch: { onChange: () => { diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 6a92ed519d..906eb3917d 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -45,7 +45,7 @@ export type LoadConfigOptionsWatch = { export type LoadConfigOptionsRemote = { /** - * An optional remote config reloading period, in seconds + * A remote config reloading period, in seconds */ reloadIntervalSeconds: number; }; @@ -59,11 +59,6 @@ export type LoadConfigOptions = { // The root directory of the config loading context. Used to find default configs. configRoot: string; - /** Absolute paths to load config files from. Configs from earlier paths have lower priority. - * @deprecated Use {@link configTargets} instead. - */ - configPaths: string[]; - // Paths to load config files from. Configs from earlier paths have lower priority. configTargets: ConfigTarget[]; @@ -114,20 +109,21 @@ export async function loadConfig( .filter((e): e is { path: string } => e.hasOwnProperty('path')) .map(configTarget => configTarget.path); - // Append deprecated configPaths to the absolute config paths received via configTargets. - options.configPaths.forEach(cp => { - if (!configPaths.includes(cp)) { - configPaths.push(cp); - } - }); - const configUrls: string[] = options.configTargets .slice() .filter((e): e is { url: string } => e.hasOwnProperty('url')) .map(configTarget => configTarget.url); - if (remote === undefined && configUrls.length > 0) { - throw new Error(`Remote config detected but this feature is turned off`); + if (remote === undefined) { + if (configUrls.length > 0) { + throw new Error( + `Please make sure you are passing the remote option when loading remote configurations. See https://backstage.io/docs/conf/writing#configuration-files for detailed info.`, + ); + } + } else if (remote.reloadIntervalSeconds <= 0) { + throw new Error( + `Remote config must be contain a non zero reloadIntervalSeconds: value`, + ); } // If no paths are provided, we default to reading diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md index ff15f45898..66e0c7d6ed 100644 --- a/packages/core-app-api/CHANGELOG.md +++ b/packages/core-app-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-app-api +## 0.2.1 + +### Patch Changes + +- c11ce4f552: Deprecated `Auth0Auth`, pointing to using `OAuth2` directly instead. +- 9d6503e86c: Switched out usage of deprecated `OAuthRequestApi` types from `@backstage/core-plugin-api`. +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + ## 0.2.0 ### Minor Changes diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 51fd6bd06f..78bfd83251 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -17,15 +17,14 @@ import { AppTheme } from '@backstage/core-plugin-api'; import { AppThemeApi } from '@backstage/core-plugin-api'; import { atlassianAuthApiRef } from '@backstage/core-plugin-api'; import { auth0AuthApiRef } from '@backstage/core-plugin-api'; -import { AuthProvider } from '@backstage/core-plugin-api'; -import { AuthRequester } from '@backstage/core-plugin-api'; -import { AuthRequesterOptions } from '@backstage/core-plugin-api'; +import { AuthProviderInfo } from '@backstage/core-plugin-api'; import { AuthRequestOptions } from '@backstage/core-plugin-api'; import { BackstageIdentity } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; +import { Config } from '@backstage/config'; import { ConfigReader } from '@backstage/config'; import { createApp as createApp_2 } from '@backstage/app-defaults'; import { DiscoveryApi } from '@backstage/core-plugin-api'; @@ -36,6 +35,7 @@ import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { FeatureFlag } from '@backstage/core-plugin-api'; import { FeatureFlagsApi } from '@backstage/core-plugin-api'; import { FeatureFlagsSaveOptions } from '@backstage/core-plugin-api'; +import { FetchApi } from '@backstage/core-plugin-api'; import { gitlabAuthApiRef } from '@backstage/core-plugin-api'; import { googleAuthApiRef } from '@backstage/core-plugin-api'; import { IconComponent } from '@backstage/core-plugin-api'; @@ -43,11 +43,13 @@ import { IdentityApi } from '@backstage/core-plugin-api'; import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; import { OAuthApi } from '@backstage/core-plugin-api'; import { OAuthRequestApi } from '@backstage/core-plugin-api'; +import { OAuthRequester } from '@backstage/core-plugin-api'; +import { OAuthRequesterOptions } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { oktaAuthApiRef } from '@backstage/core-plugin-api'; import { oneloginAuthApiRef } from '@backstage/core-plugin-api'; import { OpenIdConnectApi } from '@backstage/core-plugin-api'; -import { PendingAuthRequest } from '@backstage/core-plugin-api'; +import { PendingOAuthRequest } from '@backstage/core-plugin-api'; import { PluginOutput } from '@backstage/core-plugin-api'; import { ProfileInfo } from '@backstage/core-plugin-api'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; @@ -254,7 +256,7 @@ export class AtlassianAuth { static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T; } -// @public +// @public @deprecated export class Auth0Auth { // (undocumented) static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T; @@ -264,9 +266,7 @@ export class Auth0Auth { export type AuthApiCreateOptions = { discoveryApi: DiscoveryApi; environment?: string; - provider?: AuthProvider & { - id: string; - }; + provider?: AuthProviderInfo; }; // @public @@ -320,6 +320,12 @@ export function createApp( options?: Parameters[0], ): BackstageApp & AppContext; +// @public +export function createFetchApi(options: { + baseImplementation?: typeof fetch | undefined; + middleware?: FetchMiddleware | FetchMiddleware[] | undefined; +}): FetchApi; + // @public export function createSpecializedApp(options: AppOptions): BackstageApp; @@ -371,6 +377,27 @@ export type FeatureFlaggedProps = { } ); +// @public +export interface FetchMiddleware { + apply(next: typeof fetch): typeof fetch; +} + +// @public +export class FetchMiddlewares { + static injectIdentityAuth(options: { + identityApi: IdentityApi; + config?: Config; + urlPrefixAllowlist?: string[]; + header?: { + name: string; + value: (backstageToken: string) => string; + }; + }): FetchMiddleware; + static resolvePluginProtocol(options: { + discoveryApi: DiscoveryApi; + }): FetchMiddleware; +} + // @public export const FlatRoutes: (props: FlatRoutesProps) => JSX.Element | null; @@ -515,9 +542,9 @@ export type OAuthApiCreateOptions = AuthApiCreateOptions & { // @public export class OAuthRequestManager implements OAuthRequestApi { // (undocumented) - authRequest$(): Observable; + authRequest$(): Observable; // (undocumented) - createAuthRequester(options: AuthRequesterOptions): AuthRequester; + createAuthRequester(options: OAuthRequesterOptions): OAuthRequester; } // @public @@ -539,9 +566,7 @@ export type OneLoginAuthCreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; - provider?: AuthProvider & { - id: string; - }; + provider?: AuthProviderInfo; }; // @public diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json index 23079d7ba0..166f39a484 100644 --- a/packages/core-app-api/package.json +++ b/packages/core-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-app-api", "description": "Core app API used by Backstage apps", - "version": "0.2.0", + "version": "0.2.1", "private": false, "publishConfig": { "access": "public", @@ -30,9 +30,9 @@ }, "dependencies": { "@backstage/app-defaults": "^0.1.2", - "@backstage/core-components": "^0.8.0", + "@backstage/core-components": "^0.8.1", "@backstage/config": "^0.1.11", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", "@backstage/version-bridge": "^0.1.1", @@ -49,7 +49,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/FetchMiddlewares.ts b/packages/core-app-api/src/apis/implementations/FetchApi/FetchMiddlewares.ts new file mode 100644 index 0000000000..1cf09ff20d --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/FetchMiddlewares.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Config } from '@backstage/config'; +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware'; +import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware'; +import { FetchMiddleware } from './types'; + +/** + * A collection of common middlewares for the FetchApi. + * + * @public + */ +export class FetchMiddlewares { + /** + * Handles translation from `plugin://` URLs to concrete http(s) URLs based on + * the discovery API. + * + * @remarks + * + * If the request is for `plugin://catalog/entities?filter=x=y`, the discovery + * API will be queried for `'catalog'`. If it returned + * `https://backstage.example.net/api/catalog`, the resulting query would be + * `https://backstage.example.net/api/catalog/entities?filter=x=y`. + * + * If the incoming URL protocol was not `plugin`, the request is just passed + * through verbatim to the underlying implementation. + */ + static resolvePluginProtocol(options: { + discoveryApi: DiscoveryApi; + }): FetchMiddleware { + return new PluginProtocolResolverFetchMiddleware(options.discoveryApi); + } + + /** + * Injects a Backstage token header when the user is signed in. + * + * @remarks + * + * Per default, an `Authorization: Bearer ` is generated. This can be + * customized using the `header` option. + * + * The header injection only happens on allowlisted URLs. Per default, if the + * `config` option is passed in, the `backend.baseUrl` is allowlisted, unless + * the `urlPrefixAllowlist` option is passed in, in which case it takes + * precedence. If you pass in neither config nor an allowlist, the middleware + * will have no effect. + */ + static injectIdentityAuth(options: { + identityApi: IdentityApi; + config?: Config; + urlPrefixAllowlist?: string[]; + header?: { + name: string; + value: (backstageToken: string) => string; + }; + }): FetchMiddleware { + return IdentityAuthInjectorFetchMiddleware.create(options); + } + + private constructor() {} +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts new file mode 100644 index 0000000000..b69a4b9f7a --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.test.ts @@ -0,0 +1,153 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ConfigReader } from '@backstage/config'; +import { IdentityApi } from '@backstage/core-plugin-api'; +import { IdentityAuthInjectorFetchMiddleware } from './IdentityAuthInjectorFetchMiddleware'; + +describe('IdentityAuthInjectorFetchMiddleware', () => { + it('creates using defaults', async () => { + const middleware = IdentityAuthInjectorFetchMiddleware.create({ + identityApi: undefined as any, + }); + expect(middleware.urlPrefixAllowlist).toEqual([]); + expect(middleware.headerName).toEqual('authorization'); + expect(middleware.headerValue('t')).toEqual('Bearer t'); + }); + + it('creates using config', async () => { + const middleware = IdentityAuthInjectorFetchMiddleware.create({ + identityApi: undefined as any, + config: new ConfigReader({ + backend: { baseUrl: 'https://example.com/api' }, + }), + header: { name: 'auth', value: t => `${t}!` }, + }); + expect(middleware.urlPrefixAllowlist).toEqual(['https://example.com/api']); + expect(middleware.headerName).toEqual('auth'); + expect(middleware.headerValue('t')).toEqual('t!'); + }); + + it('creates using explicit allowlist', async () => { + const middleware = IdentityAuthInjectorFetchMiddleware.create({ + identityApi: undefined as any, + config: new ConfigReader({ + backend: { baseUrl: 'https://example.com/api' }, + }), + urlPrefixAllowlist: ['https://a.com', 'http://b.com:8080/'], + }); + expect(middleware.urlPrefixAllowlist).toEqual([ + 'https://a.com', + 'http://b.com:8080', + ]); + }); + + it('injects the header only when a token is available', async () => { + const tokenFunction = jest.fn(); + const identityApi = { + getCredentials: tokenFunction, + } as unknown as IdentityApi; + + const middleware = new IdentityAuthInjectorFetchMiddleware( + identityApi, + ['https://example.com'], + 'Authorization', + token => `Bearer ${token}`, + ); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + // No token available + tokenFunction.mockResolvedValueOnce({ token: undefined }); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([]); + + // Supply a token, header gets added + tokenFunction.mockResolvedValueOnce({ token: 'token' }); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([ + ['authorization', 'Bearer token'], + ]); + + // Token no longer available + tokenFunction.mockResolvedValueOnce({ token: undefined }); + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[2][0].headers.entries()]).toEqual([]); + }); + + it('does not overwrite an existing header with the same name', async () => { + const identityApi = { + getCredentials: () => ({ token: 'token' }), + } as unknown as IdentityApi; + + const middleware = new IdentityAuthInjectorFetchMiddleware( + identityApi, + ['https://example.com'], + 'Authorization', + token => `Bearer ${token}`, + ); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + // No token available + await outer(new Request('https://example.com')); + expect([...inner.mock.calls[0][0].headers.entries()]).toEqual([ + ['authorization', 'Bearer token'], + ]); + + // Supply a token, header gets added + await outer( + new Request('https://example.com', { + headers: { authorization: 'do-not-clobber' }, + }), + ); + expect([...inner.mock.calls[1][0].headers.entries()]).toEqual([ + ['authorization', 'do-not-clobber'], + ]); + }); + + it('does not affect requests outside the allowlist', async () => { + const identityApi = { + getCredentials: () => ({ token: 'token' }), + } as unknown as IdentityApi; + + const middleware = new IdentityAuthInjectorFetchMiddleware( + identityApi, + ['https://example.com:8080/root'], + 'Authorization', + token => `Bearer ${token}`, + ); + + const inner = jest.fn(); + const outer = middleware.apply(inner); + + await outer(new Request('https://example.com:8080/root')); + await outer(new Request('https://example.com:8080/root/sub')); + await outer(new Request('https://example.com:8080/root2')); + await outer(new Request('https://example.com/root')); + await outer(new Request('http://example.com:8080/root')); + await outer(new Request('https://example.com/root')); + + const no: string[][] = []; + const yes: string[][] = [['authorization', 'Bearer token']]; + expect([...inner.mock.calls[0][0].headers.entries()]).toEqual(yes); + expect([...inner.mock.calls[1][0].headers.entries()]).toEqual(yes); + expect([...inner.mock.calls[2][0].headers.entries()]).toEqual(no); + expect([...inner.mock.calls[3][0].headers.entries()]).toEqual(no); + expect([...inner.mock.calls[4][0].headers.entries()]).toEqual(no); + expect([...inner.mock.calls[5][0].headers.entries()]).toEqual(no); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.ts new file mode 100644 index 0000000000..46ada0a505 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/IdentityAuthInjectorFetchMiddleware.ts @@ -0,0 +1,82 @@ +/* + * 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 { Config } from '@backstage/config'; +import { IdentityApi } from '@backstage/core-plugin-api'; +import { FetchMiddleware } from './types'; + +/** + * A fetch middleware, which injects a Backstage token header when the user is + * signed in. + */ +export class IdentityAuthInjectorFetchMiddleware implements FetchMiddleware { + static create(options: { + identityApi: IdentityApi; + config?: Config; + urlPrefixAllowlist?: string[]; + header?: { + name: string; + value: (backstageToken: string) => string; + }; + }): IdentityAuthInjectorFetchMiddleware { + const allowlist: string[] = []; + if (options.urlPrefixAllowlist) { + allowlist.push(...options.urlPrefixAllowlist); + } else if (options.config) { + allowlist.push(options.config.getString('backend.baseUrl')); + } + + const headerName = options.header?.name || 'authorization'; + const headerValue = options.header?.value || (token => `Bearer ${token}`); + + return new IdentityAuthInjectorFetchMiddleware( + options.identityApi, + allowlist.map(prefix => prefix.replace(/\/$/, '')), + headerName, + headerValue, + ); + } + + constructor( + public readonly identityApi: IdentityApi, + public readonly urlPrefixAllowlist: string[], + public readonly headerName: string, + public readonly headerValue: (pluginId: string) => string, + ) {} + + apply(next: typeof fetch): typeof fetch { + return async (input, init) => { + // Skip this middleware if the header already exists, or if the URL + // doesn't match any of the allowlist items, or if there was no token + const request = new Request(input, init); + const { token } = await this.identityApi.getCredentials(); + if ( + request.headers.get(this.headerName) || + !this.urlPrefixAllowlist.some( + prefix => + request.url === prefix || request.url.startsWith(`${prefix}/`), + ) || + typeof token !== 'string' || + !token + ) { + return next(input, init); + } + + request.headers.set(this.headerName, this.headerValue(token)); + return next(request); + }; + } +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts new file mode 100644 index 0000000000..2f4ec6b536 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.test.ts @@ -0,0 +1,93 @@ +/* + * 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 { DiscoveryApi } from '@backstage/core-plugin-api'; +import { PluginProtocolResolverFetchMiddleware } from './PluginProtocolResolverFetchMiddleware'; + +describe('PluginProtocolResolverFetchMiddleware', () => { + it.each([['https://passthrough.com/a']])( + 'passes through regular URLs, %p', + async url => { + const resolve = jest.fn(); + const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const middleware = new PluginProtocolResolverFetchMiddleware( + discoveryApi, + ); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + await outer(url); + expect(inner.mock.calls[0][0]).toBe(url); + expect(resolve).not.toBeCalled(); + }, + ); + + it.each([ + [ + 'plugin://my-plugin/sub/path', + 'my-plugin', + 'https://real.com/base', + 'https://real.com/base/sub/path', + ], + [ + 'plugin://my-plugin/sub/path/', + 'my-plugin', + 'https://real.com/base/', + 'https://real.com/base/sub/path/', + ], + ['plugin://x', 'x', 'http://real.com:8080', 'http://real.com:8080'], + [ + 'plugin://x/a/b?c=d&e=f#g', + 'x', + 'https://real.com/base', + 'https://real.com/base/a/b?c=d&e=f#g', + ], + [ + 'plugin://x?c=d&e=f#g', + 'x', + 'https://real.com:8080/base', + 'https://real.com:8080/base?c=d&e=f#g', + ], + [ + 'plugin://username:password@x?c=d&e=f#g', + 'x', + 'https://real.com:8080/base', + 'https://username:password@real.com:8080/base?c=d&e=f#g', + ], + [ + 'plugin://x?c=d&e=f#g', + 'x', + 'https://username:password@real.com:8080/base', + 'https://username:password@real.com:8080/base?c=d&e=f#g', + ], + ])( + 'resolves backstage URLs, %p', + async (original, host, resolved, result) => { + const resolve = jest.fn(); + const discoveryApi = { getBaseUrl: resolve } as unknown as DiscoveryApi; + const middleware = new PluginProtocolResolverFetchMiddleware( + discoveryApi, + ); + const inner = jest.fn(); + const outer = middleware.apply(inner); + + resolve.mockResolvedValueOnce(resolved); + await outer(original); + expect(inner.mock.calls[0][0]).toBe(result); + expect(resolve).toHaveBeenLastCalledWith(host); + }, + ); +}); diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts new file mode 100644 index 0000000000..72834731fe --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/PluginProtocolResolverFetchMiddleware.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { FetchMiddleware } from './types'; + +function join(left: string, right: string): string { + if (!right || right === '/') { + return left; + } + + return `${left.replace(/\/$/, '')}/${right.replace(/^\//, '')}`; +} + +/** + * Handles translation from plugin://some-plugin-id/ to concrete http(s) + * URLs. + */ +export class PluginProtocolResolverFetchMiddleware implements FetchMiddleware { + constructor(private readonly discoveryApi: DiscoveryApi) {} + + apply(next: typeof fetch): typeof fetch { + return async (input, init) => { + const request = new Request(input, init); + const prefix = 'plugin://'; + + if (!request.url.startsWith(prefix)) { + return next(input, init); + } + + // Switch to a known protocol, since browser URL parsing misbehaves wildly + // on foreign protocols + const { hostname, pathname, search, hash, username, password } = new URL( + `http://${request.url.substring(prefix.length)}`, + ); + + let base = await this.discoveryApi.getBaseUrl(hostname); + if (username || password) { + const baseUrl = new URL(base); + const authority = `${username}${password ? `:${password}` : ''}@`; + base = `${baseUrl.protocol}//${authority}${baseUrl.host}${baseUrl.pathname}`; + } + + const target = `${join(base, pathname)}${search}${hash}`; + return next(target, request); + }; + } +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/createFetchApi.ts b/packages/core-app-api/src/apis/implementations/FetchApi/createFetchApi.ts new file mode 100644 index 0000000000..1b85695daa --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/createFetchApi.ts @@ -0,0 +1,46 @@ +/* + * 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 { FetchApi } from '@backstage/core-plugin-api'; +import { FetchMiddleware } from './types'; + +/** + * Builds a fetch API, based on the builtin fetch wrapped by a set of optional + * middleware implementations that add behaviors. + * + * @remarks + * + * The middleware are applied in reverse order, i.e. the last one will be + * "closest" to the base implementation. Passing in `[M1, M2, M3]` effectively + * leads to `M1(M2(M3(baseImplementation)))`. + * + * @public + */ +export function createFetchApi(options: { + baseImplementation?: typeof fetch | undefined; + middleware?: FetchMiddleware | FetchMiddleware[] | undefined; +}): FetchApi { + let result = options.baseImplementation || global.fetch; + + const middleware = [options.middleware ?? []].flat().reverse(); + for (const m of middleware) { + result = m.apply(result); + } + + return { + fetch: result, + }; +} diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/index.ts b/packages/core-app-api/src/apis/implementations/FetchApi/index.ts new file mode 100644 index 0000000000..0f82a9020b --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createFetchApi } from './createFetchApi'; +export { FetchMiddlewares } from './FetchMiddlewares'; +export type { FetchMiddleware } from './types'; diff --git a/packages/core-app-api/src/apis/implementations/FetchApi/types.ts b/packages/core-app-api/src/apis/implementations/FetchApi/types.ts new file mode 100644 index 0000000000..5bf029cbf0 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/FetchApi/types.ts @@ -0,0 +1,30 @@ +/* + * 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. + */ + +/** + * A middleware that modifies the behavior of an ongoing fetch. + * + * @public + */ +export interface FetchMiddleware { + /** + * Applies this middleware to an inner implementation. + * + * @param next - The next, inner, implementation, that this middleware shall + * call out to as part of the request cycle. + */ + apply(next: typeof fetch): typeof fetch; +} diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts index 4a0a07ef34..193c474361 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts @@ -16,14 +16,14 @@ import { OAuthRequestApi, - AuthRequesterOptions, + OAuthRequesterOptions, } from '@backstage/core-plugin-api'; import { OAuthRequestManager } from './OAuthRequestManager'; export default class MockOAuthApi implements OAuthRequestApi { private readonly real = new OAuthRequestManager(); - createAuthRequester(options: AuthRequesterOptions) { + createAuthRequester(options: OAuthRequesterOptions) { return this.real.createAuthRequester(options); } diff --git a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts index 7e7f3c2507..f9d9270b2a 100644 --- a/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts +++ b/packages/core-app-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts @@ -16,9 +16,9 @@ import { OAuthRequestApi, - PendingAuthRequest, - AuthRequester, - AuthRequesterOptions, + PendingOAuthRequest, + OAuthRequester, + OAuthRequesterOptions, } from '@backstage/core-plugin-api'; import { Observable } from '@backstage/types'; import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests'; @@ -34,11 +34,17 @@ import { BehaviorSubject } from '../../../lib/subjects'; * @public */ export class OAuthRequestManager implements OAuthRequestApi { - private readonly subject = new BehaviorSubject([]); - private currentRequests: PendingAuthRequest[] = []; + private readonly subject = new BehaviorSubject([]); + private currentRequests: PendingOAuthRequest[] = []; private handlerCount = 0; - createAuthRequester(options: AuthRequesterOptions): AuthRequester { + createAuthRequester(options: OAuthRequesterOptions): OAuthRequester { + if (!options.provider.id) { + // eslint-disable-next-line no-console + console.warn( + 'DEPRECATION WARNING: Not passing a provider id to createAuthRequester is deprecated, it will be required in the future', + ); + } const handler = new OAuthPendingRequests(); const index = this.handlerCount; @@ -67,8 +73,8 @@ export class OAuthRequestManager implements OAuthRequestApi { // Converts the pending request and popup options into a popup request that we can forward to subscribers. private makeAuthRequest( request: PendingRequest, - options: AuthRequesterOptions, - ): PendingAuthRequest | undefined { + options: OAuthRequesterOptions, + ): PendingOAuthRequest | undefined { const { scopes } = request; if (!scopes) { return undefined; @@ -88,7 +94,7 @@ export class OAuthRequestManager implements OAuthRequestApi { }; } - authRequest$(): Observable { + authRequest$(): Observable { return this.subject; } } diff --git a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts index d0a9dc5d99..d8f942b94b 100644 --- a/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/auth0/Auth0Auth.ts @@ -28,6 +28,23 @@ const DEFAULT_PROVIDER = { * Implements the OAuth flow to Auth0 products. * * @public + * @deprecated Use {@link OAuth2} instead + * + * @example + * + * ```ts + * OAuth2.create({ + * discoveryApi, + * oauthRequestApi, + * provider: { + * id: 'auth0', + * title: 'Auth0', + * icon: () => null, + * }, + * defaultScopes: ['openid', 'email', 'profile'], + * environment: configApi.getOptionalString('auth.environment'), + * }) + * ``` */ export default class Auth0Auth { static create(options: OAuthApiCreateOptions): typeof auth0AuthApiRef.T { diff --git a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts index 9493d0809e..ea9e2cad28 100644 --- a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts +++ b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts @@ -17,7 +17,7 @@ import { oneloginAuthApiRef, OAuthRequestApi, - AuthProvider, + AuthProviderInfo, DiscoveryApi, } from '@backstage/core-plugin-api'; import { OAuth2 } from '../oauth2'; @@ -30,7 +30,7 @@ export type OneLoginAuthCreateOptions = { discoveryApi: DiscoveryApi; oauthRequestApi: OAuthRequestApi; environment?: string; - provider?: AuthProvider & { id: string }; + provider?: AuthProviderInfo; }; const DEFAULT_PROVIDER = { diff --git a/packages/core-app-api/src/apis/implementations/auth/types.ts b/packages/core-app-api/src/apis/implementations/auth/types.ts index 825f433cec..55d6c19098 100644 --- a/packages/core-app-api/src/apis/implementations/auth/types.ts +++ b/packages/core-app-api/src/apis/implementations/auth/types.ts @@ -15,7 +15,7 @@ */ import { - AuthProvider, + AuthProviderInfo, DiscoveryApi, OAuthRequestApi, } from '@backstage/core-plugin-api'; @@ -36,5 +36,5 @@ export type OAuthApiCreateOptions = AuthApiCreateOptions & { export type AuthApiCreateOptions = { discoveryApi: DiscoveryApi; environment?: string; - provider?: AuthProvider & { id: string }; + provider?: AuthProviderInfo; }; diff --git a/packages/core-app-api/src/apis/implementations/index.ts b/packages/core-app-api/src/apis/implementations/index.ts index c494f966f2..1c79d3d164 100644 --- a/packages/core-app-api/src/apis/implementations/index.ts +++ b/packages/core-app-api/src/apis/implementations/index.ts @@ -27,5 +27,6 @@ export * from './ConfigApi'; export * from './DiscoveryApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; +export * from './FetchApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts index 261a008d4b..a467957063 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -15,9 +15,9 @@ */ import { - AuthRequester, + OAuthRequester, OAuthRequestApi, - AuthProvider, + AuthProviderInfo, DiscoveryApi, } from '@backstage/core-plugin-api'; import { showLoginPopup } from '../loginPopup'; @@ -36,7 +36,7 @@ type Options = { * Information about the auth provider to be shown to the user. * The ID Must match the backend auth plugin configuration, for example 'google'. */ - provider: AuthProvider & { id: string }; + provider: AuthProviderInfo; /** * API used to instantiate an auth requester. */ @@ -65,9 +65,9 @@ export class DefaultAuthConnector { private readonly discoveryApi: DiscoveryApi; private readonly environment: string; - private readonly provider: AuthProvider & { id: string }; + private readonly provider: AuthProviderInfo; private readonly joinScopesFunc: (scopes: Set) => string; - private readonly authRequester: AuthRequester; + private readonly authRequester: OAuthRequester; private readonly sessionTransform: (response: any) => Promise; constructor(options: Options) { diff --git a/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts index 61fdd825a2..200ba755ac 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DirectAuthConnector.ts @@ -13,18 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AuthProvider, DiscoveryApi } from '@backstage/core-plugin-api'; +import { AuthProviderInfo, DiscoveryApi } from '@backstage/core-plugin-api'; import { showLoginPopup } from '../loginPopup'; type Options = { discoveryApi: DiscoveryApi; environment?: string; - provider: AuthProvider & { id: string }; + provider: AuthProviderInfo; }; export class DirectAuthConnector { private readonly discoveryApi: DiscoveryApi; private readonly environment: string | undefined; - private readonly provider: AuthProvider & { id: string }; + private readonly provider: AuthProviderInfo; constructor(options: Options) { const { discoveryApi, environment, provider } = options; diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index 4eab9cea54..2171a72ccb 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/core-components +## 0.8.1 + +### Patch Changes + +- 2c17e5b073: Items in `` are now only active when their full path is active (including search parameters). +- 9d6503e86c: Switched out usage of deprecated `OAuthRequestApi` types from `@backstage/core-plugin-api`. +- 1680a1c5ac: Add Missing Override Components Type for SidebarSpace, SidebarSpacer, and SidebarDivider Components. +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + ## 0.8.0 ### Minor Changes diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index d098403113..f2b167691d 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -814,13 +814,10 @@ export type ResponseErrorPanelClassKey = 'text' | 'divider'; export function RoutedTabs(props: { routes: SubRoute_2[] }): JSX.Element; // Warning: (ae-forgotten-export) The symbol "SelectProps" needs to be exported by the entry point index.d.ts -// Warning: (ae-missing-release-tag) "SelectComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export function Select(props: SelectProps): JSX.Element; -// Warning: (ae-missing-release-tag) "SelectClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// // @public (undocumented) export type SelectClassKey = | 'formControl' @@ -830,11 +827,18 @@ export type SelectClassKey = | 'checkbox' | 'root'; -// Warning: (ae-missing-release-tag) "SelectInputBaseClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// +// @public (undocumented) +export type SelectedItems = string | string[] | number | number[]; + // @public (undocumented) export type SelectInputBaseClassKey = 'root' | 'input'; +// @public (undocumented) +export type SelectItem = { + label: string; + value: string | number; +}; + // Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "Sidebar" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1152,6 +1156,11 @@ export const SidebarDivider: React_2.ComponentType< } >; +// Warning: (ae-missing-release-tag) "SidebarDividerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SidebarDividerClassKey = 'root'; + // @public export const SidebarExpandButton: () => JSX.Element | null; @@ -1760,6 +1769,11 @@ export const SidebarSpace: React_2.ComponentType< } >; +// Warning: (ae-missing-release-tag) "SidebarSpaceClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SidebarSpaceClassKey = 'root'; + // Warning: (ae-missing-release-tag) "SidebarSpacer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2029,6 +2043,11 @@ export const SidebarSpacer: React_2.ComponentType< } >; +// Warning: (ae-missing-release-tag) "SidebarSpacerClassKey" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SidebarSpacerClassKey = 'root'; + // @public export const SidebarSubmenu: (props: SidebarSubmenuProps) => JSX.Element; diff --git a/packages/core-components/package.json b/packages/core-components/package.json index f47335fce8..b47a221227 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.8.0", + "version": "0.8.1", "private": false, "publishConfig": { "access": "public", @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.5", "@backstage/theme": "^0.2.14", "@material-table/core": "^3.1.0", @@ -72,8 +72,8 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/core-app-api": "^0.2.0", - "@backstage/cli": "^0.10.1", + "@backstage/core-app-api": "^0.2.1", + "@backstage/cli": "^0.10.2", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx index ef0d2373e4..0c6bd62bcb 100644 --- a/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx +++ b/packages/core-components/src/components/OAuthRequestDialog/LoginRequestListItem.tsx @@ -22,7 +22,7 @@ import Typography from '@material-ui/core/Typography'; import Button from '@material-ui/core/Button'; import React, { useState } from 'react'; import { isError } from '@backstage/errors'; -import { PendingAuthRequest } from '@backstage/core-plugin-api'; +import { PendingOAuthRequest } from '@backstage/core-plugin-api'; export type LoginRequestListItemClassKey = 'root'; @@ -36,7 +36,7 @@ const useItemStyles = makeStyles( ); type RowProps = { - request: PendingAuthRequest; + request: PendingOAuthRequest; busy: boolean; setBusy: (busy: boolean) => void; }; diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx index a0e3aba94b..022df0a7ad 100644 --- a/packages/core-components/src/components/Select/Select.tsx +++ b/packages/core-components/src/components/Select/Select.tsx @@ -21,17 +21,18 @@ import FormControl from '@material-ui/core/FormControl'; import InputBase from '@material-ui/core/InputBase'; import MenuItem from '@material-ui/core/MenuItem'; import Select from '@material-ui/core/Select'; -import Typography from '@material-ui/core/Typography'; import { createStyles, makeStyles, Theme, withStyles, } from '@material-ui/core/styles'; +import Typography from '@material-ui/core/Typography'; import React, { useEffect, useState } from 'react'; import ClosedDropdown from './static/ClosedDropdown'; import OpenedDropdown from './static/OpenedDropdown'; +/** @public */ export type SelectInputBaseClassKey = 'root' | 'input'; const BootstrapInput = withStyles( @@ -60,6 +61,7 @@ const BootstrapInput = withStyles( { name: 'BackstageSelectInputBase' }, )(InputBase); +/** @public */ export type SelectClassKey = | 'formControl' | 'label' @@ -93,6 +95,7 @@ const useStyles = makeStyles( margin: 2, }, checkbox: {}, + root: { display: 'flex', flexDirection: 'column', @@ -101,23 +104,28 @@ const useStyles = makeStyles( { name: 'BackstageSelect' }, ); -type Item = { +/** @public */ +export type SelectItem = { label: string; value: string | number; }; -type Selection = string | string[] | number | number[]; +/** @public */ +export type SelectedItems = string | string[] | number | number[]; export type SelectProps = { multiple?: boolean; - items: Item[]; + items: SelectItem[]; label: string; placeholder?: string; - selected?: Selection; - onChange: (arg: Selection) => void; + selected?: SelectedItems; + onChange: (arg: SelectedItems) => void; triggerReset?: boolean; + native?: boolean; + disabled?: boolean; }; +/** @public */ export function SelectComponent(props: SelectProps) { const { multiple, @@ -127,9 +135,11 @@ export function SelectComponent(props: SelectProps) { selected, onChange, triggerReset, + native = false, + disabled = false, } = props; const classes = useStyles(); - const [value, setValue] = useState( + const [value, setValue] = useState( selected || (multiple ? [] : ''), ); const [isOpen, setOpen] = useState(false); @@ -145,11 +155,15 @@ export function SelectComponent(props: SelectProps) { }, [selected]); const handleChange = (event: React.ChangeEvent<{ value: unknown }>) => { - setValue(event.target.value as Selection); - onChange(event.target.value as Selection); + setValue(event.target.value as SelectedItems); + onChange(event.target.value as SelectedItems); }; const handleClick = (event: React.ChangeEvent) => { + if (disabled) { + event.preventDefault(); + return; + } setOpen(previous => { if (multiple && !(event.target instanceof HTMLElement)) { return true; @@ -175,6 +189,8 @@ export function SelectComponent(props: SelectProps) { diff --git a/packages/core-components/src/components/Select/index.tsx b/packages/core-components/src/components/Select/index.tsx index 0f149f05b5..870c2a0899 100644 --- a/packages/core-components/src/components/Select/index.tsx +++ b/packages/core-components/src/components/Select/index.tsx @@ -15,6 +15,11 @@ */ export { SelectComponent as Select } from './Select'; -export type { SelectClassKey, SelectInputBaseClassKey } from './Select'; +export type { + SelectClassKey, + SelectedItems, + SelectInputBaseClassKey, + SelectItem, +} from './Select'; export type { ClosedDropdownClassKey } from './static/ClosedDropdown'; export type { OpenedDropdownClassKey } from './static/OpenedDropdown'; diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index c8c6db13a2..7d00db1b1a 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -46,6 +46,8 @@ import { SidebarItemWithSubmenuContext, } from './config'; import { SidebarSubmenu } from './SidebarSubmenu'; +import { isLocationMatch } from './utils'; +import { Location } from 'history'; export type SidebarItemClassKey = | 'root' @@ -172,7 +174,7 @@ const useStyles = makeStyles( function isSidebarItemWithSubmenuActive( submenu: ReactNode, - locationPathname: string, + currentLocation: Location, ) { // Item is active if any of submenu items have active paths const toPathnames: string[] = []; @@ -193,8 +195,8 @@ function isSidebarItemWithSubmenuActive( } }); isActive = toPathnames.some(to => { - const toPathname = resolvePath(to); - return locationPathname === toPathname.pathname; + const toLocation = resolvePath(to); + return isLocationMatch(currentLocation, toLocation); }); return isActive; } @@ -207,8 +209,8 @@ const SidebarItemWithSubmenu = ({ }: PropsWithChildren) => { const classes = useStyles(); const [isHoveredOn, setIsHoveredOn] = useState(false); - const { pathname: locationPathname } = useLocation(); - const isActive = isSidebarItemWithSubmenuActive(children, locationPathname); + const currentLocation = useLocation(); + const isActive = isSidebarItemWithSubmenuActive(children, currentLocation); const handleMouseEnter = () => { setIsHoveredOn(true); @@ -548,6 +550,8 @@ export function SidebarSearchField(props: SidebarSearchFieldProps) { ); } +export type SidebarSpaceClassKey = 'root'; + export const SidebarSpace = styled('div')( { flex: 1, @@ -555,6 +559,8 @@ export const SidebarSpace = styled('div')( { name: 'BackstageSidebarSpace' }, ); +export type SidebarSpacerClassKey = 'root'; + export const SidebarSpacer = styled('div')( { height: 8, @@ -562,6 +568,8 @@ export const SidebarSpacer = styled('div')( { name: 'BackstageSidebarSpacer' }, ); +export type SidebarDividerClassKey = 'root'; + export const SidebarDivider = styled('hr')( { height: 1, diff --git a/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx b/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx index 5e68aa8d62..6afd0bf157 100644 --- a/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx +++ b/packages/core-components/src/layout/Sidebar/Sidebar.stories.tsx @@ -17,8 +17,6 @@ import AddCircleOutlineIcon from '@material-ui/icons/AddCircleOutline'; import HomeOutlinedIcon from '@material-ui/icons/HomeOutlined'; import BuildRoundedIcon from '@material-ui/icons/BuildRounded'; -import LibraryBooksOutlinedIcon from '@material-ui/icons/LibraryBooksOutlined'; -import WebOutlinedIcon from '@material-ui/icons/WebOutlined'; import React from 'react'; import { MemoryRouter } from 'react-router-dom'; import { @@ -34,7 +32,7 @@ import { import { SidebarSubmenuItem } from './SidebarSubmenuItem'; import MenuBookIcon from '@material-ui/icons/MenuBook'; import CloudQueueIcon from '@material-ui/icons/CloudQueue'; -import SettingsApplications from '@material-ui/icons/SettingsApplications'; +import AppsIcon from '@material-ui/icons/Apps'; import AcUnitIcon from '@material-ui/icons/AcUnit'; import { SidebarSubmenu } from './SidebarSubmenu'; @@ -76,17 +74,7 @@ export const SampleScalableSidebar = () => ( - - - + (theme => ({ item: { @@ -116,14 +117,13 @@ export type SidebarSubmenuItemProps = { export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { const { title, to, icon: Icon, dropdownItems } = props; const classes = useStyles(); - const { pathname: locationPathname } = useLocation(); - const { pathname: toPathname } = useResolvedPath(to); const { setIsHoveredOn } = useContext(SidebarItemWithSubmenuContext); const closeSubmenu = () => { setIsHoveredOn(false); }; - - let isActive = locationPathname === toPathname; + const toLocation = useResolvedPath(to); + const currentLocation = useLocation(); + let isActive = isLocationMatch(currentLocation, toLocation); const [showDropDown, setShowDropDown] = useState(false); const handleClickDropdown = () => { @@ -132,7 +132,8 @@ export const SidebarSubmenuItem = (props: SidebarSubmenuItemProps) => { if (dropdownItems !== undefined) { dropdownItems.some(item => { const resolvedPath = resolvePath(item.to); - isActive = locationPathname === resolvedPath.pathname; + isActive = isLocationMatch(currentLocation, resolvedPath); + return isActive; }); return (
diff --git a/packages/core-components/src/layout/Sidebar/index.ts b/packages/core-components/src/layout/Sidebar/index.ts index 79a86023ee..3d922f1322 100644 --- a/packages/core-components/src/layout/Sidebar/index.ts +++ b/packages/core-components/src/layout/Sidebar/index.ts @@ -33,7 +33,12 @@ export { SidebarSpacer, SidebarScrollWrapper, } from './Items'; -export type { SidebarItemClassKey } from './Items'; +export type { + SidebarItemClassKey, + SidebarSpaceClassKey, + SidebarSpacerClassKey, + SidebarDividerClassKey, +} from './Items'; export { IntroCard, SidebarIntro } from './Intro'; export type { SidebarIntroClassKey } from './Intro'; export { diff --git a/packages/core-components/src/layout/Sidebar/utils.test.ts b/packages/core-components/src/layout/Sidebar/utils.test.ts new file mode 100644 index 0000000000..b08dcda0fc --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/utils.test.ts @@ -0,0 +1,98 @@ +/* + * 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 { Location, Path } from 'history'; +import { isLocationMatch } from './utils'; + +describe('isLocationMatching', () => { + let currentLocation: Location; + let toLocation: Path; + + it('return false when pathname in target and current location differ', async () => { + currentLocation = { + pathname: '/catalog', + search: '?kind=component', + state: null, + hash: '', + key: '', + }; + toLocation = { + pathname: '/catalog-a', + search: '?kind=component', + hash: '', + }; + expect(isLocationMatch(currentLocation, toLocation)).toBe(false); + }); + + it('return true when exact match between current and target location parameters', async () => { + currentLocation = { + pathname: '/catalog', + search: '?kind=component', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '?kind=component', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation)).toBe(true); + }); + + it('return true when target query parameters are subset of current location query parameters', async () => { + currentLocation = { + pathname: '/catalog', + search: '?x=foo&y=bar', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '?x=foo', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation)).toBe(true); + }); + + it('return false when no matching query parameters between target and current location', async () => { + currentLocation = { + pathname: '/catalog', + search: '?y=bar', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '?x=foo', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation)).toBe(false); + }); + + it('return true when query parameters match in different order', async () => { + currentLocation = { + pathname: '/catalog', + search: '?y=bar&x=foo', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '?x=foo&y=bar', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation)).toBe(true); + }); + + it('return true when there is a matching query parameter alongside extra parameters', async () => { + currentLocation = { + pathname: '/catalog', + search: '?y=bar&x=foo', + state: null, + hash: '', + key: '', + }; + toLocation = { pathname: '/catalog', search: '', hash: '' }; + expect(isLocationMatch(currentLocation, toLocation)).toBe(true); + }); +}); diff --git a/packages/core-components/src/layout/Sidebar/utils.ts b/packages/core-components/src/layout/Sidebar/utils.ts new file mode 100644 index 0000000000..1afebe796d --- /dev/null +++ b/packages/core-components/src/layout/Sidebar/utils.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Location, Path } from 'history'; +import { isEqual, isMatch } from 'lodash'; +import qs from 'qs'; + +export function isLocationMatch(currentLocation: Location, toLocation: Path) { + const toDecodedSearch = new URLSearchParams(toLocation.search).toString(); + const toQueryParameters = qs.parse(toDecodedSearch); + + const currentDecodedSearch = new URLSearchParams( + currentLocation.search, + ).toString(); + const currentQueryParameters = qs.parse(currentDecodedSearch); + + const matching = + isEqual(toLocation.pathname, currentLocation.pathname) && + isMatch(currentQueryParameters, toQueryParameters); + + return matching; +} diff --git a/packages/core-components/src/overridableComponents.ts b/packages/core-components/src/overridableComponents.ts index 98b8f83abd..c53c47da1b 100644 --- a/packages/core-components/src/overridableComponents.ts +++ b/packages/core-components/src/overridableComponents.ts @@ -83,6 +83,9 @@ import { ItemCardHeaderClassKey, PageClassKey, SidebarClassKey, + SidebarSpaceClassKey, + SidebarSpacerClassKey, + SidebarDividerClassKey, SidebarIntroClassKey, SidebarItemClassKey, SidebarPageClassKey, @@ -157,6 +160,9 @@ type BackstageComponentsNameToClassKey = { BackstageItemCardHeader: ItemCardHeaderClassKey; BackstagePage: PageClassKey; BackstageSidebar: SidebarClassKey; + BackstageSidebarSpace: SidebarSpaceClassKey; + BackstageSidebarSpacer: SidebarSpacerClassKey; + BackstageSidebarDivider: SidebarDividerClassKey; BackstageSidebarIntro: SidebarIntroClassKey; BackstageSidebarItem: SidebarItemClassKey; BackstageSidebarPage: SidebarPageClassKey; diff --git a/packages/core-plugin-api/CHANGELOG.md b/packages/core-plugin-api/CHANGELOG.md index fdf64779fb..06d36ef486 100644 --- a/packages/core-plugin-api/CHANGELOG.md +++ b/packages/core-plugin-api/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/core-plugin-api +## 0.3.1 + +### Patch Changes + +- 18d4f500af: Deprecated the `AnyAnalyticsContext` type and mark the `AnalyticsApi` experimental. +- 8a7372cfd5: Deprecated `auth0AuthApiRef`, `oauth2ApiRef`, `oidcAuthApiRef`, `samlAuthApiRef`, and marked the rest of the auth `ApiRef`s as experimental. For more information on how to address the deprecations, see https://backstage.io/docs/api/deprecations#generic-auth-api-refs. +- 760791a642: Renamed `AuthProvider` to `AuthProviderInfo` and add a required 'id' property to match the majority of usage. The `AuthProvider` type without the `id` property still exists but is deprecated, and all usage of it without an `id` is deprecated as well. For example, calling `createAuthRequest` without a `provider.id` is deprecated and it will be required in the future. + + The following types have been renamed. The old names are still exported but deprecated, and are scheduled for removal in a future release. + + - Renamed `AuthRequesterOptions` to `OAuthRequesterOptions` + - Renamed `AuthRequester` to `OAuthRequester` + - Renamed `PendingAuthRequest` to `PendingOAuthRequest` + ## 0.3.0 ### Minor Changes diff --git a/packages/core-plugin-api/api-report.md b/packages/core-plugin-api/api-report.md index d0ac0b5352..0cf97a4c13 100644 --- a/packages/core-plugin-api/api-report.md +++ b/packages/core-plugin-api/api-report.md @@ -35,25 +35,26 @@ export type AlertMessage = { severity?: 'success' | 'info' | 'warning' | 'error'; }; -// @public +// @alpha export type AnalyticsApi = { captureEvent(event: AnalyticsEvent): void; }; -// @public +// @alpha export const analyticsApiRef: ApiRef; -// @public +// @alpha export const AnalyticsContext: (options: { attributes: Partial; children: ReactNode; }) => JSX.Element; -// @public -export type AnalyticsContextValue = CommonAnalyticsContext & - AnyAnalyticsContext; +// @alpha +export type AnalyticsContextValue = CommonAnalyticsContext & { + [param in string]: string | boolean | number | undefined; +}; -// @public +// @alpha export type AnalyticsEvent = { action: string; subject: string; @@ -62,12 +63,12 @@ export type AnalyticsEvent = { context: AnalyticsContextValue; }; -// @public +// @alpha export type AnalyticsEventAttributes = { [attribute in string]: string | boolean | number; }; -// @public +// @alpha export type AnalyticsTracker = { captureEvent: ( action: string, @@ -79,7 +80,7 @@ export type AnalyticsTracker = { ) => void; }; -// @public +// @public @deprecated export type AnyAnalyticsContext = { [param in string]: string | boolean | number | undefined; }; @@ -143,18 +144,6 @@ export type ApiRefConfig = { description?: string; }; -// @public @deprecated -export type ApiRefsToTypes< - T extends { - [key in string]: ApiRef; - }, -> = { - [key in keyof T]: ApiRefType; -}; - -// @public @deprecated -export type ApiRefType = T extends ApiRef ? U : never; - // @public export type AppComponents = { NotFoundErrorPage: ComponentType<{}>; @@ -194,7 +183,7 @@ export type AppThemeApi = { // @public export const appThemeApiRef: ApiRef; -// @public +// @alpha export const atlassianAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; @@ -206,27 +195,26 @@ export function attachComponentData

( data: unknown, ): void; -// @public +// @public @deprecated export const auth0AuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; +// @public @deprecated (undocumented) +export type AuthProvider = Omit; + // @public -export type AuthProvider = { +export type AuthProviderInfo = { + id: string; title: string; icon: IconComponent; }; -// @public -export type AuthRequester = ( - scopes: Set, -) => Promise; +// @public @deprecated (undocumented) +export type AuthRequester = OAuthRequester; -// @public -export type AuthRequesterOptions = { - provider: AuthProvider; - onAuthRequest(scopes: Set): Promise; -}; +// @public @deprecated (undocumented) +export type AuthRequesterOptions = OAuthRequesterOptions; // @public export type AuthRequestOptions = { @@ -272,7 +260,7 @@ export type BackstageUserIdentity = { ownershipEntityRefs: string[]; }; -// @public +// @alpha export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; @@ -283,7 +271,7 @@ export type BootErrorPageProps = { error: Error; }; -// @public +// @alpha export type CommonAnalyticsContext = { pluginId: string; routeRef: string; @@ -506,23 +494,31 @@ export enum FeatureFlagState { None = 0, } +// @public +export type FetchApi = { + fetch: typeof fetch; +}; + +// @public +export const fetchApiRef: ApiRef; + // @public export function getComponentData( node: ReactNode, type: string, ): T | undefined; -// @public +// @alpha export const githubAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// @public +// @alpha export const gitlabAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi >; -// @public +// @alpha export const googleAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -570,7 +566,7 @@ export type MergeParams< P2 extends AnyParams, > = (P1[keyof P1] extends never ? {} : P1) & (P2 extends undefined ? {} : P2); -// @public +// @alpha export const microsoftAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -579,7 +575,7 @@ export const microsoftAuthApiRef: ApiRef< SessionApi >; -// @public +// @public @deprecated export const oauth2ApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -598,15 +594,28 @@ export type OAuthApi = { // @public export type OAuthRequestApi = { - createAuthRequester( - options: AuthRequesterOptions, - ): AuthRequester; - authRequest$(): Observable_2; + createAuthRequester( + options: OAuthRequesterOptions, + ): OAuthRequester; + authRequest$(): Observable_2; }; // @public export const oauthRequestApiRef: ApiRef; +// @public +export type OAuthRequester = ( + scopes: Set, +) => Promise; + +// @public +export type OAuthRequesterOptions = { + provider: Omit & { + id?: string; + }; + onAuthRequest(scopes: Set): Promise; +}; + // @public export type OAuthScope = string | string[]; @@ -616,7 +625,7 @@ export type Observable = Observable_2; // @public @deprecated export type Observer = Observer_2; -// @public +// @public @deprecated export const oidcAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -625,7 +634,7 @@ export const oidcAuthApiRef: ApiRef< SessionApi >; -// @public +// @alpha export const oktaAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -637,7 +646,7 @@ export const oktaAuthApiRef: ApiRef< // @public export type OldIconComponent = ComponentType; -// @public +// @alpha export const oneloginAuthApiRef: ApiRef< OAuthApi & OpenIdConnectApi & @@ -679,10 +688,15 @@ export type PathParams = { [name in ParamNames]: string; }; +// @public @deprecated (undocumented) +export type PendingAuthRequest = PendingOAuthRequest; + // @public -export type PendingAuthRequest = { - provider: AuthProvider; - reject: () => void; +export type PendingOAuthRequest = { + provider: Omit & { + id?: string; + }; + reject(): void; trigger(): Promise; }; @@ -738,7 +752,7 @@ export type RouteRef = { title?: string; }; -// @public +// @public @deprecated export const samlAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi >; @@ -803,7 +817,7 @@ export type TypesToApiRefs = { [key in keyof T]: ApiRef; }; -// @public +// @alpha export function useAnalytics(): AnalyticsTracker; // @public diff --git a/packages/core-plugin-api/package.json b/packages/core-plugin-api/package.json index 72827bde73..20479acfeb 100644 --- a/packages/core-plugin-api/package.json +++ b/packages/core-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-plugin-api", "description": "Core API used by Backstage plugins", - "version": "0.3.0", + "version": "0.3.1", "private": false, "publishConfig": { "access": "public", @@ -45,8 +45,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx b/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx index 075cdff3ff..af362cd921 100644 --- a/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx +++ b/packages/core-plugin-api/src/analytics/AnalyticsContext.tsx @@ -60,7 +60,7 @@ export const useAnalyticsContext = (): AnalyticsContextValue => { * Analytics contexts are additive, meaning the context ultimately emitted with * an event is the combination of all contexts in the parent tree. * - * @public + * @alpha */ export const AnalyticsContext = (options: { attributes: Partial; diff --git a/packages/core-plugin-api/src/analytics/types.ts b/packages/core-plugin-api/src/analytics/types.ts index d001c38d3e..431e453a61 100644 --- a/packages/core-plugin-api/src/analytics/types.ts +++ b/packages/core-plugin-api/src/analytics/types.ts @@ -17,7 +17,7 @@ /** * Common analytics context attributes. * - * @public + * @alpha */ export type CommonAnalyticsContext = { /** @@ -40,6 +40,7 @@ export type CommonAnalyticsContext = { * Allows arbitrary scalar values as context attributes too. * * @public + * @deprecated Will be removed, use `AnalyticsContextValue` instead */ export type AnyAnalyticsContext = { [param in string]: string | boolean | number | undefined; @@ -48,7 +49,8 @@ export type AnyAnalyticsContext = { /** * Analytics context envelope. * - * @public + * @alpha */ -export type AnalyticsContextValue = CommonAnalyticsContext & - AnyAnalyticsContext; +export type AnalyticsContextValue = CommonAnalyticsContext & { + [param in string]: string | boolean | number | undefined; +}; diff --git a/packages/core-plugin-api/src/analytics/useAnalytics.tsx b/packages/core-plugin-api/src/analytics/useAnalytics.tsx index 5a047e5a63..f1297c5e4e 100644 --- a/packages/core-plugin-api/src/analytics/useAnalytics.tsx +++ b/packages/core-plugin-api/src/analytics/useAnalytics.tsx @@ -35,7 +35,7 @@ function useAnalyticsApi(): AnalyticsApi { /** * Gets a pre-configured analytics tracker. * - * @public + * @alpha */ export function useAnalytics(): AnalyticsTracker { const trackerRef = useRef(null); diff --git a/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts b/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts index ef190d6c21..dc38d7a250 100644 --- a/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/AnalyticsApi.ts @@ -21,7 +21,7 @@ import { AnalyticsContextValue } from '../../analytics/types'; * Represents an event worth tracking in an analytics system that could inform * how users of a Backstage instance are using its features. * - * @public + * @alpha */ export type AnalyticsEvent = { /** @@ -79,7 +79,7 @@ export type AnalyticsEvent = { * A structure allowing other arbitrary metadata to be provided by analytics * event emitters. * - * @public + * @alpha */ export type AnalyticsEventAttributes = { [attribute in string]: string | boolean | number; @@ -89,7 +89,7 @@ export type AnalyticsEventAttributes = { * Represents a tracker with methods that can be called to track events in a * configured analytics service. * - * @public + * @alpha */ export type AnalyticsTracker = { captureEvent: ( @@ -103,6 +103,8 @@ export type AnalyticsTracker = { }; /** + * **EXPERIMENTAL** + * * The Analytics API is used to track user behavior in a Backstage instance. * * @remarks @@ -111,7 +113,7 @@ export type AnalyticsTracker = { * useAnalytics() hook. This will return a pre-configured AnalyticsTracker * with relevant methods for instrumentation. * - * @public + * @alpha */ export type AnalyticsApi = { /** @@ -122,9 +124,11 @@ export type AnalyticsApi = { }; /** + * **EXPERIMENTAL** + * * The {@link ApiRef} of {@link AnalyticsApi}. * - * @public + * @alpha */ export const analyticsApiRef: ApiRef = createApiRef({ id: 'core.analytics', diff --git a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts index 45ee0901c8..364a7bd29a 100644 --- a/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/ErrorApi.ts @@ -41,7 +41,15 @@ export type Error = ErrorApiError; * @public */ export type ErrorApiErrorContext = { - // If set to true, this error should not be displayed to the user. Defaults to false. + /** + * If set to true, this error should not be displayed to the user. + * + * Hidden errors are typically not displayed in the UI, but the ErrorApi + * implementation may still report them to error tracking services + * or other utilities that care about all errors. + * + * @defaultValue false + */ hidden?: boolean; }; diff --git a/packages/core-plugin-api/src/apis/definitions/FetchApi.ts b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts new file mode 100644 index 0000000000..ba304157ea --- /dev/null +++ b/packages/core-plugin-api/src/apis/definitions/FetchApi.ts @@ -0,0 +1,37 @@ +/* + * 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 { ApiRef, createApiRef } from '../system'; + +/** + * A wrapper for the fetch API, that has additional behaviors such as the + * ability to automatically inject auth information where necessary. + * + * @public + */ +export type FetchApi = { + fetch: typeof fetch; +}; + +/** + * A wrapper for the fetch API, that has additional behaviors such as the + * ability to automatically inject auth information where necessary. + * + * @public + */ +export const fetchApiRef: ApiRef = createApiRef({ + id: 'core.fetch', +}); diff --git a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts index 03e129ef6a..5b8dab29f7 100644 --- a/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-plugin-api/src/apis/definitions/OAuthRequestApi.ts @@ -14,31 +14,15 @@ * limitations under the License. */ -import { IconComponent } from '../../icons/types'; import { Observable } from '@backstage/types'; import { ApiRef, createApiRef } from '../system'; +import { AuthProviderInfo } from './auth'; /** - * Information about the auth provider that we're requesting a login towards. - * - * @remarks - * - * This should be shown to the user so that they can be informed about what login is being requested - * before a popup is shown. - * * @public + * @deprecated Use AuthProviderInfo instead */ -export type AuthProvider = { - /** - * Title for the auth provider, for example "GitHub" - */ - title: string; - - /** - * Icon for the auth provider. - */ - icon: IconComponent; -}; +export type AuthProvider = Omit; /** * Describes how to handle auth requests. Both how to show them to the user, and what to do when @@ -46,26 +30,34 @@ export type AuthProvider = { * * @public */ -export type AuthRequesterOptions = { +export type OAuthRequesterOptions = { /** * Information about the auth provider, which will be forwarded to auth requests. + * + * Not passing in an `id` is deprecated, and it will be required in the future. */ - provider: AuthProvider; + provider: Omit & { id?: string }; /** * Implementation of the auth flow, which will be called synchronously when * trigger() is called on an auth requests. */ - onAuthRequest(scopes: Set): Promise; + onAuthRequest(scopes: Set): Promise; }; +/** + * @public + * @deprecated Use OAuthRequesterOptions instead + */ +export type AuthRequesterOptions = OAuthRequesterOptions; + /** * Function used to trigger new auth requests for a set of scopes. * * @remarks * * The returned promise will resolve to the same value returned by the onAuthRequest in the - * {@link AuthRequesterOptions}. Or rejected, if the request is rejected. + * {@link OAuthRequesterOptions}. Or rejected, if the request is rejected. * * This function can be called multiple times before the promise resolves. All calls * will be merged into one request, and the scopes forwarded to the onAuthRequest will be the @@ -73,9 +65,15 @@ export type AuthRequesterOptions = { * * @public */ -export type AuthRequester = ( +export type OAuthRequester = ( scopes: Set, -) => Promise; +) => Promise; + +/** + * @public + * @deprecated Use OAuthRequester instead + */ +export type AuthRequester = OAuthRequester; /** * An pending auth request for a single auth provider. The request will remain in this pending @@ -88,16 +86,18 @@ export type AuthRequester = ( * * @public */ -export type PendingAuthRequest = { +export type PendingOAuthRequest = { /** * Information about the auth provider, as given in the AuthRequesterOptions + * + * Not passing in an `id` is deprecated, and it will be required in the future. */ - provider: AuthProvider; + provider: Omit & { id?: string }; /** * Rejects the request, causing all pending AuthRequester calls to fail with "RejectedError". */ - reject: () => void; + reject(): void; /** * Trigger the auth request to continue the auth flow, by for example showing a popup. @@ -107,6 +107,12 @@ export type PendingAuthRequest = { trigger(): Promise; }; +/** + * @public + * @deprecated Use PendingOAuthRequest instead + */ +export type PendingAuthRequest = PendingOAuthRequest; + /** * Provides helpers for implemented OAuth login flows within Backstage. * @@ -125,9 +131,9 @@ export type OAuthRequestApi = { * * See AuthRequesterOptions, AuthRequester, and handleAuthRequests for more info. */ - createAuthRequester( - options: AuthRequesterOptions, - ): AuthRequester; + createAuthRequester( + options: OAuthRequesterOptions, + ): OAuthRequester; /** * Observers pending auth requests. The returned observable will emit all @@ -140,7 +146,7 @@ export type OAuthRequestApi = { * If a auth is triggered, and the auth handler resolves successfully, then all currently pending * AuthRequester calls will resolve to the value returned by the onAuthRequest call. */ - authRequest$(): Observable; + authRequest$(): Observable; }; /** diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 37308ec29b..a362d16750 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -15,6 +15,7 @@ */ import { ApiRef, createApiRef } from '../system'; +import { IconComponent } from '../../icons/types'; import { Observable } from '@backstage/types'; /** @@ -28,6 +29,33 @@ import { Observable } from '@backstage/types'; * const googleAuthApiRef = createApiRef({ ... }) */ +/** + * Information about the auth provider. + * + * @remarks + * + * This information is used both to connect the correct auth provider in the backend, as + * well as displaying the provider to the user. + * + * @public + */ +export type AuthProviderInfo = { + /** + * The ID of the auth provider. This should match with ID of the provider in the `@backstage/auth-backend`. + */ + id: string; + + /** + * Title for the auth provider, for example "GitHub" + */ + title: string; + + /** + * Icon for the auth provider. + */ + icon: IconComponent; +}; + /** * An array of scopes, or a scope string formatted according to the * auth provider, which is typically a space separated list. @@ -282,14 +310,14 @@ export type SessionApi = { /** * Provides authentication towards Google APIs and identities. * + * @alpha This API is **EXPERIMENTAL** and might change in the future. + * * @remarks * * See {@link https://developers.google.com/identity/protocols/googlescopes} for a full list of supported scopes. * * Note that the ID token payload is only guaranteed to contain the user's numerical Google ID, * email and expiration information. Do not rely on any other fields, as they might not be present. - * - * @public */ export const googleAuthApiRef: ApiRef< OAuthApi & @@ -304,12 +332,12 @@ export const googleAuthApiRef: ApiRef< /** * Provides authentication towards GitHub APIs. * + * @alpha This API is **EXPERIMENTAL** and might change in the future. + * * @remarks * * See {@link https://developer.github.com/apps/building-oauth-apps/understanding-scopes-for-oauth-apps/} * for a full list of supported scopes. - * - * @public */ export const githubAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -320,12 +348,12 @@ export const githubAuthApiRef: ApiRef< /** * Provides authentication towards Okta APIs. * + * @alpha This API is **EXPERIMENTAL** and might change in the future. + * * @remarks * * See {@link https://developer.okta.com/docs/guides/implement-oauth-for-okta/scopes/} * for a full list of supported scopes. - * - * @public */ export const oktaAuthApiRef: ApiRef< OAuthApi & @@ -340,12 +368,12 @@ export const oktaAuthApiRef: ApiRef< /** * Provides authentication towards GitLab APIs. * + * @alpha This API is **EXPERIMENTAL** and might change in the future. + * * @remarks * * See {@link https://docs.gitlab.com/ee/user/profile/personal_access_tokens.html#limiting-scopes-of-a-personal-access-token} * for a full list of supported scopes. - * - * @public */ export const gitlabAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -362,6 +390,7 @@ export const gitlabAuthApiRef: ApiRef< * for a full list of supported scopes. * * @public + * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs */ export const auth0AuthApiRef: ApiRef< OpenIdConnectApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -372,13 +401,13 @@ export const auth0AuthApiRef: ApiRef< /** * Provides authentication towards Microsoft APIs and identities. * + * @alpha This API is **EXPERIMENTAL** and might change in the future. + * * @remarks * * For more info and a full list of supported scopes, see: * - {@link https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-permissions-and-consent} * - {@link https://docs.microsoft.com/en-us/graph/permissions-reference} - * - * @public */ export const microsoftAuthApiRef: ApiRef< OAuthApi & @@ -394,6 +423,7 @@ export const microsoftAuthApiRef: ApiRef< * Provides authentication for custom identity providers. * * @public + * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs */ export const oauth2ApiRef: ApiRef< OAuthApi & @@ -409,6 +439,7 @@ export const oauth2ApiRef: ApiRef< * Provides authentication for custom OpenID Connect identity providers. * * @public + * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs */ export const oidcAuthApiRef: ApiRef< OAuthApi & @@ -424,6 +455,7 @@ export const oidcAuthApiRef: ApiRef< * Provides authentication for SAML-based identity providers. * * @public + * @deprecated See https://backstage.io/docs/api/deprecations#generic-auth-api-refs */ export const samlAuthApiRef: ApiRef< ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -434,7 +466,7 @@ export const samlAuthApiRef: ApiRef< /** * Provides authentication towards OneLogin APIs. * - * @public + * @alpha This API is **EXPERIMENTAL** and might change in the future. */ export const oneloginAuthApiRef: ApiRef< OAuthApi & @@ -449,12 +481,11 @@ export const oneloginAuthApiRef: ApiRef< /** * Provides authentication towards Bitbucket APIs. * + * @alpha This API is **EXPERIMENTAL** and might change in the future. * @remarks * * See {@link https://support.atlassian.com/bitbucket-cloud/docs/use-oauth-on-bitbucket-cloud/} * for a full list of supported scopes. - * - * @public */ export const bitbucketAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi @@ -465,12 +496,11 @@ export const bitbucketAuthApiRef: ApiRef< /** * Provides authentication towards Atlassian APIs. * + * @alpha This API is **EXPERIMENTAL** and might change in the future. * @remarks * * See {@link https://developer.atlassian.com/cloud/jira/platform/scopes-for-connect-and-oauth-2-3LO-apps/} * for a full list of supported scopes. - * - * @public */ export const atlassianAuthApiRef: ApiRef< OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi diff --git a/packages/core-plugin-api/src/apis/definitions/index.ts b/packages/core-plugin-api/src/apis/definitions/index.ts index b7666bcb54..67d442587d 100644 --- a/packages/core-plugin-api/src/apis/definitions/index.ts +++ b/packages/core-plugin-api/src/apis/definitions/index.ts @@ -29,6 +29,7 @@ export * from './ConfigApi'; export * from './DiscoveryApi'; export * from './ErrorApi'; export * from './FeatureFlagsApi'; +export * from './FetchApi'; export * from './IdentityApi'; export * from './OAuthRequestApi'; export * from './StorageApi'; diff --git a/packages/core-plugin-api/src/apis/system/types.ts b/packages/core-plugin-api/src/apis/system/types.ts index a449e83450..96614c320c 100644 --- a/packages/core-plugin-api/src/apis/system/types.ts +++ b/packages/core-plugin-api/src/apis/system/types.ts @@ -31,33 +31,13 @@ export type ApiRef = { */ export type AnyApiRef = ApiRef; -/** - * Transforms ApiRef type into its inner API type. - * - * @public - * @deprecated unused type. - */ -export type ApiRefType = T extends ApiRef ? U : never; - /** * Wraps a type with API properties into a type holding their respective {@link ApiRef}s. - * Reverse type transform of {@link ApiRefsToTypes}. * * @public */ export type TypesToApiRefs = { [key in keyof T]: ApiRef }; -/** - * Unwraps type with {@link ApiRef} properties into a type holding their respective API types. - * Reverse type transform of {@link TypesToApiRefs}. - * - * @public - * @deprecated unused type. - */ -export type ApiRefsToTypes }> = { - [key in keyof T]: ApiRefType; -}; - /** * Provides lookup of APIs through their {@link ApiRef}s. * diff --git a/packages/core-plugin-api/src/routing/RouteRef.ts b/packages/core-plugin-api/src/routing/RouteRef.ts index 8bd3d604f6..37d79f3b83 100644 --- a/packages/core-plugin-api/src/routing/RouteRef.ts +++ b/packages/core-plugin-api/src/routing/RouteRef.ts @@ -23,17 +23,6 @@ import { } from './types'; import { OldIconComponent } from '../icons/types'; -/** - * @deprecated - * @internal - */ -export type RouteRefConfig = { - params?: ParamKeys; - path?: string; - icon?: OldIconComponent; - title: string; -}; - /** * @internal */ diff --git a/packages/core-plugin-api/src/routing/types.ts b/packages/core-plugin-api/src/routing/types.ts index bd419abb76..6232810e56 100644 --- a/packages/core-plugin-api/src/routing/types.ts +++ b/packages/core-plugin-api/src/routing/types.ts @@ -142,23 +142,6 @@ export type AnyRouteRef = | SubRouteRef | ExternalRouteRef; -// TODO(Rugvip): None of these should be found in the wild anymore, remove in next minor release -/** - * @deprecated - * @internal - */ -export type ConcreteRoute = {}; -/** - * @deprecated - * @internal - */ -export type AbsoluteRouteRef = RouteRef<{}>; -/** - * @deprecated - * @internal - */ -export type MutableRouteRef = RouteRef<{}>; - /** * A duplicate of the react-router RouteObject, but with routeRef added * @internal diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index cdcf7f4952..60d8a55635 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/create-app +## 0.4.8 + +### Patch Changes + +- 25dfc2d483: Updated the root `package.json` to include files with `.cjs` and `.mjs` extensions in the `"lint-staged"` configuration. + + To make this change to an existing app, apply the following changes to the `package.json` file: + + ```diff + "lint-staged": { + - "*.{js,jsx,ts,tsx}": [ + + "*.{js,jsx,ts,tsx,mjs,cjs}": [ + ``` + ## 0.4.7 ### Patch Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index ec739820b9..7a6c7ad551 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.7", + "version": "0.4.8", "private": false, "publishConfig": { "access": "public" @@ -41,7 +41,7 @@ }, "devDependencies": { "@types/fs-extra": "^9.0.1", - "@types/inquirer": "^7.3.1", + "@types/inquirer": "^8.1.3", "@types/node": "^14.14.32", "@types/recursive-readdir": "^2.2.0", "mock-fs": "^5.1.1", diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs index b0747c4454..a73045c050 100644 --- a/packages/create-app/templates/default-app/package.json.hbs +++ b/packages/create-app/templates/default-app/package.json.hbs @@ -38,7 +38,7 @@ }, "prettier": "@spotify/prettier-config", "lint-staged": { - "*.{js,jsx,ts,tsx}": [ + "*.{js,jsx,ts,tsx,mjs,cjs}": [ "eslint --fix", "prettier --write" ], diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index 72ff42311d..f5852b0164 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -59,7 +59,7 @@ module.exports = ({ args }) => { }, }, { - test: /\.(jsx?|mjs)$/, + test: /\.(jsx?|mjs|cjs)$/, exclude: /node_modules/, loader: require.resolve('@sucrase/webpack-loader'), options: { diff --git a/packages/test-utils/api-report.md b/packages/test-utils/api-report.md index a01f306fab..295cf3a8c3 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -34,52 +34,6 @@ export type ErrorWithContext = { context?: ErrorApiErrorContext; }; -// @public @deprecated (undocumented) -export class Keyboard { - constructor( - target: any, - { - debug, - }?: { - debug?: boolean | undefined; - }, - ); - // (undocumented) - click(): Promise; - // (undocumented) - debug: boolean; - // (undocumented) - document: any; - // (undocumented) - enter(value: any): Promise; - // (undocumented) - escape(): Promise; - // (undocumented) - get focused(): any; - // (undocumented) - static fromReadableInput(input: any): any; - // (undocumented) - _log(message: any, ...args: any[]): void; - // (undocumented) - _pretty(element: any): string; - // (undocumented) - send(chars: any): Promise; - // (undocumented) - _sendKey(key: any, charCode: any, action: any): Promise; - // (undocumented) - tab(): Promise; - // (undocumented) - static toReadableInput(chars: any): any; - // (undocumented) - toString(): string; - // (undocumented) - static type(target: any, input: any): Promise; - // (undocumented) - type(input: any): Promise; - // (undocumented) - static typeDebug(target: any, input: any): Promise; -} - // @public export type LogCollector = AsyncLogCollector | SyncLogCollector; diff --git a/packages/test-utils/src/testUtils/Keyboard.js b/packages/test-utils/src/testUtils/Keyboard.js deleted file mode 100644 index 2a7928f8cd..0000000000 --- a/packages/test-utils/src/testUtils/Keyboard.js +++ /dev/null @@ -1,225 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { act, fireEvent } from '@testing-library/react'; - -const codes = { - Tab: 9, - Enter: 10, - Click: 17 /* This keyboard can click, deal with it */, - Esc: 27, -}; - -/** - * @public - * @deprecated superseded by {@link @testing-library/user-event#userEvent} - */ -export class Keyboard { - static async type(target, input) { - await new Keyboard(target).type(input); - } - - static async typeDebug(target, input) { - await new Keyboard(target, { debug: true }).type(input); - } - - static toReadableInput(chars) { - return chars.split('').map(char => { - switch (char.charCodeAt(0)) { - case codes.Tab: - return ''; - case codes.Enter: - return ''; - case codes.Click: - return ''; - case codes.Esc: - return ''; - default: - return char; - } - }); - } - - static fromReadableInput(input) { - return input.trim().replace(/\s*<([a-zA-Z]+)>\s*/g, (match, name) => { - if (name in codes) { - return String.fromCharCode(codes[name]); - } - throw new Error(`Unknown char name: '${name}'`); - }); - } - - constructor(target, { debug = false } = {}) { - this.debug = debug; - - if (target.ownerDocument) { - this.document = target.ownerDocument; - } else if (target.baseElement) { - this.document = target.baseElement.ownerDocument; - } else { - throw new TypeError( - 'Keyboard(target): target must be DOM node or react-testing-library render() output', - ); - } - } - - toString() { - return `Keyboard{document=${this.document}, debug=${this.debug}}`; - } - - _log(message, ...args) { - if (this.debug) { - // eslint-disable-next-line no-console - console.log(`[Keyboard] ${message}`, ...args); - } - } - - _pretty(element) { - const attrs = [...element.attributes] - .map(attr => `${attr.name}="${attr.value}"`) - .join(' '); - return `<${element.nodeName.toLocaleLowerCase('en-US')} ${attrs}>`; - } - - get focused() { - return this.document.activeElement; - } - - async type(input) { - this._log( - `sending sequence '${input}' with initial focus ${this._pretty( - this.focused, - )}`, - ); - await this.send(Keyboard.fromReadableInput(input)); - } - - async send(chars) { - for (const key of chars.split('')) { - const charCode = key.charCodeAt(0); - - if (charCode === codes.Tab) { - await this.tab(); - continue; - } - - const focused = this.focused; - if (!focused || focused === this.document.body) { - throw Error( - `No element focused in document while trying to type '${Keyboard.toReadableInput( - chars, - )}'`, - ); - } - const nextValue = (focused.value || '') + key; - - if (charCode >= 32) { - await this._sendKey(key, charCode, () => { - this._log( - `sending +${key} = '${nextValue}' to ${this._pretty(focused)}`, - ); - fireEvent.change(focused, { - target: { value: nextValue }, - bubbles: true, - cancelable: true, - }); - }); - } else if (charCode === codes.Enter) { - await this.enter(focused.value || ''); - } else if (charCode === codes.Esc) { - await this.escape(); - } else if (charCode === codes.Click) { - await this.click(); - } else { - throw new Error(`Unsupported char code, ${charCode}`); - } - } - } - - async click() { - this._log(`clicking ${this._pretty(this.focused)}`); - await act(async () => fireEvent.click(this.focused)); - } - - async tab() { - await this._sendKey('Tab', codes.Tab, () => { - const focusable = this.document.querySelectorAll( - [ - 'a[href]', - 'area[href]', - 'input:not([disabled])', - 'select:not([disabled])', - 'textarea:not([disabled])', - 'button:not([disabled])', - 'iframe', - 'object', - 'embed', - '*[tabindex]', - '*[contenteditable]', - ].join(','), - ); - - const tabbable = [...focusable].filter(el => { - return el.tabIndex >= 0; - }); - - const focused = this.document.activeElement; - const focusedIndex = tabbable.indexOf(focused); - const nextFocus = tabbable[focusedIndex + (1 % tabbable.length)]; - - this._log( - `tabbing to ${this._pretty(nextFocus)} ${this.focused.textContent}`, - ); - nextFocus.focus(); - }); - } - - async enter(value) { - this._log(`submitting '${value}' via ${this._pretty(this.focused)}`); - await act(() => - this._sendKey('Enter', codes.Enter, () => { - if (this.focused.type === 'button') { - fireEvent.click(this.focused, { target: { value } }); - } else { - fireEvent.submit(this.focused, { - target: { value }, - bubbles: true, - cancelable: true, - }); - } - }), - ); - } - - async escape() { - this._log(`escape from ${this._pretty(this.focused)}`); - await act(async () => this._sendKey('Escape', codes.Esc)); - } - - async _sendKey(key, charCode, action) { - const event = { key, charCode, keyCode: charCode, which: charCode }; - const focused = this.focused; - - if (fireEvent.keyDown(focused, event)) { - if (fireEvent.keyPress(focused, event)) { - if (action) { - action(); - } - } - } - fireEvent.keyUp(focused, event); - } -} diff --git a/packages/test-utils/src/testUtils/Keyboard.test.js b/packages/test-utils/src/testUtils/Keyboard.test.js deleted file mode 100644 index 41ee3a12d6..0000000000 --- a/packages/test-utils/src/testUtils/Keyboard.test.js +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright 2020 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { Keyboard } from './Keyboard'; -import { render } from '@testing-library/react'; - -describe('testUtils.Keyboard', () => { - it('types into some inputs with focus and submits a form', async () => { - const typed1 = []; - const typed2 = []; - const typed3 = []; - - let submitted = false; - const handleSubmit = event => { - event.preventDefault(); - submitted = true; - }; - - const rendered = render( -

- typed1.push(value)} /> - typed2.push(value)} - /* eslint-disable-next-line jsx-a11y/no-autofocus */ - autoFocus - /> - typed3.push(value)} /> - , - ); - - const keyboard = new Keyboard(rendered); - await keyboard.send('xy'); - await keyboard.tab(); - await keyboard.send('abc'); - await keyboard.enter(); - - expect(typed1).toEqual([]); - expect(typed2).toEqual(['x', 'xy']); - expect(typed3).toEqual(['a', 'ab', 'abc']); - expect(submitted).toBe(true); - }); - - it('can use Keyboard.type to send readable input', async () => { - const typed1 = []; - const typed2 = []; - const typed3 = []; - - let submitted = false; - const handleSubmit = event => { - event.preventDefault(); - submitted = true; - }; - - const rendered = render( -
- typed1.push(value)} - /> - typed2.push(value)} - /> - typed3.push(value)} - /> -
, - ); - - await Keyboard.type(rendered, ' a b c '); - - expect(typed1).toEqual(['1a']); - expect(typed2).toEqual(['2b']); - expect(typed3).toEqual(['3c']); - expect(submitted).toBe(true); - }); - - it('should be able to navigate a radio input with click', async () => { - const selections = []; - - const rendered = render( -
selections.push(value)}> - - - -
, - ); - - await Keyboard.type(rendered, ' '); - - expect(selections).toEqual(['a', 'c']); - }); -}); diff --git a/packages/test-utils/src/testUtils/index.tsx b/packages/test-utils/src/testUtils/index.tsx index c778c5c837..a7850fc158 100644 --- a/packages/test-utils/src/testUtils/index.tsx +++ b/packages/test-utils/src/testUtils/index.tsx @@ -19,7 +19,6 @@ export { default as mockBreakpoint } from './mockBreakpoint'; export { wrapInTestApp, renderInTestApp } from './appWrappers'; export type { TestAppOptions } from './appWrappers'; export * from './msw'; -export * from './Keyboard'; export * from './logCollector'; export * from './testingLibrary'; export { TestApiProvider, TestApiRegistry } from './TestApiProvider'; diff --git a/plugins/allure/package.json b/plugins/allure/package.json index 8b3ee3b882..261c5e1879 100644 --- a/plugins/allure/package.json +++ b/plugins/allure/package.json @@ -23,8 +23,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -37,8 +37,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json index 15055b2a4f..5f48d4404e 100644 --- a/plugins/analytics-module-ga/package.json +++ b/plugins/analytics-module-ga/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/config": "^0.1.5", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -35,8 +35,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md new file mode 100644 index 0000000000..64abdb929a --- /dev/null +++ b/plugins/apache-airflow/CHANGELOG.md @@ -0,0 +1,22 @@ +# @backstage/plugin-apache-airflow + +## 0.1.0 + +### Minor Changes + +- 9aea335911: Introduces a new plugin for the Apache Airflow workflow management platform. + This implementation has been tested with the Apache Airflow v2 API, + authenticating with basic authentication through the Backstage proxy plugin. + + Supported functionality includes: + + - Information card of version information of the Airflow instance + - Information card of instance health for the meta-database and scheduler + - Table of DAGs with meta information and status, along with a link to view + details in the Airflow UI + +### Patch Changes + +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 diff --git a/plugins/apache-airflow/README.md b/plugins/apache-airflow/README.md index c57ab9da43..bdbb1b981f 100644 --- a/plugins/apache-airflow/README.md +++ b/plugins/apache-airflow/README.md @@ -2,6 +2,9 @@ Welcome to the apache-airflow plugin! +This plugin serves as frontend to the REST API exposed by Apache Airflow. +Note only [Airflow v2 (and later)](https://airflow.apache.org/docs/apache-airflow/stable/deprecated-rest-api-ref.html) integrate with the plugin. + ## Feature Requests & Ideas - [ ] Add support for running multiple instances of Airflow for monitoring diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json index a361961d12..58c5dceb77 100644 --- a/plugins/apache-airflow/package.json +++ b/plugins/apache-airflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apache-airflow", - "version": "0.0.0", + "version": "0.1.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,8 +20,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", @@ -33,8 +33,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 93df7e68aa..fd057cf358 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -32,8 +32,8 @@ "dependencies": { "@asyncapi/react-component": "^1.0.0-next.25", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog": "^0.7.4", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", @@ -54,8 +54,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index dc45804449..cfdae9f4ef 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-backend +## 0.5.1 + +### Patch Changes + +- 699c2e9ddc: export minimal typescript types for OIDC provider +- Updated dependencies + - @backstage/backend-common@0.9.14 + - @backstage/catalog-model@0.9.8 + ## 0.5.0 ### Minor Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index a637118130..9ed523c678 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -47,6 +47,16 @@ export type AtlassianProviderOptions = { }; }; +// @public +export type AuthHandler = ( + input: AuthResult, +) => Promise; + +// @public +export type AuthHandlerResult = { + profile: ProfileInfo; +}; + // Warning: (ae-missing-release-tag) "AuthProviderFactory" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -258,7 +268,6 @@ export const createOAuth2Provider: ( options?: OAuth2ProviderOptions | undefined, ) => AuthProviderFactory; -// Warning: (ae-forgotten-export) The symbol "OidcProviderOptions" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "createOidcProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -534,6 +543,20 @@ export type OAuthState = { origin?: string; }; +// @public +export type OidcAuthResult = { + tokenset: TokenSet; + userinfo: UserinfoResponse; +}; + +// @public +export type OidcProviderOptions = { + authHandler?: AuthHandler; + signIn?: { + resolver?: SignInResolver; + }; +}; + // Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -606,6 +629,22 @@ export type SamlProviderOptions = { }; }; +// @public +export type SignInInfo = { + profile: ProfileInfo; + result: AuthResult; +}; + +// @public +export type SignInResolver = ( + info: SignInInfo, + context: { + tokenIssuer: TokenIssuer; + catalogIdentityClient: CatalogIdentityClient; + logger: Logger_2; + }, +) => Promise; + // Warning: (ae-missing-release-tag) "TokenIssuer" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -637,8 +676,6 @@ export type WebMessageResponse = // Warnings were encountered during analysis: // // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts -// src/providers/atlassian/provider.d.ts:37:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts -// src/providers/atlassian/provider.d.ts:42:9 - (ae-forgotten-export) The symbol "SignInResolver" needs to be exported by the entry point index.d.ts // src/providers/aws-alb/provider.d.ts:77:5 - (ae-forgotten-export) The symbol "AwsAlbResult" needs to be exported by the entry point index.d.ts // src/providers/github/provider.d.ts:71:58 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag // src/providers/github/provider.d.ts:71:90 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index b2822c4f33..c7d208eef2 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-auth-backend", "description": "A Backstage backend plugin that handles authentication", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.13", + "@backstage/backend-common": "^0.9.14", "@backstage/catalog-client": "^0.5.2", - "@backstage/catalog-model": "^0.9.7", + "@backstage/catalog-model": "^0.9.8", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", "@backstage/test-utils": "^0.1.24", @@ -73,7 +73,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 92c76b04b7..a3fc77bc02 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -17,7 +17,7 @@ import express from 'express'; import { THOUSAND_DAYS_MS, TEN_MINUTES_MS, OAuthAdapter } from './OAuthAdapter'; import { encodeState } from './helpers'; -import { OAuthHandlers } from './types'; +import { OAuthHandlers, OAuthResponse } from './types'; const mockResponseData = { providerInfo: { @@ -36,6 +36,12 @@ const mockResponseData = { }, }; +function mkTokenBody(payload: unknown): string { + return Buffer.from(JSON.stringify(payload), 'utf8') + .toString('base64') + .replace(/=/g, ''); +} + describe('OAuthAdapter', () => { class MyAuthProvider implements OAuthHandlers { async start() { @@ -249,4 +255,86 @@ describe('OAuthAdapter', () => { 'Refresh token is not supported for provider test-provider', ); }); + + it('correctly populates incomplete identities', async () => { + const mockRefresh = jest.fn, [express.Request]>(); + + const oauthProvider = new OAuthAdapter( + { + refresh: mockRefresh, + start: jest.fn(), + handler: jest.fn(), + } as OAuthHandlers, + { + ...oAuthProviderOptions, + tokenIssuer: { + issueToken: async ({ claims }) => `a.${mkTokenBody(claims)}.a`, + listPublicKeys: async () => ({ keys: [] }), + }, + disableRefresh: false, + isOriginAllowed: () => false, + }, + ); + + const mockRequest = { + header: () => 'XMLHttpRequest', + cookies: { + 'test-provider-refresh-token': 'token', + }, + query: {}, + } as unknown as express.Request; + + const mockResponse = { + json: jest.fn().mockReturnThis(), + status: jest.fn().mockReturnThis(), + } as unknown as express.Response; + + // Without a token + mockRefresh.mockResolvedValueOnce({ + ...mockResponseData, + backstageIdentity: { + id: 'foo', + token: '', + }, + }); + await oauthProvider.refresh(mockRequest, mockResponse); + expect(mockResponse.json).toHaveBeenCalledTimes(1); + expect(mockResponse.json).toHaveBeenLastCalledWith({ + ...mockResponseData, + backstageIdentity: { + id: 'foo', + token: `a.${mkTokenBody({ sub: 'user:default/foo' })}.a`, + idToken: `a.${mkTokenBody({ sub: 'user:default/foo' })}.a`, + identity: { + type: 'user', + userEntityRef: 'user:default/foo', + ownershipEntityRefs: [], + }, + }, + }); + + // With a token + mockRefresh.mockResolvedValueOnce({ + ...mockResponseData, + backstageIdentity: { + id: 'foo', + token: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`, + }, + }); + await oauthProvider.refresh(mockRequest, mockResponse); + expect(mockResponse.json).toHaveBeenCalledTimes(2); + expect(mockResponse.json).toHaveBeenLastCalledWith({ + ...mockResponseData, + backstageIdentity: { + id: 'foo', + token: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`, + idToken: `z.${mkTokenBody({ sub: 'user:my-ns/foo' })}.z`, + identity: { + type: 'user', + userEntityRef: 'user:my-ns/foo', + ownershipEntityRefs: [], + }, + }, + }); + }); }); diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index eb3f7efa42..4d5d507aa1 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -17,6 +17,11 @@ import express from 'express'; import crypto from 'crypto'; import { URL } from 'url'; +import { + ENTITY_DEFAULT_NAMESPACE, + parseEntityRef, + stringifyEntityRef, +} from '@backstage/catalog-model'; import { AuthProviderRouteHandlers, AuthProviderConfig, @@ -243,8 +248,14 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { return prepareBackstageIdentityResponse(identity); } + const userEntityRef = stringifyEntityRef( + parseEntityRef(identity.id, { + defaultKind: 'user', + defaultNamespace: ENTITY_DEFAULT_NAMESPACE, + }), + ); const token = await this.options.tokenIssuer.issueToken({ - claims: { sub: identity.id }, + claims: { sub: userEntityRef }, }); return prepareBackstageIdentityResponse({ ...identity, token }); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 4ecbb98fd2..9589e97265 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -34,6 +34,10 @@ export type { AuthProviderRouteHandlers, AuthProviderFactoryOptions, AuthProviderFactory, + AuthHandler, + AuthHandlerResult, + SignInResolver, + SignInInfo, } from './types'; // These types are needed for a postMessage from the login pop-up diff --git a/plugins/auth-backend/src/providers/oidc/index.ts b/plugins/auth-backend/src/providers/oidc/index.ts index 20d014a783..39bd02928d 100644 --- a/plugins/auth-backend/src/providers/oidc/index.ts +++ b/plugins/auth-backend/src/providers/oidc/index.ts @@ -13,5 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +export type { OidcAuthResult, OidcProviderOptions } from './provider'; export { createOidcProvider } from './provider'; diff --git a/plugins/auth-backend/src/providers/oidc/provider.ts b/plugins/auth-backend/src/providers/oidc/provider.ts index 158af6f830..fe4c042500 100644 --- a/plugins/auth-backend/src/providers/oidc/provider.ts +++ b/plugins/auth-backend/src/providers/oidc/provider.ts @@ -56,7 +56,11 @@ type OidcImpl = { client: Client; }; -type AuthResult = { +/** + * authentication result for the OIDC which includes the token set and user information (a profile response sent by OIDC server) + * @public + */ +export type OidcAuthResult = { tokenset: TokenSet; userinfo: UserinfoResponse; }; @@ -66,8 +70,8 @@ export type Options = OAuthProviderOptions & { scope?: string; prompt?: string; tokenSignedResponseAlg?: string; - signInResolver?: SignInResolver; - authHandler: AuthHandler; + signInResolver?: SignInResolver; + authHandler: AuthHandler; tokenIssuer: TokenIssuer; catalogIdentityClient: CatalogIdentityClient; logger: Logger; @@ -78,8 +82,8 @@ export class OidcAuthProvider implements OAuthHandlers { private readonly scope?: string; private readonly prompt?: string; - private readonly signInResolver?: SignInResolver; - private readonly authHandler: AuthHandler; + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; private readonly tokenIssuer: TokenIssuer; private readonly catalogIdentityClient: CatalogIdentityClient; private readonly logger: Logger; @@ -113,7 +117,7 @@ export class OidcAuthProvider implements OAuthHandlers { ): Promise<{ response: OAuthResponse; refreshToken?: string }> { const { strategy } = await this.implementation; const strategyResponse = await executeFrameHandlerStrategy< - AuthResult, + OidcAuthResult, PrivateInfo >(req, strategy); const { @@ -158,7 +162,7 @@ export class OidcAuthProvider implements OAuthHandlers { ( tokenset: TokenSet, userinfo: UserinfoResponse, - done: PassportDoneCallback, + done: PassportDoneCallback, ) => { if (typeof done !== 'function') { throw new Error( @@ -180,7 +184,7 @@ export class OidcAuthProvider implements OAuthHandlers { // Use this function to grab the user profile info from the token // Then populate the profile with it - private async handleResult(result: AuthResult): Promise { + private async handleResult(result: OidcAuthResult): Promise { const { profile } = await this.authHandler(result); const response: OAuthResponse = { providerInfo: { @@ -210,27 +214,37 @@ export class OidcAuthProvider implements OAuthHandlers { } } -export const oAuth2DefaultSignInResolver: SignInResolver = async ( - info, - ctx, -) => { - const { profile } = info; +export const oAuth2DefaultSignInResolver: SignInResolver = + async (info, ctx) => { + const { profile } = info; - if (!profile.email) { - throw new Error('Profile contained no email'); - } - const userId = profile.email.split('@')[0]; - const token = await ctx.tokenIssuer.issueToken({ - claims: { sub: userId, ent: [`user:default/${userId}`] }, - }); - return { id: userId, token }; -}; + if (!profile.email) { + throw new Error('Profile contained no email'); + } + const userId = profile.email.split('@')[0]; + const token = await ctx.tokenIssuer.issueToken({ + claims: { sub: userId, ent: [`user:default/${userId}`] }, + }); + return { id: userId, token }; + }; +/** + * OIDC provider callback options. An auth handler and a sign in resolver + * can be passed while creating a OIDC provider. + * + * authHandler : called after sign in was successful, a new object must be returned which includes a profile + * signInResolver: called after sign in was successful, expects to return a new {@link BackstageSignInResult} + * + * Both options are optional. There is fallback for authHandler where the default handler expect an e-mail explicitly + * otherwise it throws an error + * + * @public + */ export type OidcProviderOptions = { - authHandler?: AuthHandler; + authHandler?: AuthHandler; signIn?: { - resolver?: SignInResolver; + resolver?: SignInResolver; }; }; @@ -260,7 +274,7 @@ export const createOidcProvider = ( tokenIssuer, }); - const authHandler: AuthHandler = options?.authHandler + const authHandler: AuthHandler = options?.authHandler ? options.authHandler : async ({ userinfo }) => ({ profile: { @@ -271,7 +285,7 @@ export const createOidcProvider = ( }); const signInResolverFn = options?.signIn?.resolver ?? oAuth2DefaultSignInResolver; - const signInResolver: SignInResolver = info => + const signInResolver: SignInResolver = info => signInResolverFn(info, { catalogIdentityClient, tokenIssuer, diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index dafa52163c..ecb466c768 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -240,6 +240,10 @@ export type ProfileInfo = { picture?: string; }; +/** + * type of sign in information context, includes the profile information and authentication result which contains auth. related information + * @public + */ export type SignInInfo = { /** * The simple profile passed down for use in the frontend. @@ -252,6 +256,11 @@ export type SignInInfo = { result: AuthResult; }; +/** + * Sign in resolver type describes the function which handles the result of a successful authentication + * and it must return a valid {@link BackstageSignInResult} + * @public + */ export type SignInResolver = ( info: SignInInfo, context: { @@ -261,6 +270,10 @@ export type SignInResolver = ( }, ) => Promise; +/** + * The return type of authentication handler which must contain a valid profile information + * @public + */ export type AuthHandlerResult = { profile: ProfileInfo }; /** @@ -270,6 +283,8 @@ export type AuthHandlerResult = { profile: ProfileInfo }; * * Throwing an error in the function will cause the authentication to fail, making it * possible to use this function as a way to limit access to a certain group of users. + * + * @public */ export type AuthHandler = ( input: AuthResult, diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index e0e74a9bc6..ff802a30e1 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,66 @@ # @backstage/plugin-azure-devops-backend +## 0.2.5 + +### Patch Changes + +- daf32e2c9b: Created some initial filters that can be used to create pull request columns: + + - All + - AssignedToUser + - AssignedToCurrentUser + - AssignedToTeam + - AssignedToTeams + - AssignedToCurrentUsersTeams + - CreatedByUser + - CreatedByCurrentUser + - CreatedByTeam + - CreatedByTeams + - CreatedByCurrentUsersTeams + + Example custom column creation: + + ```tsx + const COLUMN_CONFIGS: PullRequestColumnConfig[] = [ + { + title: 'Created by me', + filters: [{ type: FilterType.CreatedByCurrentUser }], + }, + { + title: 'Created by Backstage Core', + filters: [ + { + type: FilterType.CreatedByTeam, + teamName: 'Backstage Core', + }, + ], + }, + { + title: 'Assigned to my teams', + filters: [{ type: FilterType.AssignedToCurrentUsersTeams }], + }, + { + title: 'Other PRs', + filters: [{ type: FilterType.All }], + simplified: true, + }, + ]; + + + } + />; + ``` + +- Updated dependencies + - @backstage/backend-common@0.9.14 + - @backstage/plugin-azure-devops-common@0.1.3 + ## 0.2.4 ### Patch Changes diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index 9741380fb1..a7aec3f2d6 100644 --- a/plugins/azure-devops-backend/package.json +++ b/plugins/azure-devops-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-backend", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.13", + "@backstage/backend-common": "^0.9.14", "@backstage/config": "^0.1.11", - "@backstage/plugin-azure-devops-common": "^0.1.2", + "@backstage/plugin-azure-devops-common": "^0.1.3", "@types/express": "^4.17.6", "azure-devops-node-api": "^11.0.1", "express": "^4.17.1", @@ -31,7 +31,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", "@types/supertest": "^2.0.8", "supertest": "^4.0.2", "msw": "^0.35.0" diff --git a/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts b/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts index aa843e9c17..7a0d0a279e 100644 --- a/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts +++ b/plugins/azure-devops-backend/src/utils/azure-devops-utils.ts @@ -215,6 +215,7 @@ function convertReviewer( return { id: identityRef.id, displayName: identityRef.displayName, + uniqueName: identityRef.uniqueName, imageUrl: getAvatarUrl(identityRef), isRequired: identityRef.isRequired, isContainer: identityRef.isContainer, diff --git a/plugins/azure-devops-common/CHANGELOG.md b/plugins/azure-devops-common/CHANGELOG.md index b2c8645e94..84c2cae9a5 100644 --- a/plugins/azure-devops-common/CHANGELOG.md +++ b/plugins/azure-devops-common/CHANGELOG.md @@ -1,5 +1,62 @@ # @backstage/plugin-azure-devops-common +## 0.1.3 + +### Patch Changes + +- daf32e2c9b: Created some initial filters that can be used to create pull request columns: + + - All + - AssignedToUser + - AssignedToCurrentUser + - AssignedToTeam + - AssignedToTeams + - AssignedToCurrentUsersTeams + - CreatedByUser + - CreatedByCurrentUser + - CreatedByTeam + - CreatedByTeams + - CreatedByCurrentUsersTeams + + Example custom column creation: + + ```tsx + const COLUMN_CONFIGS: PullRequestColumnConfig[] = [ + { + title: 'Created by me', + filters: [{ type: FilterType.CreatedByCurrentUser }], + }, + { + title: 'Created by Backstage Core', + filters: [ + { + type: FilterType.CreatedByTeam, + teamName: 'Backstage Core', + }, + ], + }, + { + title: 'Assigned to my teams', + filters: [{ type: FilterType.AssignedToCurrentUsersTeams }], + }, + { + title: 'Other PRs', + filters: [{ type: FilterType.All }], + simplified: true, + }, + ]; + + + } + />; + ``` + ## 0.1.2 ### Patch Changes diff --git a/plugins/azure-devops-common/api-report.md b/plugins/azure-devops-common/api-report.md index bb458f169a..85a9e54847 100644 --- a/plugins/azure-devops-common/api-report.md +++ b/plugins/azure-devops-common/api-report.md @@ -61,6 +61,10 @@ export interface CreatedBy { // (undocumented) imageUrl?: string; // (undocumented) + teamIds?: string[]; + // (undocumented) + teamNames?: string[]; + // (undocumented) uniqueName?: string; } @@ -254,6 +258,8 @@ export interface Reviewer { // (undocumented) isRequired?: boolean; // (undocumented) + uniqueName?: string; + // (undocumented) voteStatus: PullRequestVoteStatus; } diff --git a/plugins/azure-devops-common/package.json b/plugins/azure-devops-common/package.json index 7b82296c40..f8a9622bff 100644 --- a/plugins/azure-devops-common/package.json +++ b/plugins/azure-devops-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops-common", - "version": "0.1.2", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "devDependencies": { - "@backstage/cli": "^0.10.1" + "@backstage/cli": "^0.10.2" }, "files": [ "dist" diff --git a/plugins/azure-devops-common/src/types.ts b/plugins/azure-devops-common/src/types.ts index e6bd174079..0befcf0b60 100644 --- a/plugins/azure-devops-common/src/types.ts +++ b/plugins/azure-devops-common/src/types.ts @@ -145,6 +145,7 @@ export interface DashboardPullRequest { export interface Reviewer { id?: string; displayName?: string; + uniqueName?: string; imageUrl?: string; isRequired?: boolean; isContainer?: boolean; @@ -164,6 +165,8 @@ export interface CreatedBy { displayName?: string; uniqueName?: string; imageUrl?: string; + teamIds?: string[]; + teamNames?: string[]; } export interface Repository { diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index 57f80f508b..c79c3abdbc 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,69 @@ # @backstage/plugin-azure-devops +## 0.1.7 + +### Patch Changes + +- daf32e2c9b: Created some initial filters that can be used to create pull request columns: + + - All + - AssignedToUser + - AssignedToCurrentUser + - AssignedToTeam + - AssignedToTeams + - AssignedToCurrentUsersTeams + - CreatedByUser + - CreatedByCurrentUser + - CreatedByTeam + - CreatedByTeams + - CreatedByCurrentUsersTeams + + Example custom column creation: + + ```tsx + const COLUMN_CONFIGS: PullRequestColumnConfig[] = [ + { + title: 'Created by me', + filters: [{ type: FilterType.CreatedByCurrentUser }], + }, + { + title: 'Created by Backstage Core', + filters: [ + { + type: FilterType.CreatedByTeam, + teamName: 'Backstage Core', + }, + ], + }, + { + title: 'Assigned to my teams', + filters: [{ type: FilterType.AssignedToCurrentUsersTeams }], + }, + { + title: 'Other PRs', + filters: [{ type: FilterType.All }], + simplified: true, + }, + ]; + + + } + />; + ``` + +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + - @backstage/plugin-azure-devops-common@0.1.3 + - @backstage/catalog-model@0.9.8 + - @backstage/plugin-catalog-react@0.6.7 + ## 0.1.6 ### Patch Changes diff --git a/plugins/azure-devops/api-report.md b/plugins/azure-devops/api-report.md index 0f3e3310ef..11e238e6f5 100644 --- a/plugins/azure-devops/api-report.md +++ b/plugins/azure-devops/api-report.md @@ -6,9 +6,55 @@ /// import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; import { Entity } from '@backstage/catalog-model'; import { SvgIconProps } from '@material-ui/core'; +// Warning: (ae-missing-release-tag) "AllFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AllFilter = BaseFilter & { + type: FilterType.All; +}; + +// Warning: (ae-missing-release-tag) "AssignedToTeamFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AssignedToTeamFilter = BaseFilter & { + type: FilterType.AssignedToTeam; + teamId: string; +}; + +// Warning: (ae-missing-release-tag) "AssignedToTeamsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AssignedToTeamsFilter = BaseFilter & + ( + | { + type: FilterType.AssignedToTeams; + teamIds: string[]; + } + | { + type: FilterType.AssignedToCurrentUsersTeams; + teamIds?: string[]; + } + ); + +// Warning: (ae-missing-release-tag) "AssignedToUserFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type AssignedToUserFilter = BaseFilter & + ( + | { + type: FilterType.AssignedToUser; + email: string; + } + | { + type: FilterType.AssignedToCurrentUser; + email?: string; + } + ); + // Warning: (ae-missing-release-tag) "azureDevOpsPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -25,11 +71,71 @@ export const AzurePullRequestsIcon: (props: SvgIconProps) => JSX.Element; export const AzurePullRequestsPage: ({ projectName, pollingInterval, + defaultColumnConfigs, }: { projectName?: string | undefined; pollingInterval?: number | undefined; + defaultColumnConfigs?: PullRequestColumnConfig[] | undefined; }) => JSX.Element; +// Warning: (ae-missing-release-tag) "BaseFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type BaseFilter = { + type: FilterType; +}; + +// Warning: (ae-missing-release-tag) "CreatedByTeamFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type CreatedByTeamFilter = BaseFilter & + ({ + type: FilterType.CreatedByTeam; + } & ( + | { + teamId: string; + } + | { + teamName: string; + } + )); + +// Warning: (ae-missing-release-tag) "CreatedByTeamsFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type CreatedByTeamsFilter = BaseFilter & + ( + | ({ + type: FilterType.CreatedByTeams; + } & ( + | { + teamIds: string[]; + } + | { + teamNames: string[]; + } + )) + | { + type: FilterType.CreatedByCurrentUsersTeams; + teamIds?: string[]; + } + ); + +// Warning: (ae-missing-release-tag) "CreatedByUserFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type CreatedByUserFilter = BaseFilter & + ( + | { + type: FilterType.CreatedByUser; + email: string; + } + | { + type: FilterType.CreatedByCurrentUser; + email?: string; + } + ); + // Warning: (ae-missing-release-tag) "EntityAzurePipelinesContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -48,10 +154,67 @@ export const EntityAzurePullRequestsContent: ({ defaultLimit?: number | undefined; }) => JSX.Element; +// Warning: (ae-missing-release-tag) "Filter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type Filter = + | AssignedToUserFilter + | CreatedByUserFilter + | AssignedToTeamFilter + | CreatedByTeamFilter + | AssignedToTeamsFilter + | CreatedByTeamsFilter + | AllFilter; + +// Warning: (ae-missing-release-tag) "FilterType" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export enum FilterType { + // (undocumented) + All = 'All', + // (undocumented) + AssignedToCurrentUser = 'AssignedToCurrentUser', + // (undocumented) + AssignedToCurrentUsersTeams = 'AssignedToCurrentUsersTeams', + // (undocumented) + AssignedToTeam = 'AssignedToTeam', + // (undocumented) + AssignedToTeams = 'AssignedToTeams', + // (undocumented) + AssignedToUser = 'AssignedToUser', + // (undocumented) + CreatedByCurrentUser = 'CreatedByCurrentUser', + // (undocumented) + CreatedByCurrentUsersTeams = 'CreatedByCurrentUsersTeams', + // (undocumented) + CreatedByTeam = 'CreatedByTeam', + // (undocumented) + CreatedByTeams = 'CreatedByTeams', + // (undocumented) + CreatedByUser = 'CreatedByUser', +} + // Warning: (ae-missing-release-tag) "isAzureDevOpsAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const isAzureDevOpsAvailable: (entity: Entity) => boolean; +// Warning: (ae-missing-release-tag) "PullRequestColumnConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface PullRequestColumnConfig { + // (undocumented) + filters: Filter[]; + // (undocumented) + simplified?: boolean; + // (undocumented) + title: string; +} + +// Warning: (ae-missing-release-tag) "PullRequestFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index a37259739f..5d048c6471 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.1.6", + "version": "0.1.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,12 +27,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/catalog-model": "^0.9.8", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.4", - "@backstage/plugin-azure-devops-common": "^0.1.2", - "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-azure-devops-common": "^0.1.3", + "@backstage/plugin-catalog-react": "^0.6.7", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,8 +46,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx index 9f21c4b81b..158c4bca5b 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx +++ b/plugins/azure-devops/src/components/PullRequestsPage/PullRequestsPage.tsx @@ -21,56 +21,17 @@ import { Progress, ResponseErrorPanel, } from '@backstage/core-components'; -import { PullRequestGroup, PullRequestGroupConfig } from './lib/types'; -import React, { useEffect, useState } from 'react'; -import { getCreatedByUserFilter, getPullRequestGroups } from './lib/utils'; -import { useDashboardPullRequests, useUserEmail } from '../../hooks'; +import { PullRequestColumnConfig, PullRequestGroup } from './lib/types'; +import React, { useState } from 'react'; +import { getPullRequestGroupConfigs, getPullRequestGroups } from './lib/utils'; -import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { FilterType } from './lib/filters'; import { PullRequestGrid } from './lib/PullRequestGrid'; - -function usePullRequestGroupConfigs( - userEmail: string | undefined, -): PullRequestGroupConfig[] { - const [pullRequestGroupConfigs, setPullRequestGroupConfigs] = useState< - PullRequestGroupConfig[] - >([]); - - useEffect(() => { - const prGroupConfigs: PullRequestGroupConfig[] = [ - { title: 'Created by me', filter: getCreatedByUserFilter(userEmail) }, - { title: 'Other PRs', filter: _ => true, simplified: false }, - ]; - - setPullRequestGroupConfigs(prGroupConfigs); - }, [userEmail]); - - return pullRequestGroupConfigs; -} - -function usePullRequestGroups( - pullRequests: DashboardPullRequest[] | undefined, - pullRequestGroupConfigs: PullRequestGroupConfig[], -): PullRequestGroup[] { - const [pullRequestGroups, setPullRequestGroups] = useState< - PullRequestGroup[] - >([]); - - useEffect(() => { - if (pullRequests) { - const groups = getPullRequestGroups( - pullRequests, - pullRequestGroupConfigs, - ); - setPullRequestGroups(groups); - } - }, [pullRequests, pullRequestGroupConfigs]); - - return pullRequestGroups; -} +import { useDashboardPullRequests } from '../../hooks'; +import { useFilterProcessor } from './lib/hooks'; type PullRequestsPageContentProps = { - pullRequestGroups: PullRequestGroup[]; + pullRequestGroups: PullRequestGroup[] | undefined; loading: boolean; error?: Error; }; @@ -80,7 +41,7 @@ const PullRequestsPageContent = ({ loading, error, }: PullRequestsPageContentProps) => { - if (loading && pullRequestGroups.length <= 0) { + if (loading && (!pullRequestGroups || pullRequestGroups.length <= 0)) { return ; } @@ -88,25 +49,50 @@ const PullRequestsPageContent = ({ return ; } - return ; + return ; }; +const DEFAULT_COLUMN_CONFIGS: PullRequestColumnConfig[] = [ + { + title: 'Created by me', + filters: [{ type: FilterType.CreatedByCurrentUser }], + simplified: false, + }, + { + title: 'Other PRs', + filters: [{ type: FilterType.All }], + simplified: true, + }, +]; + type PullRequestsPageProps = { projectName?: string; pollingInterval?: number; + defaultColumnConfigs?: PullRequestColumnConfig[]; }; export const PullRequestsPage = ({ projectName, pollingInterval, + defaultColumnConfigs, }: PullRequestsPageProps) => { const { pullRequests, loading, error } = useDashboardPullRequests( projectName, pollingInterval, ); - const userEmail = useUserEmail(); - const pullRequestGroupConfigs = usePullRequestGroupConfigs(userEmail); - const pullRequestGroups = usePullRequestGroups( + + const [columnConfigs] = useState( + defaultColumnConfigs ?? DEFAULT_COLUMN_CONFIGS, + ); + + const filterProcessor = useFilterProcessor(); + + const pullRequestGroupConfigs = getPullRequestGroupConfigs( + columnConfigs, + filterProcessor, + ); + + const pullRequestGroups = getPullRequestGroups( pullRequests, pullRequestGroupConfigs, ); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/index.ts index e8b6cfa6bb..4061c72a8e 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/index.ts +++ b/plugins/azure-devops/src/components/PullRequestsPage/index.ts @@ -15,3 +15,17 @@ */ export { PullRequestsPage } from './PullRequestsPage'; +export type { PullRequestColumnConfig } from './lib/types'; +export { FilterType } from './lib/filters'; +export type { + BaseFilter, + Filter, + PullRequestFilter, + AssignedToUserFilter, + CreatedByUserFilter, + AssignedToTeamFilter, + CreatedByTeamFilter, + AssignedToTeamsFilter, + CreatedByTeamsFilter, + AllFilter, +} from './lib/filters'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/allFilter.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/allFilter.ts new file mode 100644 index 0000000000..90ec822ef6 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/allFilter.ts @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BaseFilter, FilterType, PullRequestFilter } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; + +export type AllFilter = BaseFilter & { + type: FilterType.All; +}; + +export function createAllFilter(): PullRequestFilter { + return (_pullRequest: DashboardPullRequest): boolean => true; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToTeamFilter.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToTeamFilter.ts new file mode 100644 index 0000000000..8e816db2c3 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToTeamFilter.ts @@ -0,0 +1,39 @@ +/* + * 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 { BaseFilter, FilterType, PullRequestFilter } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { stringArrayHas } from '../../../../utils'; + +export type AssignedToTeamFilter = BaseFilter & { + type: FilterType.AssignedToTeam; + teamId: string; +}; + +export function createAssignedToTeamFilter( + filter: AssignedToTeamFilter, +): PullRequestFilter { + return (pullRequest: DashboardPullRequest): boolean => { + const reviewerIds = pullRequest.reviewers?.map(reviewer => reviewer.id); + + if (!reviewerIds) { + return false; + } + + return stringArrayHas(reviewerIds, filter.teamId, true); + }; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToTeamsFilter.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToTeamsFilter.ts new file mode 100644 index 0000000000..ce5e5bcc11 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToTeamsFilter.ts @@ -0,0 +1,51 @@ +/* + * 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 { BaseFilter, FilterType, PullRequestFilter } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { createAssignedToTeamFilter } from './assignedToTeamFilter'; + +export type AssignedToTeamsFilter = BaseFilter & + ( + | { + type: FilterType.AssignedToTeams; + teamIds: string[]; + } + | { + type: FilterType.AssignedToCurrentUsersTeams; + teamIds?: string[]; + } + ); + +export function createAssignedToTeamsFilter( + filter: AssignedToTeamsFilter, +): PullRequestFilter { + const teamIds = filter.teamIds; + + return (pullRequest: DashboardPullRequest): boolean => { + if (!teamIds) { + return false; + } + + return teamIds.some(teamId => { + return createAssignedToTeamFilter({ + type: FilterType.AssignedToTeam, + teamId, + })(pullRequest); + }); + }; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToUserFilter.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToUserFilter.ts new file mode 100644 index 0000000000..95ee3d5a59 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/assignedToUserFilter.ts @@ -0,0 +1,50 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BaseFilter, FilterType, PullRequestFilter } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { stringArrayHas } from '../../../../utils'; + +export type AssignedToUserFilter = BaseFilter & + ( + | { + type: FilterType.AssignedToUser; + email: string; + } + | { + type: FilterType.AssignedToCurrentUser; + email?: string; + } + ); + +export function createAssignedToUserFilter( + filter: AssignedToUserFilter, +): PullRequestFilter { + const email = filter.email; + + return (pullRequest: DashboardPullRequest): boolean => { + const uniqueNames = pullRequest.reviewers?.map( + reviewer => reviewer.uniqueName, + ); + + if (!email || !uniqueNames) { + return false; + } + + return stringArrayHas(uniqueNames, email, true); + }; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createFilter.test.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createFilter.test.ts new file mode 100644 index 0000000000..b0ac4d402b --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createFilter.test.ts @@ -0,0 +1,150 @@ +/* + * 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 { Filter, FilterType } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { createFilter } from './createFilter'; + +describe('createFilter', () => { + const pullRequest = { + createdBy: { + uniqueName: 'user1@backstage.com', + teamIds: ['team1Id', 'team2Id'], + }, + reviewers: [ + { uniqueName: 'user2@backstage.com' }, + { id: 'team2Id' }, + { id: 'team3Id' }, + ], + } as DashboardPullRequest; + + const testCases: Array<{ filter: Filter; result: boolean }> = [ + { + filter: { + type: FilterType.AssignedToUser, + email: 'user2@backstage.com', + }, + result: true, + }, + { + filter: { + type: FilterType.AssignedToUser, + email: 'random-user@backstage.com', + }, + result: false, + }, + { + filter: { + type: FilterType.CreatedByUser, + email: 'user1@backstage.com', + }, + result: true, + }, + { + filter: { + type: FilterType.CreatedByUser, + email: 'random-user@backstage.com', + }, + result: false, + }, + { + filter: { + type: FilterType.AssignedToTeam, + teamId: 'team2Id', + }, + result: true, + }, + { + filter: { + type: FilterType.AssignedToTeam, + teamId: 'randomTeamId', + }, + result: false, + }, + { + filter: { + type: FilterType.CreatedByTeam, + teamId: 'team1Id', + }, + result: true, + }, + { + filter: { + type: FilterType.CreatedByTeam, + teamId: 'randomTeamId', + }, + result: false, + }, + { + filter: { + type: FilterType.AssignedToTeams, + teamIds: ['team2Id', 'randomTeamId'], + }, + result: true, + }, + { + filter: { + type: FilterType.AssignedToTeams, + teamIds: ['team2Id', 'team3Id'], + }, + result: true, + }, + { + filter: { + type: FilterType.AssignedToTeams, + teamIds: ['randomTeam1Id', 'randomTeam2Id'], + }, + result: false, + }, + { + filter: { + type: FilterType.CreatedByTeams, + teamIds: ['team1Id', 'randomTeamId'], + }, + result: true, + }, + { + filter: { + type: FilterType.CreatedByTeams, + teamIds: ['team1Id', 'team2Id'], + }, + result: true, + }, + { + filter: { + type: FilterType.CreatedByTeams, + teamIds: ['randomTeam1Id', 'randomTeam2Id'], + }, + result: false, + }, + { + filter: { + type: FilterType.All, + }, + result: true, + }, + ]; + + testCases.forEach(({ filter, result }) => { + it(`should return ${String(result)} when pull request ${ + result ? 'is' : 'is not' + } ${filter.type}`, () => { + const pullRequestFilter = createFilter(filter); + expect(pullRequestFilter(pullRequest)).toBe(result); + }); + }); +}); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createFilter.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createFilter.ts new file mode 100644 index 0000000000..492d0539c5 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createFilter.ts @@ -0,0 +1,71 @@ +/* + * 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 { Filter, FilterType, PullRequestFilter } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { createAllFilter } from './allFilter'; +import { createAssignedToTeamFilter } from './assignedToTeamFilter'; +import { createAssignedToTeamsFilter } from './assignedToTeamsFilter'; +import { createAssignedToUserFilter } from './assignedToUserFilter'; +import { createCreatedByTeamFilter } from './createdByTeamFilter'; +import { createCreatedByTeamsFilter } from './createdByTeamsFilter'; +import { createCreatedByUserFilter } from './createdByUserFilter'; + +export function createFilter(filters: Filter | Filter[]): PullRequestFilter { + const mapFilter = (filter: Filter): PullRequestFilter => { + switch (filter.type) { + case FilterType.AssignedToUser: + case FilterType.AssignedToCurrentUser: + return createAssignedToUserFilter(filter); + + case FilterType.CreatedByUser: + case FilterType.CreatedByCurrentUser: + return createCreatedByUserFilter(filter); + + case FilterType.AssignedToTeam: + return createAssignedToTeamFilter(filter); + + case FilterType.CreatedByTeam: + return createCreatedByTeamFilter(filter); + + case FilterType.AssignedToTeams: + case FilterType.AssignedToCurrentUsersTeams: + return createAssignedToTeamsFilter(filter); + + case FilterType.CreatedByTeams: + case FilterType.CreatedByCurrentUsersTeams: + return createCreatedByTeamsFilter(filter); + + case FilterType.All: + return createAllFilter(); + + default: + return _ => false; + } + }; + + if (Array.isArray(filters)) { + if (filters.length === 1) { + return mapFilter(filters[0]); + } + + return (pullRequest: DashboardPullRequest): boolean => + filters.every(filter => mapFilter(filter)(pullRequest)); + } + + return mapFilter(filters); +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByTeamFilter.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByTeamFilter.ts new file mode 100644 index 0000000000..70a388b540 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByTeamFilter.ts @@ -0,0 +1,42 @@ +/* + * 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 { BaseFilter, FilterType, PullRequestFilter } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { stringArrayHas } from '../../../../utils'; + +export type CreatedByTeamFilter = BaseFilter & + ({ + type: FilterType.CreatedByTeam; + } & ({ teamId: string } | { teamName: string })); + +export function createCreatedByTeamFilter( + filter: CreatedByTeamFilter, +): PullRequestFilter { + return (pullRequest: DashboardPullRequest): boolean => { + const [createdByTeams, team] = + 'teamId' in filter + ? [pullRequest.createdBy?.teamIds, filter.teamId] + : [pullRequest.createdBy?.teamNames, filter.teamName]; + + if (!createdByTeams) { + return false; + } + + return stringArrayHas(createdByTeams, team, true); + }; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByTeamsFilter.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByTeamsFilter.ts new file mode 100644 index 0000000000..34ed81db5d --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByTeamsFilter.ts @@ -0,0 +1,65 @@ +/* + * 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 { BaseFilter, FilterType, PullRequestFilter } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { createCreatedByTeamFilter } from './createdByTeamFilter'; + +export type CreatedByTeamsFilter = BaseFilter & + ( + | ({ + type: FilterType.CreatedByTeams; + } & ({ teamIds: string[] } | { teamNames: string[] })) + | { + type: FilterType.CreatedByCurrentUsersTeams; + teamIds?: string[]; + } + ); + +export function createCreatedByTeamsFilter( + filter: CreatedByTeamsFilter, +): PullRequestFilter { + return (pullRequest: DashboardPullRequest): boolean => { + if ('teamNames' in filter) { + const teamNames = filter.teamNames; + + if (!teamNames) { + return false; + } + + return teamNames.some(teamName => { + return createCreatedByTeamFilter({ + type: FilterType.CreatedByTeam, + teamName, + })(pullRequest); + }); + } + + const teamIds = filter.teamIds; + + if (!teamIds) { + return false; + } + + return teamIds.some(teamId => { + return createCreatedByTeamFilter({ + type: FilterType.CreatedByTeam, + teamId, + })(pullRequest); + }); + }; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByUserFilter.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByUserFilter.ts new file mode 100644 index 0000000000..69d4f23db5 --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/createdByUserFilter.ts @@ -0,0 +1,48 @@ +/* + * 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 { BaseFilter, FilterType, PullRequestFilter } from './types'; + +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; +import { equalsIgnoreCase } from '../../../../utils'; + +export type CreatedByUserFilter = BaseFilter & + ( + | { + type: FilterType.CreatedByUser; + email: string; + } + | { + type: FilterType.CreatedByCurrentUser; + email?: string; + } + ); + +export function createCreatedByUserFilter( + filter: CreatedByUserFilter, +): PullRequestFilter { + const email = filter.email; + + return (pullRequest: DashboardPullRequest): boolean => { + const uniqueName = pullRequest.createdBy?.uniqueName; + + if (!email || !uniqueName) { + return false; + } + + return equalsIgnoreCase(email, uniqueName); + }; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/index.ts new file mode 100644 index 0000000000..df165dc9ca --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/index.ts @@ -0,0 +1,30 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { createFilter } from './createFilter'; +export { FilterTypes, FilterType } from './types'; +export type { + BaseFilter, + Filter, + PullRequestFilter, + AssignedToUserFilter, + CreatedByUserFilter, + AssignedToTeamFilter, + CreatedByTeamFilter, + AssignedToTeamsFilter, + CreatedByTeamsFilter, + AllFilter, +} from './types'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/types.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/types.ts new file mode 100644 index 0000000000..bc4643d51b --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/filters/types.ts @@ -0,0 +1,83 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { AllFilter } from './allFilter'; +import { AssignedToTeamFilter } from './assignedToTeamFilter'; +import { AssignedToTeamsFilter } from './assignedToTeamsFilter'; +import { AssignedToUserFilter } from './assignedToUserFilter'; +import { CreatedByTeamFilter } from './createdByTeamFilter'; +import { CreatedByTeamsFilter } from './createdByTeamsFilter'; +import { CreatedByUserFilter } from './createdByUserFilter'; +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; + +export enum FilterType { + All = 'All', + + // Assigned To + AssignedToUser = 'AssignedToUser', + AssignedToCurrentUser = 'AssignedToCurrentUser', + AssignedToTeam = 'AssignedToTeam', + AssignedToTeams = 'AssignedToTeams', + AssignedToCurrentUsersTeams = 'AssignedToCurrentUsersTeams', + + // Created By + CreatedByUser = 'CreatedByUser', + CreatedByCurrentUser = 'CreatedByCurrentUser', + CreatedByTeam = 'CreatedByTeam', + CreatedByTeams = 'CreatedByTeams', + CreatedByCurrentUsersTeams = 'CreatedByCurrentUsersTeams', +} + +export const FilterTypes = [ + FilterType.All, + + FilterType.AssignedToUser, + FilterType.AssignedToCurrentUser, + FilterType.AssignedToTeam, + FilterType.AssignedToTeams, + FilterType.AssignedToCurrentUsersTeams, + + FilterType.CreatedByUser, + FilterType.CreatedByCurrentUser, + FilterType.CreatedByTeam, + FilterType.CreatedByTeams, + FilterType.CreatedByCurrentUsersTeams, +] as const; + +export type BaseFilter = { + type: FilterType; +}; + +export type Filter = + | AssignedToUserFilter + | CreatedByUserFilter + | AssignedToTeamFilter + | CreatedByTeamFilter + | AssignedToTeamsFilter + | CreatedByTeamsFilter + | AllFilter; + +export type { + AssignedToUserFilter, + CreatedByUserFilter, + AssignedToTeamFilter, + CreatedByTeamFilter, + AssignedToTeamsFilter, + CreatedByTeamsFilter, + AllFilter, +}; + +export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/index.ts new file mode 100644 index 0000000000..2e87dfea0c --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './useFilterProcessor'; diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/useFilterProcessor.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/useFilterProcessor.ts new file mode 100644 index 0000000000..546b23430d --- /dev/null +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/useFilterProcessor.ts @@ -0,0 +1,39 @@ +/* + * 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 { Filter, FilterType } from '../filters'; + +import { useUserEmail } from '../../../../hooks'; + +export function useFilterProcessor(): (filters: Filter[]) => Filter[] { + const userEmail = useUserEmail(); + + return (filters: Filter[]): Filter[] => { + for (const filter of filters) { + switch (filter.type) { + case FilterType.AssignedToCurrentUser: + case FilterType.CreatedByCurrentUser: + filter.email = userEmail; + break; + + default: + break; + } + } + + return filters; + }; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/types.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/types.ts index 2d936f7b64..74c8ebe605 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/lib/types.ts +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/types.ts @@ -14,23 +14,24 @@ * limitations under the License. */ -import { - DashboardPullRequest, - Team, -} from '@backstage/plugin-azure-devops-common'; +import { Filter, PullRequestFilter } from './filters'; -export interface PullRequestGroup { +import { DashboardPullRequest } from '@backstage/plugin-azure-devops-common'; + +export interface PullRequestColumnConfig { title: string; - pullRequests: DashboardPullRequest[]; + filters: Filter[]; simplified?: boolean; } -export type PullRequestFilter = (pullRequest: DashboardPullRequest) => boolean; - -export type TeamFilter = (team: Team) => boolean; - export interface PullRequestGroupConfig { title: string; filter: PullRequestFilter; simplified?: boolean; } + +export interface PullRequestGroup { + title: string; + pullRequests: DashboardPullRequest[]; + simplified?: boolean; +} diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.test.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.test.ts index da27b2e06e..d128cbd1db 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.test.ts +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.test.ts @@ -19,33 +19,7 @@ import { PullRequestVoteStatus, Reviewer, } from '@backstage/plugin-azure-devops-common'; -import { - arrayExtract, - getCreatedByUserFilter, - getPullRequestGroups, - reviewerFilter, -} from './utils'; - -describe('getCreatedByUserFilter', () => { - it('should filter if pull request is created by user', () => { - const userEmail = 'user1@backstage.com'; - const pr = { - createdBy: { uniqueName: userEmail }, - } as DashboardPullRequest; - const result = getCreatedByUserFilter(userEmail)(pr); - expect(result).toBe(true); - }); - - it('should not filter if pull request is not created by user', () => { - const userEmail1 = 'user1@backstage.com'; - const userEmail2 = 'user2@backstage.com'; - const pr = { - createdBy: { uniqueName: userEmail1 }, - } as DashboardPullRequest; - const result = getCreatedByUserFilter(userEmail2)(pr); - expect(result).toBe(false); - }); -}); +import { arrayExtract, getPullRequestGroups, reviewerFilter } from './utils'; describe('reviewerFilter', () => { it('should return false if reviewer has no vote and is not required', () => { @@ -128,11 +102,15 @@ describe('getPullRequestGroups', () => { ]; const configs = [ - { title: 'Created by me', filter: getCreatedByUserFilter(userEmail) }, + { + title: 'Created by me', + filter: (pullRequest: DashboardPullRequest): boolean => + pullRequest.createdBy?.uniqueName === userEmail, + }, { title: 'Other PRs', filter: (_: unknown) => true, simplified: true }, ]; - const result = getPullRequestGroups(pullRequests, configs); + const result = getPullRequestGroups(pullRequests, configs) ?? []; expect(result.length).toBe(2); diff --git a/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.ts index c9cf866010..c4fcdc9f7e 100644 --- a/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.ts +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/utils.ts @@ -19,25 +19,13 @@ import { PullRequestVoteStatus, Reviewer, } from '@backstage/plugin-azure-devops-common'; +import { Filter, createFilter } from './filters'; import { - PullRequestFilter, + PullRequestColumnConfig, PullRequestGroup, PullRequestGroupConfig, } from './types'; -/** - * Creates a filter that matches pull requests created by `userEmail`. - * @param userEmail an email to filter by. - * @returns a filter for pull requests created by `userEmail`. - */ -export function getCreatedByUserFilter( - userEmail: string | undefined, -): PullRequestFilter { - return (pullRequest: DashboardPullRequest): boolean => - pullRequest.createdBy?.uniqueName?.toLocaleLowerCase() === - userEmail?.toLocaleLowerCase(); -} - /** * Filters a reviewer based on vote status and if the reviewer is required. * @param reviewer a reviewer to filter. @@ -97,9 +85,13 @@ export function arrayExtract(arr: T[], filter: (value: T) => unknown): T[] { * @returns a list of pull request groups. */ export function getPullRequestGroups( - pullRequests: DashboardPullRequest[], + pullRequests: DashboardPullRequest[] | undefined, configs: PullRequestGroupConfig[], -): PullRequestGroup[] { +): PullRequestGroup[] | undefined { + if (!pullRequests) { + return undefined; + } + const remainingPullRequests: DashboardPullRequest[] = [...pullRequests]; const pullRequestGroups: PullRequestGroup[] = []; @@ -115,3 +107,17 @@ export function getPullRequestGroups( return pullRequestGroups; } + +export function getPullRequestGroupConfigs( + columnConfigs: PullRequestColumnConfig[], + filterProcessor: (filters: Filter[]) => Filter[], +): PullRequestGroupConfig[] { + return columnConfigs.map(columnConfig => { + const filters = filterProcessor(columnConfig.filters); + return { + title: columnConfig.title, + filter: createFilter(filters), + simplified: columnConfig.simplified, + }; + }); +} diff --git a/plugins/azure-devops/src/index.ts b/plugins/azure-devops/src/index.ts index c9b804bc8d..8289a93a5c 100644 --- a/plugins/azure-devops/src/index.ts +++ b/plugins/azure-devops/src/index.ts @@ -23,3 +23,18 @@ export { } from './plugin'; export { AzurePullRequestsIcon } from './components/AzurePullRequestsIcon'; + +export { FilterType } from './components/PullRequestsPage'; +export type { + PullRequestColumnConfig, + BaseFilter, + Filter, + PullRequestFilter, + AssignedToUserFilter, + CreatedByUserFilter, + AssignedToTeamFilter, + CreatedByTeamFilter, + AssignedToTeamsFilter, + CreatedByTeamsFilter, + AllFilter, +} from './components/PullRequestsPage'; diff --git a/plugins/azure-devops/src/utils/arrayHas.ts b/plugins/azure-devops/src/utils/arrayHas.ts new file mode 100644 index 0000000000..8dac8b8ec9 --- /dev/null +++ b/plugins/azure-devops/src/utils/arrayHas.ts @@ -0,0 +1,34 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function arrayHas(arr: T[], value: T): boolean { + return new Set(arr).has(value); +} + +export function stringArrayHas( + arr: Array, + value: string | undefined, + ignoreCase: boolean = false, +): boolean { + if (ignoreCase) { + return arrayHas( + arr.map(a => a?.toLocaleLowerCase('en-US')), + value?.toLocaleLowerCase('en-US'), + ); + } + + return arrayHas(arr, value); +} diff --git a/plugins/azure-devops/src/utils/equalsIgnoreCase.ts b/plugins/azure-devops/src/utils/equalsIgnoreCase.ts new file mode 100644 index 0000000000..12b058e6ae --- /dev/null +++ b/plugins/azure-devops/src/utils/equalsIgnoreCase.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export function equalsIgnoreCase(str1: string, str2: string): boolean { + return str1.toLocaleLowerCase('en-US') === str2.toLocaleLowerCase('en-US'); +} diff --git a/plugins/azure-devops/src/utils/index.ts b/plugins/azure-devops/src/utils/index.ts new file mode 100644 index 0000000000..f2214e8106 --- /dev/null +++ b/plugins/azure-devops/src/utils/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './arrayHas'; +export * from './equalsIgnoreCase'; +export * from './getDurationFromDates'; diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 441f72c550..9d35a3b9e2 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -28,8 +28,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.5", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", @@ -43,8 +43,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 3db79b9cdf..88da003778 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -23,8 +23,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/cli": "^0.10.1", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog": "^0.7.4", "@backstage/plugin-catalog-react": "^0.6.5", "@date-io/luxon": "1.x", @@ -42,7 +42,7 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", "@backstage/dev-utils": "^0.2.14", "@testing-library/jest-dom": "^5.10.1", "cross-fetch": "^3.0.6" diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 35a0a9e079..c4a02c7403 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -40,8 +40,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index dc7fc50f76..ce502f8f46 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-backend +## 0.19.2 + +### Patch Changes + +- 3368f27aef: Fixed the handling of optional locations so that the catalog no longer logs `NotFoundError`s for missing optional locations. +- Updated dependencies + - @backstage/backend-common@0.9.14 + - @backstage/catalog-model@0.9.8 + ## 0.19.1 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 6cfb73a3d1..d49723b7d2 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend", "description": "The Backstage backend plugin that provides the Backstage catalog", - "version": "0.19.1", + "version": "0.19.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.9.13", + "@backstage/backend-common": "^0.9.14", "@backstage/catalog-client": "^0.5.2", - "@backstage/catalog-model": "^0.9.7", + "@backstage/catalog-model": "^0.9.8", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.10", @@ -63,7 +63,7 @@ }, "devDependencies": { "@backstage/backend-test-utils": "^0.1.10", - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", "@backstage/test-utils": "^0.1.24", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts index ab053e8748..463d982ca2 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.test.ts @@ -165,6 +165,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { type: 'url', target: 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml', + presence: 'optional', }, optional: true, }); @@ -174,6 +175,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { type: 'url', target: 'https://dev.azure.com/shopify/engineering/_git/ios-app?path=/src/catalog-info.yaml', + presence: 'optional', }, optional: true, }); @@ -211,6 +213,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { type: 'url', target: 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/catalog-info.yaml', + presence: 'optional', }, optional: true, }); @@ -249,6 +252,7 @@ describe('AzureDevOpsDiscoveryProcessor', () => { type: 'url', target: 'https://dev.azure.com/shopify/engineering/_git/backstage?path=/src/main/catalog.yaml', + presence: 'optional', }, optional: true, }); diff --git a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts index 58ee2155e9..aeac64f54d 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AzureDevOpsDiscoveryProcessor.ts @@ -95,6 +95,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { { type: 'url', target: `${baseUrl}/${org}/${project}/_git/${file.repository.name}?path=${file.path}`, + presence: 'optional', }, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting diff --git a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts index 2a63605b0d..3f8d0ba0fb 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BitbucketDiscoveryProcessor.test.ts @@ -208,6 +208,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.mycompany.com/projects/backstage/repos/backstage/browse/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -217,6 +218,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.mycompany.com/projects/demo/repos/demo/browse/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -242,6 +244,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -263,6 +266,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.mycompany.com/projects/backstage/repos/test/browse/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -291,6 +295,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.mycompany.com/projects/backstage/repos/techdocs-cli/browse/catalog-info.yaml', + presence: 'optional', }, optional: true, }); @@ -334,6 +339,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', + presence: 'optional', }, optional: true, }); @@ -343,6 +349,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-two/src/master/catalog-info.yaml', + presence: 'optional', }, optional: true, }); @@ -370,6 +377,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-one/src/master/my/nested/path/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -379,6 +387,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-two/src/master/my/nested/path/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -406,6 +415,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -415,6 +425,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-two/src/master/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -441,6 +452,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -468,6 +480,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-three/src/master/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -493,6 +506,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', + presence: 'optional', }, optional: true, }); @@ -528,6 +542,7 @@ describe('BitbucketDiscoveryProcessor', () => { type: 'url', target: 'https://bitbucket.org/myworkspace/repository-one/src/master/catalog-info.yaml', + presence: 'optional', }, optional: true, }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts index a0e44d7648..311de05673 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.test.ts @@ -201,6 +201,7 @@ describe('GitlabDiscoveryProcessor', () => { location: { type: 'url', target: 'https://gitlab.fake/1/-/blob/main/catalog-info.yaml', + presence: 'optional', }, optional: true, }, @@ -209,6 +210,7 @@ describe('GitlabDiscoveryProcessor', () => { location: { type: 'url', target: 'https://gitlab.fake/2/-/blob/master/catalog-info.yaml', + presence: 'optional', }, optional: true, }, @@ -246,6 +248,7 @@ describe('GitlabDiscoveryProcessor', () => { location: { type: 'url', target: 'https://gitlab.fake/1/-/blob/master/catalog-info.yaml', + presence: 'optional', }, optional: true, }, diff --git a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts index 445f02829e..70ccd44793 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GitLabDiscoveryProcessor.ts @@ -113,6 +113,7 @@ export class GitLabDiscoveryProcessor implements CatalogProcessor { // The alternative is using the `buildRawUrl` function, which does not support subgroups, so providing a raw // URL here won't work either. target: `${project.web_url}/-/blob/${project_branch}/${catalogPath}`, + presence: 'optional', }, true, ), diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts index 12d9e66659..59c25bb051 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.test.ts @@ -157,6 +157,7 @@ describe('GithubDiscoveryProcessor', () => { type: 'url', target: 'https://github.com/backstage/backstage/blob/master/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -165,6 +166,7 @@ describe('GithubDiscoveryProcessor', () => { location: { type: 'url', target: 'https://github.com/backstage/demo/blob/master/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -197,6 +199,7 @@ describe('GithubDiscoveryProcessor', () => { type: 'url', target: 'https://github.com/backstage/tech-docs/blob/main/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -251,6 +254,7 @@ describe('GithubDiscoveryProcessor', () => { type: 'url', target: 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + presence: 'optional', }, optional: true, }); @@ -306,6 +310,7 @@ describe('GithubDiscoveryProcessor', () => { type: 'url', target: 'https://github.com/backstage/techdocs-cli/blob/master/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -315,6 +320,7 @@ describe('GithubDiscoveryProcessor', () => { type: 'url', target: 'https://github.com/backstage/techdocs-container/blob/master/catalog.yaml', + presence: 'optional', }, optional: true, }); @@ -370,6 +376,7 @@ describe('GithubDiscoveryProcessor', () => { location: { type: 'url', target: 'https://github.com/backstage/test/blob/master/catalog.yaml', + presence: 'optional', }, optional: true, }); diff --git a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts index b104d87542..9bd6cc775e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/GithubDiscoveryProcessor.ts @@ -125,6 +125,7 @@ export class GithubDiscoveryProcessor implements CatalogProcessor { { type: 'url', target: `${repository.url}${path}`, + presence: 'optional', }, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts index bc6c5a540f..397d259bae 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.test.ts @@ -29,6 +29,7 @@ describe('BitbucketRepositoryParser', () => { { type: 'url', target: `${browseUrl}${path}`, + presence: 'optional', }, true, ), diff --git a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts index 27f07d58a2..34a9284b3e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts +++ b/plugins/catalog-backend/src/ingestion/processors/bitbucket/BitbucketRepositoryParser.ts @@ -30,6 +30,7 @@ export const defaultRepositoryParser: BitbucketRepositoryParser = { type: 'url', target: target, + presence: 'optional', }, // Not all locations may actually exist, since the user defined them as a wildcard pattern. // Thus, we emit them as optional and let the downstream processor find them while not outputting diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts index b42c8839cf..756b6a3639 100644 --- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts +++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingOrchestrator.ts @@ -284,7 +284,7 @@ export class DefaultCatalogProcessingOrchestrator entity: LocationEntity, context: Context, ): Promise { - const { type = context.location.type } = entity.spec; + const { type = context.location.type, presence = 'required' } = entity.spec; const targets = new Array(); if (entity.spec.target) { targets.push(entity.spec.target); @@ -318,9 +318,9 @@ export class DefaultCatalogProcessingOrchestrator { type, target, - presence: 'required', + presence, }, - false, + presence === 'optional', context.collector.onEmit, this.options.parser, context.cache.forProcessor(processor, target), diff --git a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts index 436038db66..b0c46b4b9f 100644 --- a/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts +++ b/plugins/catalog-backend/src/processing/ProcessorOutputCollector.ts @@ -105,8 +105,9 @@ export class ProcessorOutputCollector { this.deferredEntities.push({ entity, locationKey: location }); } else if (i.type === 'location') { + const presence = i.optional ? 'optional' : 'required'; const entity = locationSpecToLocationEntity( - i.location, + { presence, ...i.location }, this.parentEntity, ); const locationKey = getEntityLocationRef(entity); diff --git a/plugins/catalog-backend/src/util/conversion.ts b/plugins/catalog-backend/src/util/conversion.ts index a834f81255..559a472c25 100644 --- a/plugins/catalog-backend/src/util/conversion.ts +++ b/plugins/catalog-backend/src/util/conversion.ts @@ -81,6 +81,7 @@ export function locationSpecToLocationEntity( spec: { type: location.type, target: location.target, + presence: location.presence, }, }; diff --git a/plugins/catalog-common/.eslintrc.js b/plugins/catalog-common/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/catalog-common/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/catalog-common/README.md b/plugins/catalog-common/README.md new file mode 100644 index 0000000000..2566a737d8 --- /dev/null +++ b/plugins/catalog-common/README.md @@ -0,0 +1,9 @@ +# Catalog Common + +Shared isomorphic code for the catalog plugin. + +## Links + +- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog) +- [Backend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/catalog-backend) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/catalog-common/api-report.md b/plugins/catalog-common/api-report.md new file mode 100644 index 0000000000..8e03fc16b5 --- /dev/null +++ b/plugins/catalog-common/api-report.md @@ -0,0 +1,31 @@ +## API Report File for "@backstage/plugin-catalog-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { Permission } from '@backstage/plugin-permission-common'; + +// @public +export const catalogEntityDeletePermission: Permission; + +// @public +export const catalogEntityReadPermission: Permission; + +// @public +export const catalogEntityRefreshPermission: Permission; + +// @public +export const catalogLocationCreatePermission: Permission; + +// @public +export const catalogLocationDeletePermission: Permission; + +// @public +export const catalogLocationReadPermission: Permission; + +// @public (undocumented) +export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; + +// @public (undocumented) +export const RESOURCE_TYPE_CATALOG_LOCATION = 'catalog-location'; +``` diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json new file mode 100644 index 0000000000..439a76fe58 --- /dev/null +++ b/plugins/catalog-common/package.json @@ -0,0 +1,41 @@ +{ + "name": "@backstage/plugin-catalog-common", + "description": "Common functionalities for the catalog plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/catalog-common" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli build", + "lint": "backstage-cli lint", + "test": "backstage-cli test --passWithNoTests", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/plugin-permission-common": "^0.2.0" + }, + "devDependencies": { + "@backstage/cli": "^0.10.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/catalog-common/src/index.ts b/plugins/catalog-common/src/index.ts new file mode 100644 index 0000000000..a38a5ed93b --- /dev/null +++ b/plugins/catalog-common/src/index.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Provides shared objects useful for interacting with the catalog and its + * entities, such as catalog permissions. + * + * @packageDocumentation + */ + +export { + RESOURCE_TYPE_CATALOG_ENTITY, + RESOURCE_TYPE_CATALOG_LOCATION, + catalogEntityReadPermission, + catalogEntityDeletePermission, + catalogEntityRefreshPermission, + catalogLocationReadPermission, + catalogLocationCreatePermission, + catalogLocationDeletePermission, +} from './permissions'; diff --git a/plugins/catalog-common/src/permissions.ts b/plugins/catalog-common/src/permissions.ts new file mode 100644 index 0000000000..eb7b01c31e --- /dev/null +++ b/plugins/catalog-common/src/permissions.ts @@ -0,0 +1,113 @@ +/* + * 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 { Permission } from '@backstage/plugin-permission-common'; + +/** + * {@link https://backstage.io/docs/features/software-catalog/software-catalog-overview} + * @public + */ +export const RESOURCE_TYPE_CATALOG_ENTITY = 'catalog-entity'; + +/** + * {@link https://backstage.io/docs/features/software-catalog/descriptor-format#kind-location} + * @public + */ +export const RESOURCE_TYPE_CATALOG_LOCATION = 'catalog-location'; + +/** + * This permission is used to authorize actions that involve reading one or more + * entities from the catalog. + * + * If this permission is not authorized, it will appear that the entity does not + * exist in the catalog — both in the frontend and in API responses. + * @public + */ +export const catalogEntityReadPermission: Permission = { + name: 'catalog.entity.read', + attributes: { + action: 'read', + }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}; + +/** + * This permission is used to designate actions that involve removing one or + * more entities from the catalog. + * @public + */ +export const catalogEntityDeletePermission: Permission = { + name: 'catalog.entity.delete', + attributes: { + action: 'delete', + }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}; + +/** + * This permission is used to designate refreshing one or more entities from the + * catalog. + * @public + */ +export const catalogEntityRefreshPermission: Permission = { + name: 'catalog.entity.refresh', + attributes: { + action: 'update', + }, + resourceType: RESOURCE_TYPE_CATALOG_ENTITY, +}; + +/** + * This permission is used to designate actions that involve reading one or more + * locations from the catalog. + * + * If this permission is not authorized, it will appear that the location does + * not exist in the catalog — both in the frontend and in API responses. + * @public + */ +export const catalogLocationReadPermission: Permission = { + name: 'catalog.location.read', + attributes: { + action: 'read', + }, + resourceType: RESOURCE_TYPE_CATALOG_LOCATION, +}; + +/** + * This permission is used to designate actions that involve creating catalog + * locations. + * @public + */ +export const catalogLocationCreatePermission: Permission = { + name: 'catalog.location.create', + attributes: { + action: 'create', + }, + resourceType: RESOURCE_TYPE_CATALOG_LOCATION, +}; + +/** + * This permission is used to designate actions that involve deleting locations + * from the catalog. + * @public + */ +export const catalogLocationDeletePermission: Permission = { + name: 'catalog.location.delete', + attributes: { + action: 'delete', + }, + resourceType: RESOURCE_TYPE_CATALOG_LOCATION, +}; diff --git a/plugins/catalog-common/src/setupTests.ts b/plugins/catalog-common/src/setupTests.ts new file mode 100644 index 0000000000..fb7d1a181a --- /dev/null +++ b/plugins/catalog-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index b421a15265..59afadc62e 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -23,8 +23,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -42,8 +42,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index d417a337df..e017c45d09 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.10", "@backstage/integration-react": "^0.1.15", @@ -56,8 +56,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index c628735ef1..0f7e08ead2 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-react +## 0.6.7 + +### Patch Changes + +- 6156fb8730: `useEntityTypeFilter`: Skip updating selected types if a kind filter change did not change them. +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + - @backstage/catalog-model@0.9.8 + +## 0.6.6 + +### Patch Changes + +- 4c0f0b2003: Removed dependency on `@backstage/core-app-api`. + ## 0.6.5 ### Patch Changes diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index c4aa254226..d5cf06107c 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -9,6 +9,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client'; import { CatalogApi } from '@backstage/catalog-client'; +import { CatalogListResponse } from '@backstage/catalog-client'; import { ComponentEntity } from '@backstage/catalog-model'; import { ComponentProps } from 'react'; import { Context } from 'react'; @@ -271,14 +272,13 @@ export const EntityRefLink: React_2.ForwardRefExoticComponent< | 'children' | 'key' | 'id' - | 'className' | 'classes' - | 'innerRef' | 'defaultChecked' | 'defaultValue' | 'suppressContentEditableWarning' | 'suppressHydrationWarning' | 'accessKey' + | 'className' | 'contentEditable' | 'contextMenu' | 'draggable' @@ -519,6 +519,7 @@ export const EntityRefLink: React_2.ForwardRefExoticComponent< | 'onTransitionEndCapture' | 'component' | 'variant' + | 'innerRef' | 'download' | 'href' | 'hrefLang' @@ -847,6 +848,12 @@ export function useEntityOwnership(): { // @public export function useEntityTypeFilter(): EntityTypeReturn; +// @public +export function useOwnedEntities(allowedKinds?: string[]): { + loading: boolean; + ownedEntities: CatalogListResponse | undefined; +}; + // Warning: (ae-missing-release-tag) "useOwnUser" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index b5a6884fa5..0bdffc0724 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-react", "description": "A frontend library that helps other Backstage plugins interact with the catalog", - "version": "0.6.5", + "version": "0.6.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,9 @@ }, "dependencies": { "@backstage/catalog-client": "^0.5.2", - "@backstage/catalog-model": "^0.9.7", - "@backstage/core-app-api": "^0.2.0", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/catalog-model": "^0.9.8", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.4", "@backstage/integration": "^0.6.10", "@backstage/types": "^0.1.1", @@ -53,7 +52,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/catalog-react/src/hooks/index.ts b/plugins/catalog-react/src/hooks/index.ts index bbbca3a501..a6684732ce 100644 --- a/plugins/catalog-react/src/hooks/index.ts +++ b/plugins/catalog-react/src/hooks/index.ts @@ -42,3 +42,4 @@ export { useEntityOwnership, loadIdentityOwnerRefs, } from './useEntityOwnership'; +export { useOwnedEntities } from './useOwnedEntities'; diff --git a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx index 5a8ed6775a..bf5777becc 100644 --- a/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx +++ b/plugins/catalog-react/src/hooks/useEntityTypeFilter.tsx @@ -16,6 +16,7 @@ import { useEffect, useMemo, useRef, useState } from 'react'; import { useAsync } from 'react-use'; +import isEqual from 'lodash/isEqual'; import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef } from '../api'; import { useEntityListProvider } from './useEntityListProvider'; @@ -107,7 +108,9 @@ export function useEntityTypeFilter(): EntityTypeReturn { const stillValidTypes = selectedTypes.filter(value => newTypes.includes(value), ); - setSelectedTypes(stillValidTypes); + if (!isEqual(selectedTypes, stillValidTypes)) { + setSelectedTypes(stillValidTypes); + } }, [loading, kind, selectedTypes, setSelectedTypes, entities]); useEffect(() => { diff --git a/plugins/catalog-react/src/hooks/useOwnedEntities.ts b/plugins/catalog-react/src/hooks/useOwnedEntities.ts new file mode 100644 index 0000000000..f9191364cb --- /dev/null +++ b/plugins/catalog-react/src/hooks/useOwnedEntities.ts @@ -0,0 +1,71 @@ +/* + * 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 { catalogApiRef } from './../api'; +import { + loadCatalogOwnerRefs, + loadIdentityOwnerRefs, +} from './useEntityOwnership'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model'; +import { CatalogListResponse } from '@backstage/catalog-client'; +import { useAsync } from 'react-use'; +import { useMemo } from 'react'; + +/** + * Takes the relevant parts of the Backstage identity, and translates them into + * a list of entities which are owned by the user. Takes an optional parameter + * to filter the entities based on allowedKinds + * + * @public + * + * @param allowedKinds - Array of allowed kinds to filter the entities + * @returns CatalogListResponse + */ +export function useOwnedEntities(allowedKinds?: string[]): { + loading: boolean; + ownedEntities: CatalogListResponse | undefined; +} { + const identityApi = useApi(identityApiRef); + const catalogApi = useApi(catalogApiRef); + + const { loading, value: refs } = useAsync(async () => { + const identityRefs = await loadIdentityOwnerRefs(identityApi); + const catalogRefs = await loadCatalogOwnerRefs(catalogApi, identityRefs); + const catalogs = await catalogApi.getEntities( + allowedKinds + ? { + filter: { + kind: allowedKinds, + [`relations.${RELATION_OWNED_BY}`]: + [...identityRefs, ...catalogRefs] || [], + }, + } + : { + filter: { + [`relations.${RELATION_OWNED_BY}`]: + [...identityRefs, ...catalogRefs] || [], + }, + }, + ); + return catalogs; + }, []); + + const ownedEntities = useMemo(() => { + return refs; + }, [refs]); + + return useMemo(() => ({ loading, ownedEntities }), [loading, ownedEntities]); +} diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 2be7fc8575..80271c89af 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -66,7 +66,7 @@ export type BackstageOverrides = Overrides & { // Warning: (ae-missing-release-tag) "CatalogClientWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated export class CatalogClientWrapper implements CatalogApi { constructor(options: { client: CatalogClient; identityApi: IdentityApi }); // (undocumented) diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 57e14b5475..00504d7777 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -33,8 +33,8 @@ "dependencies": { "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.3", "@backstage/integration-react": "^0.1.15", "@backstage/plugin-catalog-react": "^0.6.5", @@ -53,8 +53,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog/src/CatalogClientWrapper.ts b/plugins/catalog/src/CatalogClientWrapper.ts index 0463cbf30e..c5f072e96c 100644 --- a/plugins/catalog/src/CatalogClientWrapper.ts +++ b/plugins/catalog/src/CatalogClientWrapper.ts @@ -30,6 +30,13 @@ import { IdentityApi } from '@backstage/core-plugin-api'; /** * CatalogClient wrapper that injects identity token for all requests + * + * @deprecated The default catalog client now uses the `fetchApiRef` + * implementation, which in turn by default issues tokens just the same as this + * class used to assist in doing. If you use a custom `fetchApiRef` + * implementation that does NOT issue tokens, or use a custom `catalogApiRef` + * implementation which does not use the default `fetchApiRef`, you can wrap + * your catalog API in this class to get back the old behavior. */ export class CatalogClientWrapper implements CatalogApi { private readonly identityApi: IdentityApi; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 20f937b2e7..66fdf4638f 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -22,7 +22,6 @@ import { entityRouteRef, starredEntitiesApiRef, } from '@backstage/plugin-catalog-react'; -import { CatalogClientWrapper } from './CatalogClientWrapper'; import { createComponentRouteRef, viewTechDocRouteRef } from './routes'; import { createApiFactory, @@ -30,7 +29,7 @@ import { createPlugin, createRoutableExtension, discoveryApiRef, - identityApiRef, + fetchApiRef, storageApiRef, } from '@backstage/core-plugin-api'; @@ -39,14 +38,13 @@ export const catalogPlugin = createPlugin({ apis: [ createApiFactory({ api: catalogApiRef, - deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, - factory: ({ discoveryApi, identityApi }) => - new CatalogClientWrapper({ - client: new CatalogClient({ discoveryApi }), - identityApi, - }), + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new CatalogClient({ discoveryApi, fetchApi }), }), - createApiFactory({ api: starredEntitiesApiRef, deps: { storageApi: storageApiRef }, diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index e2aa415043..f449e5556c 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -52,8 +52,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 6e91b22fcb..44dafc8245 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -49,8 +49,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index 951a93110a..c5cb3e3acb 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -23,8 +23,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.4", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", @@ -43,8 +43,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index 59d34cc00d..accf5e2d01 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.5", "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", @@ -38,8 +38,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 45975163e4..94ec2bb5fe 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -56,8 +56,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 9975992ea5..4db5aa31e2 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/plugin-explore-react": "^0.0.8", "@backstage/theme": "^0.2.14", @@ -50,8 +50,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 68ee004605..784346e07c 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -22,8 +22,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -36,8 +36,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index b28c8bd89f..6542721b61 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", @@ -50,8 +50,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index e73dc7736a..5580595dc8 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index ab11255e08..720f624fdd 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/integration": "^0.6.10", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -40,8 +40,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 4c4a339415..15c8f61978 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-github-actions +## 0.4.27 + +### Patch Changes + +- 89bd772b00: Show empty state only when workflow API call has completed +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + - @backstage/catalog-model@0.9.8 + - @backstage/plugin-catalog-react@0.6.7 + ## 0.4.26 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 434a0b6224..22cd220e9c 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-actions", "description": "A Backstage plugin that integrates towards GitHub Actions", - "version": "0.4.26", + "version": "0.4.27", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,11 +33,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/catalog-model": "^0.9.8", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/integration": "^0.6.10", - "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-catalog-react": "^0.6.7", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -52,8 +52,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index 1965f98ef1..d593ae7ac0 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.3", "@backstage/integration": "^0.6.10", "@backstage/integration-react": "^0.1.15", @@ -40,8 +40,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 834fd2f7cf..b6d3f2f569 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -32,8 +32,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,8 +45,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index ec5a37bf36..7f165cbef5 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -45,8 +45,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/home/package.json b/plugins/home/package.json index b04e4a787d..4d14388d09 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -36,8 +36,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 5c07bc9d21..a476abebd8 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", @@ -40,8 +40,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index c909c54806..b8d171f9c1 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -52,6 +52,8 @@ export interface JenkinsInfo { // (undocumented) baseUrl: string; // (undocumented) + crumbIssuer?: boolean; + // (undocumented) headers?: Record; // (undocumented) jobFullName: string; @@ -77,6 +79,8 @@ export interface JenkinsInstanceConfig { // (undocumented) baseUrl: string; // (undocumented) + crumbIssuer?: boolean; + // (undocumented) name: string; // (undocumented) username: string; diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts index 5f74259eef..a410d8ad92 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.test.ts @@ -411,4 +411,17 @@ describe('JenkinsApi', () => { }); expect(mockedJenkinsClient.job.build).toBeCalledWith(jobFullName); }); + + it('buildProject with crumbIssuer option', async () => { + const info: JenkinsInfo = { ...jenkinsInfo, crumbIssuer: true }; + await jenkinsApi.buildProject(info, jobFullName); + + expect(mockedJenkins).toHaveBeenCalledWith({ + baseUrl: jenkinsInfo.baseUrl, + headers: jenkinsInfo.headers, + promisify: true, + crumbIssuer: true, + }); + expect(mockedJenkinsClient.job.build).toBeCalledWith(jobFullName); + }); }); diff --git a/plugins/jenkins-backend/src/service/jenkinsApi.ts b/plugins/jenkins-backend/src/service/jenkinsApi.ts index 13705ce049..f156259f04 100644 --- a/plugins/jenkins-backend/src/service/jenkinsApi.ts +++ b/plugins/jenkins-backend/src/service/jenkinsApi.ts @@ -146,6 +146,7 @@ export class JenkinsApiImpl { baseUrl: jenkinsInfo.baseUrl, headers: jenkinsInfo.headers, promisify: true, + crumbIssuer: jenkinsInfo.crumbIssuer, }) as any; } diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts index 08439297a7..21a2022bb3 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts @@ -210,6 +210,7 @@ describe('DefaultJenkinsInfoProvider', () => { expect(mockCatalog.getEntityByName).toBeCalledWith(entityRef); expect(info).toStrictEqual({ baseUrl: 'https://jenkins.example.com', + crumbIssuer: undefined, headers: { Authorization: 'Basic YmFja3N0YWdlIC0gYm90OjEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNlZGYwMTI=', diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts index 1a992e73f1..f5cb52b696 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts @@ -38,6 +38,7 @@ export interface JenkinsInfo { baseUrl: string; headers?: Record; jobFullName: string; // TODO: make this an array + crumbIssuer?: boolean; } export interface JenkinsInstanceConfig { @@ -45,6 +46,7 @@ export interface JenkinsInstanceConfig { baseUrl: string; username: string; apiKey: string; + crumbIssuer?: boolean; } /** @@ -70,6 +72,7 @@ export class JenkinsConfig { baseUrl: c.getString('baseUrl'), username: c.getString('username'), apiKey: c.getString('apiKey'), + crumbIssuer: c.getOptionalBoolean('crumbIssuer'), })) || []; // load unnamed default config @@ -81,6 +84,7 @@ export class JenkinsConfig { const baseUrl = jenkinsConfig.getOptionalString('baseUrl'); const username = jenkinsConfig.getOptionalString('username'); const apiKey = jenkinsConfig.getOptionalString('apiKey'); + const crumbIssuer = jenkinsConfig.getOptionalBoolean('crumbIssuer'); if (hasNamedDefault && (baseUrl || username || apiKey)) { throw new Error( @@ -98,12 +102,13 @@ export class JenkinsConfig { if (unnamedAllPresent) { const unnamedInstanceConfig = [ - { name: DEFAULT_JENKINS_NAME, baseUrl, username, apiKey }, + { name: DEFAULT_JENKINS_NAME, baseUrl, username, apiKey, crumbIssuer }, ] as { name: string; baseUrl: string; username: string; apiKey: string; + crumbIssuer: boolean; }[]; return new JenkinsConfig([ @@ -227,6 +232,7 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { Authorization: `Basic ${creds}`, }, jobFullName, + crumbIssuer: instanceConfig.crumbIssuer, }; } diff --git a/plugins/jenkins/api-report.md b/plugins/jenkins/api-report.md index f33e565d89..a6926c1639 100644 --- a/plugins/jenkins/api-report.md +++ b/plugins/jenkins/api-report.md @@ -9,8 +9,8 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; -import { EntityName } from '@backstage/catalog-model'; -import { EntityRef } from '@backstage/catalog-model'; +import type { EntityName } from '@backstage/catalog-model'; +import type { EntityRef } from '@backstage/catalog-model'; import { IdentityApi } from '@backstage/core-plugin-api'; import { InfoCardVariants } from '@backstage/core-components'; import { RouteRef } from '@backstage/core-plugin-api'; diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 2211bf54de..862c2cdfb3 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -33,8 +33,9 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", + "@backstage/errors": "^0.1.5", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -49,8 +50,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/jenkins/src/api/JenkinsApi.ts b/plugins/jenkins/src/api/JenkinsApi.ts index 4b23fb4292..33edf0ade3 100644 --- a/plugins/jenkins/src/api/JenkinsApi.ts +++ b/plugins/jenkins/src/api/JenkinsApi.ts @@ -19,7 +19,8 @@ import { DiscoveryApi, IdentityApi, } from '@backstage/core-plugin-api'; -import { EntityName, EntityRef } from '@backstage/catalog-model'; +import type { EntityName, EntityRef } from '@backstage/catalog-model'; +import { ResponseError } from '@backstage/errors'; export const jenkinsApiRef = createApiRef({ id: 'plugin.jenkins.service2', @@ -140,7 +141,7 @@ export class JenkinsClient implements JenkinsApi { url.searchParams.append('branch', filter.branch); } - const idToken = await this.identityApi.getIdToken(); + const idToken = await this.getToken(); const response = await fetch(url.href, { method: 'GET', headers: { @@ -151,8 +152,8 @@ export class JenkinsClient implements JenkinsApi { return ( (await response.json()).projects?.map((p: Project) => ({ ...p, - onRestartClick: async () => { - await this.retry({ + onRestartClick: () => { + return this.retry({ entity, jobFullName: p.fullName, buildNumber: String(p.lastBuild.number), @@ -179,7 +180,7 @@ export class JenkinsClient implements JenkinsApi { jobFullName, )}/${encodeURIComponent(buildNumber)}`; - const idToken = await this.identityApi.getIdToken(); + const idToken = await this.getToken(); const response = await fetch(url, { method: 'GET', headers: { @@ -207,12 +208,21 @@ export class JenkinsClient implements JenkinsApi { jobFullName, )}/${encodeURIComponent(buildNumber)}:rebuild`; - const idToken = await this.identityApi.getIdToken(); - await fetch(url, { + const idToken = await this.getToken(); + const response = await fetch(url, { method: 'POST', headers: { ...(idToken && { Authorization: `Bearer ${idToken}` }), }, }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + } + + private async getToken() { + const { token } = await this.identityApi.getCredentials(); + return token; } } diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx index b1eaf2c24c..a959ba39c9 100644 --- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -13,17 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { Box, IconButton, Link, Typography, Tooltip } from '@material-ui/core'; +import React, { useState } from 'react'; +import { Box, IconButton, Link, Tooltip, Typography } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; import JenkinsLogo from '../../../../assets/JenkinsLogo.svg'; import { Link as RouterLink } from 'react-router-dom'; import { JenkinsRunStatus } from '../Status'; import { useBuilds } from '../../../useBuilds'; import { buildRouteRef } from '../../../../plugin'; -import { Table, TableColumn } from '@backstage/core-components'; +import { Progress, Table, TableColumn } from '@backstage/core-components'; import { Project } from '../../../../api/JenkinsApi'; -import { useRouteRef } from '@backstage/core-plugin-api'; +import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; const FailCount = ({ count }: { count: number }): JSX.Element | null => { if (count !== 0) { @@ -173,13 +173,46 @@ const generatedColumns: TableColumn[] = [ { title: 'Actions', sorting: false, - render: (row: Partial) => ( - - - - - - ), + render: (row: Partial) => { + const ActionWrapper = () => { + const [isLoadingRebuild, setIsLoadingRebuild] = useState(false); + const alertApi = useApi(alertApiRef); + + const onRebuild = async () => { + if (row.onRestartClick) { + setIsLoadingRebuild(true); + try { + await row.onRestartClick(); + alertApi.post({ + message: 'Jenkins re-build has successfully executed', + severity: 'success', + }); + } catch (e) { + alertApi.post({ + message: `Jenkins re-build has failed. Error: ${e.message}`, + severity: 'error', + }); + } finally { + setIsLoadingRebuild(false); + } + } + }; + + return ( + + <> + {isLoadingRebuild && } + {!isLoadingRebuild && ( + + + + )} + + + ); + }; + return ; + }, width: '10%', }, ]; diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 309e239a68..d3353a1f0d 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -22,8 +22,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -36,8 +36,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 0329a9bc5b..94826738b3 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-kubernetes +## 0.5.1 + +### Patch Changes + +- 6f0c850a86: Fixed styling bug for the CronJobsAccordions and updated Completed pods to display a green dot. +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + - @backstage/catalog-model@0.9.8 + - @backstage/plugin-catalog-react@0.6.7 + ## 0.5.0 ### Minor Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 3a2ea6a9a9..02b8e85855 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes", "description": "A Backstage plugin that integrates towards Kubernetes", - "version": "0.5.0", + "version": "0.5.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.7", + "@backstage/catalog-model": "^0.9.8", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", - "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", + "@backstage/plugin-catalog-react": "^0.6.7", "@backstage/plugin-kubernetes-common": "^0.2.0", "@kubernetes/client-node": "^0.16.0", "@backstage/theme": "^0.2.14", @@ -53,8 +53,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/kubernetes/src/components/Cluster/Cluster.tsx b/plugins/kubernetes/src/components/Cluster/Cluster.tsx index b7a819cdbf..eb12ba20f8 100644 --- a/plugins/kubernetes/src/components/Cluster/Cluster.tsx +++ b/plugins/kubernetes/src/components/Cluster/Cluster.tsx @@ -148,9 +148,9 @@ export const Cluster = ({ clusterObjects, podsWithErrors }: ClusterProps) => { - - - + + + diff --git a/plugins/kubernetes/src/utils/pod.tsx b/plugins/kubernetes/src/utils/pod.tsx index bfb40750cd..50cc983b9d 100644 --- a/plugins/kubernetes/src/utils/pod.tsx +++ b/plugins/kubernetes/src/utils/pod.tsx @@ -63,7 +63,13 @@ export const containerStatuses = (pod: V1Pod): ReactNode => { const renderCell = (reason: string | undefined) => ( Container: {next.name}} + value={ + reason === 'Completed' ? ( + Container: {next.name} + ) : ( + Container: {next.name} + ) + } subvalue={reason} />
diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 43aabc3e3f..3820e85fdd 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -34,8 +34,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.10", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -48,8 +48,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index b7be235b9e..9a32e23e18 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -32,8 +32,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 0576d644da..eb2599237c 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-org +## 0.3.31 + +### Patch Changes + +- fe86adbcd2: Added `entityFilterKind` property for `EntityOwnershipCard` +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + - @backstage/catalog-model@0.9.8 + - @backstage/plugin-catalog-react@0.6.7 + ## 0.3.30 ### Patch Changes diff --git a/plugins/org/api-report.md b/plugins/org/api-report.md index 88ce00fd52..60caa45134 100644 --- a/plugins/org/api-report.md +++ b/plugins/org/api-report.md @@ -33,9 +33,11 @@ export const EntityMembersListCard: (_props: { // @public (undocumented) export const EntityOwnershipCard: ({ variant, + entityFilterKind, }: { entity?: Entity | undefined; variant?: InfoCardVariants | undefined; + entityFilterKind?: string[] | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityUserProfileCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -75,9 +77,11 @@ export { orgPlugin as plugin }; // @public (undocumented) export const OwnershipCard: ({ variant, + entityFilterKind, }: { entity?: Entity | undefined; variant?: InfoCardVariants | undefined; + entityFilterKind?: string[] | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "UserProfileCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/org/package.json b/plugins/org/package.json index 4e7f556547..def4bac002 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-org", "description": "A Backstage plugin that helps you create entity pages for your organization", - "version": "0.3.30", + "version": "0.3.31", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -21,14 +21,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", - "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/catalog-model": "^0.9.8", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", + "@backstage/plugin-catalog-react": "^0.6.7", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", + "pluralize": "^8.0.0", "qs": "^6.10.1", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", @@ -38,8 +39,9 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", + "@backstage/catalog-client": "^0.5.2", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx index 8d6fc1a002..439365e583 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.test.tsx @@ -14,7 +14,11 @@ * limitations under the License. */ -import { GroupEntity, UserEntity } from '@backstage/catalog-model'; +import { + CatalogEntitiesRequest, + CatalogListResponse, +} from '@backstage/catalog-client'; +import { Entity, GroupEntity, UserEntity } from '@backstage/catalog-model'; import { CatalogApi, catalogApiRef, @@ -26,6 +30,97 @@ import { queryByText } from '@testing-library/react'; import React from 'react'; import { OwnershipCard } from './OwnershipCard'; +const items = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'API', + metadata: { + name: 'my-api', + }, + spec: { + type: 'openapi', + }, + relations: [ + { + type: 'ownedBy', + target: { + name: 'my-team', + namespace: 'default', + kind: 'Group', + }, + }, + ], + }, + { + kind: 'Component', + metadata: { + name: 'my-service', + }, + spec: { + type: 'service', + }, + relations: [ + { + type: 'ownedBy', + target: { + name: 'my-team', + namespace: 'default', + kind: 'Group', + }, + }, + ], + }, + { + kind: 'Component', + metadata: { + name: 'my-library', + namespace: 'other-namespace', + }, + spec: { + type: 'library', + }, + relations: [ + { + type: 'ownedBy', + target: { + name: 'my-team', + namespace: 'default', + kind: 'Group', + }, + }, + ], + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'System', + metadata: { + name: 'my-system', + }, + relations: [ + { + type: 'ownedBy', + target: { + name: 'my-team', + namespace: 'default', + kind: 'Group', + }, + }, + ], + }, +] as Entity[]; + +const getEntitiesMock = ( + request?: CatalogEntitiesRequest, +): Promise> => { + const filterKinds = + !Array.isArray(request?.filter) && Array.isArray(request?.filter?.kind) + ? request?.filter?.kind ?? [] + : []; // we expect the request to be like { filter: { kind: ['API','System'], .... }. If changed in OwnerShipCard, let's change in also here + return Promise.resolve({ + items: items.filter(item => filterKinds.find(k => k === item.kind)), + } as CatalogListResponse); +}; + describe('OwnershipCard', () => { const groupEntity: GroupEntity = { apiVersion: 'backstage.io/v1alpha1', @@ -49,75 +144,12 @@ describe('OwnershipCard', () => { ], }; - const items = [ - { - kind: 'API', - metadata: { - name: 'my-api', - }, - spec: { - type: 'openapi', - }, - relations: [ - { - type: 'ownedBy', - target: { - name: 'my-team', - namespace: 'default', - kind: 'Group', - }, - }, - ], - }, - { - kind: 'Component', - metadata: { - name: 'my-service', - }, - spec: { - type: 'service', - }, - relations: [ - { - type: 'ownedBy', - target: { - name: 'my-team', - namespace: 'default', - kind: 'Group', - }, - }, - ], - }, - { - kind: 'Component', - metadata: { - name: 'my-library', - namespace: 'other-namespace', - }, - spec: { - type: 'library', - }, - relations: [ - { - type: 'ownedBy', - target: { - name: 'my-team', - namespace: 'default', - kind: 'Group', - }, - }, - ], - }, - ] as any; - it('displays entity counts', async () => { const catalogApi: jest.Mocked = { getEntities: jest.fn(), } as any; - catalogApi.getEntities.mockResolvedValue({ - items, - }); + catalogApi.getEntities.mockImplementation(getEntitiesMock); const { getByText } = await renderInTestApp( @@ -132,6 +164,17 @@ describe('OwnershipCard', () => { }, ); + expect(catalogApi.getEntities).toHaveBeenCalledWith({ + filter: { kind: ['Component', 'API'] }, + fields: [ + 'kind', + 'metadata.name', + 'metadata.namespace', + 'spec.type', + 'relations', + ], + }); + expect(getByText('OPENAPI')).toBeInTheDocument(); expect( queryByText(getByText('OPENAPI').parentElement!, '1'), @@ -144,6 +187,38 @@ describe('OwnershipCard', () => { expect( queryByText(getByText('LIBRARY').parentElement!, '1'), ).toBeInTheDocument(); + expect(() => getByText('SYSTEM')).toThrowError(); + }); + + it('applies CustomFilterDefinition', async () => { + const catalogApi: jest.Mocked = { + getEntities: jest.fn(), + } as any; + + catalogApi.getEntities.mockImplementation(getEntitiesMock); + + const { getByText } = await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/create': catalogRouteRef, + }, + }, + ); + + expect(getByText('SYSTEM')).toBeInTheDocument(); + expect( + queryByText(getByText('SYSTEM').parentElement!, '1'), + ).toBeInTheDocument(); + expect(getByText('OPENAPI')).toBeInTheDocument(); + expect( + queryByText(getByText('OPENAPI').parentElement!, '1'), + ).toBeInTheDocument(); + expect(() => getByText('LIBRARY')).toThrowError(); }); it('links to the catalog with the group filter', async () => { @@ -151,9 +226,7 @@ describe('OwnershipCard', () => { getEntities: jest.fn(), } as any; - catalogApi.getEntities.mockResolvedValue({ - items, - }); + catalogApi.getEntities.mockImplementation(getEntitiesMock); const { getByText } = await renderInTestApp( @@ -199,9 +272,7 @@ describe('OwnershipCard', () => { getEntities: jest.fn(), } as any; - catalogApi.getEntities.mockResolvedValue({ - items, - }); + catalogApi.getEntities.mockImplementation(getEntitiesMock); const { getByText } = await renderInTestApp( diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index fe23191779..e8d7c44816 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -40,6 +40,7 @@ import { } from '@material-ui/core'; import qs from 'qs'; import React from 'react'; +import pluralize from 'pluralize'; import { useAsync } from 'react-use'; type EntityTypeProps = { @@ -96,7 +97,7 @@ const EntityCountTile = ({ {counter} - {name} + {pluralize(name, counter)} @@ -128,10 +129,12 @@ const getQueryParams = ( export const OwnershipCard = ({ variant, + entityFilterKind, }: { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; variant?: InfoCardVariants; + entityFilterKind?: string[]; }) => { const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); @@ -142,7 +145,7 @@ export const OwnershipCard = ({ error, value: componentsWithCounters, } = useAsync(async () => { - const kinds = ['Component', 'API']; + const kinds = entityFilterKind ?? ['Component', 'API']; const entitiesList = await catalogApi.getEntities({ filter: { kind: kinds, @@ -162,17 +165,17 @@ export const OwnershipCard = ({ const counts = ownedEntitiesList.reduce( (acc: EntityTypeProps[], ownedEntity) => { - if (typeof ownedEntity.spec?.type !== 'string') return acc; - const match = acc.find( - x => x.kind === ownedEntity.kind && x.type === ownedEntity.spec?.type, + x => + x.kind === ownedEntity.kind && + x.type === (ownedEntity.spec?.type ?? ownedEntity.kind), ); if (match) { match.count += 1; } else { acc.push({ kind: ownedEntity.kind, - type: ownedEntity.spec?.type, + type: ownedEntity.spec?.type?.toString() ?? ownedEntity.kind, count: 1, }); } diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 59e76878b5..1b6c38465f 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -49,8 +49,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index d6a14a5b2a..012cb24490 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -50,8 +50,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts index 00a5690c0a..56c45fa015 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/index.ts @@ -96,6 +96,16 @@ export function createFetchRailsAction(options: { description: "Don't run Webpack install", type: 'boolean', }, + skipTest: { + title: 'skipTest', + description: 'Skip test files', + type: 'boolean', + }, + force: { + title: 'force', + description: 'Overwrite files that already exist', + type: 'boolean', + }, api: { title: 'api', description: 'Preconfigure smaller stack for API only apps', diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts index 99a113f4f7..cced9f7ac2 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.test.ts @@ -27,6 +27,8 @@ describe('railsArgumentResolver', () => { [{ api: true }, ['--api']], [{ skipBundle: true }, ['--skip-bundle']], [{ skipWebpackInstall: true }, ['--skip-webpack-install']], + [{ skipTest: true }, ['--skip-test']], + [{ force: true }, ['--force']], [{ webpacker: 'vue' }, ['--webpack', 'vue']], [{ database: 'postgresql' }, ['--database', 'postgresql']], [{ railsVersion: 'dev' }, ['--dev']], diff --git a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts index 60251a702d..4006ead630 100644 --- a/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts +++ b/plugins/scaffolder-backend-module-rails/src/actions/fetch/rails/railsArgumentResolver.ts @@ -51,6 +51,8 @@ export type RailsRunOptions = { railsVersion?: RailsVersion; skipBundle?: boolean; skipWebpackInstall?: boolean; + skipTest?: boolean; + force?: boolean; }; export const railsArgumentResolver = ( @@ -76,6 +78,14 @@ export const railsArgumentResolver = ( argumentsToRun.push('--skip-webpack-install'); } + if (options?.skipTest) { + argumentsToRun.push('--skip-test'); + } + + if (options?.force) { + argumentsToRun.push('--force'); + } + if ( options?.webpacker && Object.values(Webpacker).includes(options?.webpacker as Webpacker) diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 3986961f16..50d10b564c 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-scaffolder-backend +## 0.15.17 + +### Patch Changes + +- eec0750d8d: Makes cookiecutter a default, but optional action based on if a containerRunner argument is passed in to createRouter or createBuiltinActions +- ed52f74ab3: Adding changes to create GitLab Merge Request using custom action +- Updated dependencies + - @backstage/plugin-catalog-backend@0.19.2 + - @backstage/backend-common@0.9.14 + - @backstage/catalog-model@0.9.8 + ## 0.15.16 ### Patch Changes diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 6ed4625f66..b7c1a9146c 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -68,7 +68,7 @@ export const createBuiltinActions: (options: { reader: UrlReader; integrations: ScmIntegrations; catalogClient: CatalogApi; - containerRunner: ContainerRunner; + containerRunner?: ContainerRunner; config: Config; }) => TemplateAction[]; @@ -179,6 +179,13 @@ export function createPublishGitlabAction(options: { config: Config; }): TemplateAction; +// Warning: (ae-missing-release-tag) "createPublishGitlabMergeRequestAction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const createPublishGitlabMergeRequestAction: (options: { + integrations: ScmIntegrationRegistry; +}) => TemplateAction; + // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -289,7 +296,7 @@ export interface RouterOptions { // (undocumented) config: Config; // (undocumented) - containerRunner: ContainerRunner; + containerRunner?: ContainerRunner; // (undocumented) database: PluginDatabaseManager; // (undocumented) diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 2dae658d74..3686c33c40 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend", "description": "The Backstage backend plugin that helps you create new things", - "version": "0.15.16", + "version": "0.15.17", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ "build:assets": "node scripts/build-nunjucks.js" }, "dependencies": { - "@backstage/backend-common": "^0.9.13", + "@backstage/backend-common": "^0.9.14", "@backstage/catalog-client": "^0.5.2", - "@backstage/catalog-model": "^0.9.7", + "@backstage/catalog-model": "^0.9.8", "@backstage/config": "^0.1.11", "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.10", - "@backstage/plugin-catalog-backend": "^0.19.1", + "@backstage/plugin-catalog-backend": "^0.19.2", "@backstage/plugin-scaffolder-common": "^0.1.1", "@backstage/plugin-scaffolder-backend-module-cookiecutter": "^0.1.5", "@backstage/types": "^0.1.1", @@ -73,7 +73,7 @@ "vm2": "^3.9.5" }, "devDependencies": { - "@backstage/cli": "^0.10.1", + "@backstage/cli": "^0.10.2", "@backstage/test-utils": "^0.1.24", "@types/command-exists": "^1.2.0", "@types/fs-extra": "^9.0.1", diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index d38dfe3cdc..93ecceb76a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -36,6 +36,7 @@ import { createPublishGithubAction, createPublishGithubPullRequestAction, createPublishGitlabAction, + createPublishGitlabMergeRequestAction, } from './publish'; import { createGithubActionsDispatchAction, @@ -46,22 +47,17 @@ export const createBuiltinActions = (options: { reader: UrlReader; integrations: ScmIntegrations; catalogClient: CatalogApi; - containerRunner: ContainerRunner; + containerRunner?: ContainerRunner; config: Config; }) => { const { reader, integrations, containerRunner, catalogClient, config } = options; - return [ + const actions = [ createFetchPlainAction({ reader, integrations, }), - createFetchCookiecutterAction({ - reader, - integrations, - containerRunner, - }), createFetchTemplateAction({ integrations, reader, @@ -77,6 +73,9 @@ export const createBuiltinActions = (options: { integrations, config, }), + createPublishGitlabMergeRequestAction({ + integrations, + }), createPublishBitbucketAction({ integrations, config, @@ -97,4 +96,16 @@ export const createBuiltinActions = (options: { integrations, }), ]; + + if (containerRunner) { + actions.push( + createFetchCookiecutterAction({ + reader, + integrations, + containerRunner, + }), + ); + } + + return actions; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts new file mode 100644 index 0000000000..2ac369249b --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts @@ -0,0 +1,186 @@ +/* + * 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 { createTemplateAction } from '../../createTemplateAction'; +import { readFile } from 'fs-extra'; +import { Gitlab } from '@gitbeaker/node'; +import globby from 'globby'; +import { Types } from '@gitbeaker/core'; + +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { InputError } from '@backstage/errors'; +import { parseRepoUrl } from './util'; +import { resolveSafeChildPath } from '@backstage/backend-common'; + +export type GitlabMergeRequestActionInput = { + projectid: string; + repoUrl: string; + title: string; + description: string; + branchName: string; + targetPath: string; +}; + +export const createPublishGitlabMergeRequestAction = (options: { + integrations: ScmIntegrationRegistry; +}) => { + const { integrations } = options; + + return createTemplateAction({ + id: 'publish:gitlab:merge-request', + schema: { + input: { + required: ['projectid', 'repoUrl', 'targetPath', 'branchName'], + type: 'object', + properties: { + repoUrl: { + type: 'string', + title: 'Repository Location', + description: `Accepts the format 'gitlab.com/group_name/project_name' where 'project_name' is the repository name and 'group_name' is a group or username`, + }, + projectid: { + type: 'string', + title: 'projectid', + description: 'Project ID/Name(slug) of the Gitlab Project', + }, + title: { + type: 'string', + title: 'Merge Request Name', + description: 'The name for the merge request', + }, + description: { + type: 'string', + title: 'Merge Request Description', + description: 'The description of the merge request', + }, + branchName: { + type: 'string', + title: 'Destination Branch name', + description: 'The description of the merge request', + }, + targetPath: { + type: 'string', + title: 'Repository Subdirectory', + description: 'Subdirectory of repository to apply changes to', + }, + }, + }, + output: { + type: 'object', + properties: { + projectid: { + title: 'Gitlab Project id/Name(slug)', + type: 'string', + }, + mergeRequestURL: { + title: 'MergeRequest(MR) URL', + type: 'string', + description: 'Link to the merge request in GitLab', + }, + }, + }, + }, + async handler(ctx) { + const repoUrl = ctx.input.repoUrl; + const { host } = parseRepoUrl(repoUrl, integrations); + const integrationConfig = integrations.gitlab.byHost(host); + + const actions: Types.CommitAction[] = []; + + const destinationBranch = ctx.input.branchName; + + if (!integrationConfig) { + throw new InputError( + `No matching integration configuration for host ${host}, please check your integrations config`, + ); + } + + if (!integrationConfig.config.token) { + throw new InputError(`No token available for host ${host}`); + } + + const api = new Gitlab({ + host: integrationConfig.config.baseUrl, + token: integrationConfig.config.token, + }); + + const fileRoot = ctx.workspacePath; + const localFilePaths = await globby([`${ctx.input.targetPath}/**`], { + cwd: fileRoot, + gitignore: true, + dot: true, + }); + + const fileContents = await Promise.all( + localFilePaths.map(p => readFile(resolveSafeChildPath(fileRoot, p))), + ); + + const repoFilePaths = localFilePaths.map(repoFilePath => { + return repoFilePath; + }); + + for (let i = 0; i < repoFilePaths.length; i++) { + actions.push({ + action: 'create', + filePath: repoFilePaths[i], + content: fileContents[i].toString(), + }); + } + + const projects = await api.Projects.show(ctx.input.projectid); + + const { default_branch: defaultBranch } = projects; + + try { + await api.Branches.create( + ctx.input.projectid, + destinationBranch, + String(defaultBranch), + ); + } catch (e) { + throw new InputError(`The branch creation failed ${e}`); + } + + try { + await api.Commits.create( + ctx.input.projectid, + destinationBranch, + ctx.input.title, + actions, + ); + } catch (e) { + throw new InputError( + `Committing the changes to ${destinationBranch} failed ${e}`, + ); + } + + try { + const mergeRequestUrl = await api.MergeRequests.create( + ctx.input.projectid, + destinationBranch, + String(defaultBranch), + ctx.input.title, + { description: ctx.input.description }, + ).then((mergeRequest: { web_url: string }) => { + return mergeRequest.web_url; + }); + ctx.output('projectid', ctx.input.projectid); + ctx.output('mergeRequestUrl', mergeRequestUrl); + } catch (e) { + throw new InputError(`Merge request creation failed${e}`); + } + }, + }); +}; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index a292438d3a..c42b04e55e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -20,3 +20,4 @@ export { createPublishFileAction } from './file'; export { createPublishGithubAction } from './github'; export { createPublishGithubPullRequestAction } from './githubPullRequest'; export { createPublishGitlabAction } from './gitlab'; +export { createPublishGitlabMergeRequestAction } from './gitlabMergeRequest'; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index b8f2968287..9d26857ab9 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -55,7 +55,7 @@ export interface RouterOptions { catalogClient: CatalogApi; actions?: TemplateAction[]; taskWorkers?: number; - containerRunner: ContainerRunner; + containerRunner?: ContainerRunner; taskBroker?: TaskBroker; } diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 73fd8fae72..45419e82eb 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -96,6 +96,24 @@ export type FieldExtensionOptions = { validation?: CustomFieldValidator; }; +// Warning: (ae-missing-release-tag) "OwnedEntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const OwnedEntityPicker: ({ + onChange, + schema: { title, description }, + required, + uiSchema, + rawErrors, + formData, + idSchema, +}: FieldProps) => JSX.Element; + +// Warning: (ae-missing-release-tag) "OwnedEntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const OwnedEntityPickerFieldExtension: () => null; + // Warning: (ae-missing-release-tag) "OwnerPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index e3aaa722b0..3d42126624 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -34,8 +34,8 @@ "@backstage/catalog-client": "^0.5.2", "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.10", "@backstage/integration-react": "^0.1.15", @@ -66,10 +66,10 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/plugin-catalog": "^0.7.4", - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", + "@backstage/plugin-catalog": "^0.7.4", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index a8ef524a13..452a5d97b1 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -22,7 +22,7 @@ import { TextField } from '@material-ui/core'; import FormControl from '@material-ui/core/FormControl'; import Autocomplete from '@material-ui/lab/Autocomplete'; import { FieldProps } from '@rjsf/core'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useAsync } from 'react-use'; export const EntityPicker = ({ @@ -48,9 +48,18 @@ export const EntityPicker = ({ formatEntityRefTitle(e, { defaultKind }), ); - const onSelect = (_: any, value: string | null) => { - onChange(value || ''); - }; + const onSelect = useCallback( + (_: any, value: string | null) => { + onChange(value || ''); + }, + [onChange], + ); + + useEffect(() => { + if (entityRefs?.length === 1) { + onChange(entityRefs[0]); + } + }, [entityRefs, onChange]); return ( 0 && !formData} > ) => { + const allowedKinds = uiSchema['ui:options']?.allowedKinds as string[]; + const defaultKind = uiSchema['ui:options']?.defaultKind as string | undefined; + const { ownedEntities, loading } = useOwnedEntities(allowedKinds); + + const entityRefs = ownedEntities?.items + .map(e => formatEntityRefTitle(e, { defaultKind })) + .filter(n => n); + + const onSelect = (_: any, value: string | null) => { + onChange(value || ''); + }; + + return ( + 0 && !formData} + > + ( + + )} + /> + + ); +}; diff --git a/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts new file mode 100644 index 0000000000..7cf5add6eb --- /dev/null +++ b/plugins/scaffolder/src/components/fields/OwnedEntityPicker/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { OwnedEntityPicker } from './OwnedEntityPicker'; diff --git a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx index c4b1e79262..92261b6cac 100644 --- a/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx +++ b/plugins/scaffolder/src/components/fields/RepoUrlPicker/RepoUrlPicker.tsx @@ -13,19 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React, { useCallback, useEffect } from 'react'; -import { FieldProps } from '@rjsf/core'; -import { scaffolderApiRef } from '../../../api'; +import { + Progress, + Select, + SelectedItems, + SelectItem, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; import { scmIntegrationsApiRef } from '@backstage/integration-react'; -import { useAsync } from 'react-use'; -import Select from '@material-ui/core/Select'; -import InputLabel from '@material-ui/core/InputLabel'; -import Input from '@material-ui/core/Input'; import FormControl from '@material-ui/core/FormControl'; import FormHelperText from '@material-ui/core/FormHelperText'; - -import { useApi } from '@backstage/core-plugin-api'; -import { Progress } from '@backstage/core-components'; +import Input from '@material-ui/core/Input'; +import InputLabel from '@material-ui/core/InputLabel'; +import { FieldProps } from '@rjsf/core'; +import React, { useCallback, useEffect } from 'react'; +import { useAsync } from 'react-use'; +import { scaffolderApiRef } from '../../../api'; function splitFormData(url: string | undefined, allowedOwners?: string[]) { let host = undefined; @@ -106,10 +109,10 @@ export const RepoUrlPicker = ({ allowedOwners, ); const updateHost = useCallback( - (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => { + (value: SelectedItems) => { onChange( serializeFormData({ - host: evt.target.value as string, + host: value as string, owner, repo, organization, @@ -121,6 +124,21 @@ export const RepoUrlPicker = ({ [onChange, owner, repo, organization, workspace, project], ); + const updateOwnerSelect = useCallback( + (value: SelectedItems) => + onChange( + serializeFormData({ + host, + owner: value as string, + repo, + organization, + workspace, + project, + }), + ), + [onChange, host, repo, organization, workspace, project], + ); + const updateOwner = useCallback( (evt: React.ChangeEvent<{ name?: string; value: unknown }>) => onChange( @@ -224,6 +242,16 @@ export const RepoUrlPicker = ({ return ; } + const hostsOptions: SelectItem[] = integrations + ? integrations + .filter(i => allowedHosts?.includes(i.host)) + .map(i => ({ label: i.title, value: i.host })) + : [{ label: 'Loading...', value: 'loading' }]; + + const ownersOptions: SelectItem[] = allowedOwners + ? allowedOwners.map(i => ({ label: i, value: i })) + : [{ label: 'Loading...', value: 'loading' }]; + return ( <> 0 && !host} > - Host - + - {allowedOwners ? ( - allowedOwners.map(i => ( - - )) - ) : ( -

loading

- )} - ; - + label="Owner Available" + onChange={updateOwnerSelect} + disabled={ownersOptions.length === 1} + selected={owner} + items={ownersOptions} + /> + The organization, user or project that this repo will belong to diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 5adc3d3141..299470e220 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -18,3 +18,4 @@ export * from './EntityPicker'; export * from './OwnerPicker'; export * from './RepoUrlPicker'; export * from './TextValuePicker'; +export * from './OwnedEntityPicker'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index be934546b3..12a29809e0 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -24,6 +24,7 @@ import { RepoUrlPicker, } from '../components/fields/RepoUrlPicker'; import { FieldExtensionOptions } from './types'; +import { OwnedEntityPicker } from '../components/fields/OwnedEntityPicker'; export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [ { @@ -44,4 +45,8 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [ component: OwnerPicker, name: 'OwnerPicker', }, + { + component: OwnedEntityPicker, + name: 'OwnedEntityPicker', + }, ]; diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 0180c24b90..dfbd2af296 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -31,6 +31,7 @@ export { EntityPickerFieldExtension, EntityNamePickerFieldExtension, OwnerPickerFieldExtension, + OwnedEntityPickerFieldExtension, RepoUrlPickerFieldExtension, ScaffolderPage, scaffolderPlugin as plugin, @@ -42,6 +43,7 @@ export { OwnerPicker, RepoUrlPicker, TextValuePicker, + OwnedEntityPicker, } from './components/fields'; export { FavouriteTemplate } from './components/FavouriteTemplate'; export { TemplateList } from './components/TemplateList'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index b6ef0ebe17..95cad50eba 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -35,6 +35,7 @@ import { discoveryApiRef, identityApiRef, } from '@backstage/core-plugin-api'; +import { OwnedEntityPicker } from './components/fields/OwnedEntityPicker'; export const scaffolderPlugin = createPlugin({ id: 'scaffolder', @@ -95,3 +96,10 @@ export const ScaffolderPage = scaffolderPlugin.provide( mountPoint: rootRouteRef, }), ); + +export const OwnedEntityPickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: OwnedEntityPicker, + name: 'OwnedEntityPicker', + }), +); diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index f36078c503..52ca5efaff 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 0.0.7 + +### Patch Changes + +- 68512f5178: Add `newClient()` method to re-use the configuration of the elastic search + engine with custom clients + ## 0.0.6 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 1e798119c3..6dc0256586 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -3,18 +3,103 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts -import { Client } from '@elastic/elasticsearch'; +/// + import { Config } from '@backstage/config'; +import type { ConnectionOptions } from 'tls'; import { IndexableDocument } from '@backstage/search-common'; import { Logger as Logger_2 } from 'winston'; import { SearchEngine } from '@backstage/search-common'; import { SearchQuery } from '@backstage/search-common'; import { SearchResultSet } from '@backstage/search-common'; +// Warning: (ae-missing-release-tag) "ElasticSearchClientOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-unresolved-link) The @link reference could not be resolved: The package "@backstage/plugin-search-backend-module-elasticsearch" does not have an export "ElasticSearchEngine" +// +// @public +export interface ElasticSearchClientOptions { + // Warning: (ae-forgotten-export) The symbol "ElasticSearchAgentOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false; + // Warning: (ae-forgotten-export) The symbol "ElasticSearchAuth" needs to be exported by the entry point index.d.ts + // + // (undocumented) + auth?: ElasticSearchAuth; + // (undocumented) + cloud?: { + id: string; + username?: string; + password?: string; + }; + // (undocumented) + compression?: 'gzip'; + // Warning: (ae-forgotten-export) The symbol "ElasticSearchConnectionConstructor" needs to be exported by the entry point index.d.ts + // + // (undocumented) + Connection?: ElasticSearchConnectionConstructor; + // (undocumented) + disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor'; + // (undocumented) + enableMetaHeader?: boolean; + // (undocumented) + headers?: Record; + // (undocumented) + maxRetries?: number; + // (undocumented) + name?: string | symbol; + // Warning: (ae-forgotten-export) The symbol "ElasticSearchNodeOptions" needs to be exported by the entry point index.d.ts + // + // (undocumented) + node?: + | string + | string[] + | ElasticSearchNodeOptions + | ElasticSearchNodeOptions[]; + // (undocumented) + nodeFilter?: (connection: any) => boolean; + // (undocumented) + nodes?: + | string + | string[] + | ElasticSearchNodeOptions + | ElasticSearchNodeOptions[]; + // (undocumented) + nodeSelector?: ((connections: any[]) => any) | string; + // (undocumented) + opaqueIdPrefix?: string; + // (undocumented) + pingTimeout?: number; + // (undocumented) + provider?: 'aws' | 'elastic'; + // (undocumented) + proxy?: string | URL; + // (undocumented) + requestTimeout?: number; + // (undocumented) + resurrectStrategy?: 'ping' | 'optimistic' | 'none'; + // (undocumented) + sniffEndpoint?: string; + // (undocumented) + sniffInterval?: number | boolean; + // (undocumented) + sniffOnConnectionFault?: boolean; + // (undocumented) + sniffOnStart?: boolean; + // (undocumented) + ssl?: ConnectionOptions; + // (undocumented) + suggestCompression?: boolean; + // Warning: (ae-forgotten-export) The symbol "ElasticSearchTransportConstructor" needs to be exported by the entry point index.d.ts + // + // (undocumented) + Transport?: ElasticSearchTransportConstructor; +} + // @public (undocumented) export class ElasticSearchSearchEngine implements SearchEngine { constructor( - elasticSearchClient: Client, + elasticSearchClientOptions: ElasticSearchClientOptions, aliasPostfix: string, indexPrefix: string, logger: Logger_2, @@ -22,11 +107,15 @@ export class ElasticSearchSearchEngine implements SearchEngine { // Warning: (ae-forgotten-export) The symbol "ElasticSearchOptions" needs to be exported by the entry point index.d.ts // // (undocumented) - static fromConfig( - options: ElasticSearchOptions, - ): Promise; + static fromConfig({ + logger, + config, + aliasPostfix, + indexPrefix, + }: ElasticSearchOptions): Promise; // (undocumented) index(type: string, documents: IndexableDocument[]): Promise; + newClient(create: (options: ElasticSearchClientOptions) => T): T; // (undocumented) query(query: SearchQuery): Promise; // Warning: (ae-forgotten-export) The symbol "ElasticSearchQueryTranslator" needs to be exported by the entry point index.d.ts diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 633af83ca6..0873fb24ac 100644 --- a/plugins/search-backend-module-elasticsearch/package.json +++ b/plugins/search-backend-module-elasticsearch/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-elasticsearch", "description": "A module for the search backend that implements search using ElasticSearch", - "version": "0.0.6", + "version": "0.0.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/backend-common": "^0.9.13", - "@backstage/cli": "^0.10.1", + "@backstage/backend-common": "^0.9.14", + "@backstage/cli": "^0.10.2", "@elastic/elasticsearch-mock": "^0.3.0" }, "files": [ diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientOptions.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientOptions.ts new file mode 100644 index 0000000000..73cb149c0a --- /dev/null +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchClientOptions.ts @@ -0,0 +1,125 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { ConnectionOptions as TLSConnectionOptions } from 'tls'; + +/** + * Options used to configure the `@elastic/elasticsearch` client and + * are what will be passed as an argument to the + * {@link ElasticSearchEngine.newClient} method + * + * They are drawn from the `ClientOptions` class of `@elastic/elasticsearch`, + * but are maintained separately so that this interface is not coupled to + */ +export interface ElasticSearchClientOptions { + provider?: 'aws' | 'elastic'; + node?: + | string + | string[] + | ElasticSearchNodeOptions + | ElasticSearchNodeOptions[]; + nodes?: + | string + | string[] + | ElasticSearchNodeOptions + | ElasticSearchNodeOptions[]; + Transport?: ElasticSearchTransportConstructor; + Connection?: ElasticSearchConnectionConstructor; + maxRetries?: number; + requestTimeout?: number; + pingTimeout?: number; + sniffInterval?: number | boolean; + sniffOnStart?: boolean; + sniffEndpoint?: string; + sniffOnConnectionFault?: boolean; + resurrectStrategy?: 'ping' | 'optimistic' | 'none'; + suggestCompression?: boolean; + compression?: 'gzip'; + ssl?: TLSConnectionOptions; + agent?: ElasticSearchAgentOptions | ((opts?: any) => unknown) | false; + nodeFilter?: (connection: any) => boolean; + nodeSelector?: ((connections: any[]) => any) | string; + headers?: Record; + opaqueIdPrefix?: string; + name?: string | symbol; + auth?: ElasticSearchAuth; + proxy?: string | URL; + enableMetaHeader?: boolean; + cloud?: { + id: string; + username?: string; + password?: string; + }; + disablePrototypePoisoningProtection?: boolean | 'proto' | 'constructor'; +} + +export type ElasticSearchAuth = + | { + username: string; + password: string; + } + | { + apiKey: + | string + | { + id: string; + api_key: string; + }; + }; + +export interface ElasticSearchNodeOptions { + url: URL; + id?: string; + agent?: ElasticSearchAgentOptions; + ssl?: TLSConnectionOptions; + headers?: Record; + roles?: { + master: boolean; + data: boolean; + ingest: boolean; + ml: boolean; + }; +} + +export interface ElasticSearchAgentOptions { + keepAlive?: boolean; + keepAliveMsecs?: number; + maxSockets?: number; + maxFreeSockets?: number; +} + +export interface ElasticSearchConnectionConstructor { + new (opts?: any): any; + statuses: { + ALIVE: string; + DEAD: string; + }; + roles: { + MASTER: string; + DATA: string; + INGEST: string; + ML: string; + }; +} + +export interface ElasticSearchTransportConstructor { + new (opts?: any): any; + sniffReasons: { + SNIFF_ON_START: string; + SNIFF_INTERVAL: string; + SNIFF_ON_CONNECTION_FAULT: string; + DEFAULT: string; + }; +} diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts index 2877632013..1566a35b57 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.test.ts @@ -15,7 +15,6 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { SearchEngine } from '@backstage/search-common'; import { Client } from '@elastic/elasticsearch'; import Mock from '@elastic/elasticsearch-mock'; import { @@ -33,28 +32,31 @@ class ElasticSearchSearchEngineForTranslatorTests extends ElasticSearchSearchEng } const mock = new Mock(); -const client = new Client({ +const options = { node: 'http://localhost:9200', Connection: mock.getConnection(), -}); +}; describe('ElasticSearchSearchEngine', () => { - let testSearchEngine: SearchEngine; + let testSearchEngine: ElasticSearchSearchEngine; let inspectableSearchEngine: ElasticSearchSearchEngineForTranslatorTests; + let client: Client; beforeEach(() => { testSearchEngine = new ElasticSearchSearchEngine( - client, + options, 'search', '', getVoidLogger(), ); inspectableSearchEngine = new ElasticSearchSearchEngineForTranslatorTests( - client, + options, 'search', '', getVoidLogger(), ); + // eslint-disable-next-line dot-notation + client = testSearchEngine['elasticSearchClient']; }); describe('queryTranslator', () => { diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 7912c691b3..48caf0d4f1 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -30,6 +30,10 @@ import esb from 'elastic-builder'; import { isEmpty, isNaN as nan, isNumber } from 'lodash'; import { Logger } from 'winston'; +import type { ElasticSearchClientOptions } from './ElasticSearchClientOptions'; + +export type { ElasticSearchClientOptions }; + export type ConcreteElasticSearchQuery = { documentTypes?: string[]; elasticSearchQuery: Object; @@ -68,105 +72,49 @@ function isBlank(str: string) { * @public */ export class ElasticSearchSearchEngine implements SearchEngine { + private readonly elasticSearchClient: Client; + constructor( - private readonly elasticSearchClient: Client, + private readonly elasticSearchClientOptions: ElasticSearchClientOptions, private readonly aliasPostfix: string, private readonly indexPrefix: string, private readonly logger: Logger, - ) {} + ) { + this.elasticSearchClient = this.newClient(options => new Client(options)); + } - static async fromConfig(options: ElasticSearchOptions) { - const { - logger, - config, - aliasPostfix = `search`, - indexPrefix = ``, - } = options; + static async fromConfig({ + logger, + config, + aliasPostfix = `search`, + indexPrefix = ``, + }: ElasticSearchOptions) { + const options = await createElasticSearchClientOptions( + config.getConfig('search.elasticsearch'), + ); + if (options.provider === 'elastic') { + logger.info('Initializing Elastic.co ElasticSearch search engine.'); + } else if (options.provider === 'aws') { + logger.info('Initializing AWS ElasticSearch search engine.'); + } else { + logger.info('Initializing ElasticSearch search engine.'); + } return new ElasticSearchSearchEngine( - await ElasticSearchSearchEngine.constructElasticSearchClient( - logger, - config.getConfig('search.elasticsearch'), - ), + options, aliasPostfix, indexPrefix, logger, ); } - private static async constructElasticSearchClient( - logger: Logger, - config?: Config, - ) { - if (!config) { - throw new Error('No elastic search config found'); - } - - const clientOptionsConfig = config.getOptionalConfig('clientOptions'); - const sslConfig = clientOptionsConfig?.getOptionalConfig('ssl'); - - if (config.getOptionalString('provider') === 'elastic') { - logger.info('Initializing Elastic.co ElasticSearch search engine.'); - const authConfig = config.getConfig('auth'); - return new Client({ - cloud: { - id: config.getString('cloudId'), - }, - auth: { - username: authConfig.getString('username'), - password: authConfig.getString('password'), - }, - ...(sslConfig - ? { - ssl: { - rejectUnauthorized: - sslConfig?.getOptionalBoolean('rejectUnauthorized'), - }, - } - : {}), - }); - } - if (config.getOptionalString('provider') === 'aws') { - logger.info('Initializing AWS ElasticSearch search engine.'); - const awsCredentials = await awsGetCredentials(); - const AWSConnection = createAWSConnection(awsCredentials); - return new Client({ - node: config.getString('node'), - ...AWSConnection, - ...(sslConfig - ? { - ssl: { - rejectUnauthorized: - sslConfig?.getOptionalBoolean('rejectUnauthorized'), - }, - } - : {}), - }); - } - logger.info('Initializing ElasticSearch search engine.'); - const authConfig = config.getOptionalConfig('auth'); - const auth = - authConfig && - (authConfig.has('apiKey') - ? { - apiKey: authConfig.getString('apiKey'), - } - : { - username: authConfig.getString('username'), - password: authConfig.getString('password'), - }); - return new Client({ - node: config.getString('node'), - auth, - ...(sslConfig - ? { - ssl: { - rejectUnauthorized: - sslConfig?.getOptionalBoolean('rejectUnauthorized'), - }, - } - : {}), - }); + /** + * Create a custom search client from the derived elastic search + * configuration. This need not be the same client that the engine uses + * internally. + */ + newClient(create: (options: ElasticSearchClientOptions) => T): T { + return create(this.elasticSearchClientOptions); } protected translator(query: SearchQuery): ConcreteElasticSearchQuery { @@ -349,3 +297,75 @@ export function decodePageCursor(pageCursor?: string): { page: number } { export function encodePageCursor({ page }: { page: number }): string { return Buffer.from(`${page}`, 'utf-8').toString('base64'); } + +async function createElasticSearchClientOptions( + config?: Config, +): Promise { + if (!config) { + throw new Error('No elastic search config found'); + } + const clientOptionsConfig = config.getOptionalConfig('clientOptions'); + const sslConfig = clientOptionsConfig?.getOptionalConfig('ssl'); + + if (config.getOptionalString('provider') === 'elastic') { + const authConfig = config.getConfig('auth'); + return { + provider: 'elastic', + cloud: { + id: config.getString('cloudId'), + }, + auth: { + username: authConfig.getString('username'), + password: authConfig.getString('password'), + }, + ...(sslConfig + ? { + ssl: { + rejectUnauthorized: + sslConfig?.getOptionalBoolean('rejectUnauthorized'), + }, + } + : {}), + }; + } + if (config.getOptionalString('provider') === 'aws') { + const awsCredentials = await awsGetCredentials(); + const AWSConnection = createAWSConnection(awsCredentials); + return { + provider: 'aws', + node: config.getString('node'), + ...AWSConnection, + ...(sslConfig + ? { + ssl: { + rejectUnauthorized: + sslConfig?.getOptionalBoolean('rejectUnauthorized'), + }, + } + : {}), + }; + } + const authConfig = config.getOptionalConfig('auth'); + const auth = + authConfig && + (authConfig.has('apiKey') + ? { + apiKey: authConfig.getString('apiKey'), + } + : { + username: authConfig.getString('username'), + password: authConfig.getString('password'), + }); + return { + node: config.getString('node'), + auth, + ...(sslConfig + ? { + ssl: { + rejectUnauthorized: + sslConfig?.getOptionalBoolean('rejectUnauthorized'), + }, + } + : {}), + }; +} diff --git a/plugins/search-backend-module-elasticsearch/src/engines/index.ts b/plugins/search-backend-module-elasticsearch/src/engines/index.ts index 2f3641c114..d5eee37803 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/index.ts @@ -15,4 +15,7 @@ */ export { ElasticSearchSearchEngine } from './ElasticSearchSearchEngine'; -export type { ConcreteElasticSearchQuery } from './ElasticSearchSearchEngine'; +export type { + ConcreteElasticSearchQuery, + ElasticSearchClientOptions, +} from './ElasticSearchSearchEngine'; diff --git a/plugins/search-backend-module-elasticsearch/src/index.ts b/plugins/search-backend-module-elasticsearch/src/index.ts index b4dab93b76..8cf96de858 100644 --- a/plugins/search-backend-module-elasticsearch/src/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/index.ts @@ -21,3 +21,4 @@ */ export { ElasticSearchSearchEngine } from './engines'; +export type { ElasticSearchClientOptions } from './engines'; diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 4b3cb30264..77a1be1eb4 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -18,6 +18,7 @@ import { getVoidLogger } from '@backstage/backend-common'; import { IndexBuilder, LunrSearchEngine, + SearchEngine, } from '@backstage/plugin-search-backend-node'; import express from 'express'; import request from 'supertest'; @@ -26,11 +27,13 @@ import { createRouter } from './router'; describe('createRouter', () => { let app: express.Express; + let mockSearchEngine: jest.Mocked; beforeAll(async () => { const logger = getVoidLogger(); const searchEngine = new LunrSearchEngine({ logger }); const indexBuilder = new IndexBuilder({ logger, searchEngine }); + const router = await createRouter({ engine: indexBuilder.getSearchEngine(), logger, @@ -49,5 +52,63 @@ describe('createRouter', () => { expect(response.status).toEqual(200); expect(response.body).toMatchObject({ results: [] }); }); + + describe('search result filtering', () => { + beforeAll(async () => { + const logger = getVoidLogger(); + mockSearchEngine = { + index: jest.fn(), + setTranslator: jest.fn(), + query: jest.fn(), + }; + const indexBuilder = new IndexBuilder({ + logger, + searchEngine: mockSearchEngine, + }); + + const router = await createRouter({ + engine: indexBuilder.getSearchEngine(), + logger, + }); + app = express().use(router); + }); + + describe('where the search result set includes unsafe results', () => { + const safeResult = { + type: 'software-catalog', + document: { + text: 'safe', + title: 'safe-location', + // eslint-disable-next-line no-script-url + location: '/catalog/default/component/safe', + }, + }; + beforeEach(() => { + mockSearchEngine.query.mockResolvedValue({ + results: [ + { + type: 'software-catalog', + document: { + text: 'unsafe', + title: 'unsafe-location', + // eslint-disable-next-line no-script-url + location: 'javascript:alert("unsafe")', + }, + }, + safeResult, + ], + nextPageCursor: '', + previousPageCursor: '', + }); + }); + + it('removes the unsafe results', async () => { + const response = await request(app).get('/query'); + + expect(response.status).toEqual(200); + expect(response.body).toMatchObject({ results: [safeResult] }); + }); + }); + }); }); }); diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 5bd99988a7..aae1914fc1 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -25,10 +25,28 @@ export type RouterOptions = { logger: Logger; }; +const allowedLocationProtocols = ['http:', 'https:']; + export async function createRouter( options: RouterOptions, ): Promise { const { engine, logger } = options; + + const filterResultSet = ({ results, ...resultSet }: SearchResultSet) => ({ + ...resultSet, + results: results.filter(result => { + const protocol = new URL(result.document.location, 'https://example.com') + .protocol; + const isAllowed = allowedLocationProtocols.includes(protocol); + if (!isAllowed) { + logger.info( + `Rejected search result for "${result.document.title}" as location protocol "${protocol}" is unsafe`, + ); + } + return isAllowed; + }), + }); + const router = Router(); router.get( '/query', @@ -46,8 +64,8 @@ export async function createRouter( ); try { - const results = await engine?.query(req.query); - res.send(results); + const resultSet = await engine?.query(req.query); + res.send(filterResultSet(resultSet)); } catch (err) { throw new Error( `There was a problem performing the search query. ${err}`, diff --git a/plugins/search/package.json b/plugins/search/package.json index dbef8e5608..85996d65c3 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -32,8 +32,8 @@ "dependencies": { "@backstage/catalog-model": "^0.9.7", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.4", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/search-common": "^0.2.1", @@ -52,8 +52,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 41dc522e77..0f41bfaf16 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -33,8 +33,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -49,8 +49,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index a706d43e0e..a03dde616b 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", @@ -39,8 +39,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 451ec86659..d1daf47b67 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -34,8 +34,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -50,8 +50,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index e5b55836a4..faa49a962e 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -48,8 +48,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index 16e1ec6784..9466433b8e 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -20,26 +20,26 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/catalog-model": "^0.9.7", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", + "@backstage/errors": "^0.1.4", + "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-tech-insights-common": "^0.2.0", "@backstage/theme": "^0.2.14", + "@backstage/types": "^0.1.1", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", - "react-use": "^17.2.4", "react-router-dom": "6.0.0-beta.0", - "@backstage/plugin-catalog-react": "^0.6.5", - "@backstage/plugin-tech-insights-common": "^0.2.0", - "@backstage/catalog-model": "^0.9.7", - "@backstage/errors": "^0.1.4", - "@backstage/types": "^0.1.1" + "react-use": "^17.2.4" }, "peerDependencies": { "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", @@ -47,8 +47,8 @@ "@testing-library/user-event": "^13.1.8", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "msw": "^0.35.0", - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.0.6", + "msw": "^0.35.0" }, "files": [ "dist" diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 7d6fdb4afe..b68a05c546 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -31,8 +31,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -46,8 +46,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 5cab32a73f..d12e2df366 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs +## 0.12.10 + +### Patch Changes + +- e7cce2b603: Fix issue where assets weren't being fetched from the correct URL path for doc URLs without trailing slashes +- Updated dependencies + - @backstage/core-plugin-api@0.3.1 + - @backstage/core-components@0.8.1 + - @backstage/catalog-model@0.9.8 + - @backstage/plugin-catalog-react@0.6.7 + ## 0.12.9 ### Patch Changes diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 4ac62a2b56..f9e3db913b 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs", "description": "The Backstage plugin that renders technical documentation for your components", - "version": "0.12.9", + "version": "0.12.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,15 +32,15 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.9.7", + "@backstage/catalog-model": "^0.9.8", "@backstage/config": "^0.1.11", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.5", "@backstage/integration": "^0.6.10", "@backstage/integration-react": "^0.1.15", "@backstage/plugin-catalog": "^0.7.4", - "@backstage/plugin-catalog-react": "^0.6.5", + "@backstage/plugin-catalog-react": "^0.6.7", "@backstage/plugin-search": "^0.5.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -62,8 +62,8 @@ "react-dom": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/techdocs/src/client.test.ts b/plugins/techdocs/src/client.test.ts index bdb2f74772..d1a421c69f 100644 --- a/plugins/techdocs/src/client.test.ts +++ b/plugins/techdocs/src/client.test.ts @@ -62,6 +62,12 @@ describe('TechDocsStorageClient', () => { ).resolves.toEqual( `${mockBaseUrl}/static/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test.js`, ); + + await expect( + storageApi.getBaseUrl('../test.js', mockEntity, 'some-docs-path'), + ).resolves.toEqual( + `${mockBaseUrl}/static/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test.js`, + ); }); it('should return base url with correct entity structure', async () => { diff --git a/plugins/techdocs/src/client.ts b/plugins/techdocs/src/client.ts index 8a3ad59c03..f965cd7ced 100644 --- a/plugins/techdocs/src/client.ts +++ b/plugins/techdocs/src/client.ts @@ -263,9 +263,11 @@ export class TechDocsStorageClient implements TechDocsStorageApi { const { kind, namespace, name } = entityId; const apiOrigin = await this.getApiOrigin(); + const newBaseUrl = `${apiOrigin}/static/docs/${namespace}/${kind}/${name}/${path}`; + return new URL( oldBaseUrl, - `${apiOrigin}/static/docs/${namespace}/${kind}/${name}/${path}`, + newBaseUrl.endsWith('/') ? newBaseUrl : `${newBaseUrl}/`, ).toString(); } } diff --git a/plugins/todo/package.json b/plugins/todo/package.json index 10f2b311ca..deef4ac7f2 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -28,8 +28,8 @@ }, "dependencies": { "@backstage/catalog-model": "^0.9.7", - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.3", "@backstage/plugin-catalog-react": "^0.6.5", "@backstage/theme": "^0.2.14", @@ -42,8 +42,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/user-settings/api-report.md b/plugins/user-settings/api-report.md index a801f2e807..0d8f132f37 100644 --- a/plugins/user-settings/api-report.md +++ b/plugins/user-settings/api-report.md @@ -119,5 +119,6 @@ export const UserSettingsThemeToggle: () => JSX.Element; export const useUserProfile: () => { profile: ProfileInfo; displayName: string; + loading: boolean; }; ``` diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index aaef5eb0a3..6d9f4bac21 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", @@ -44,8 +44,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/user-settings/src/components/useUserProfileInfo.ts b/plugins/user-settings/src/components/useUserProfileInfo.ts index 336ea34ee4..6f9b5c154e 100644 --- a/plugins/user-settings/src/components/useUserProfileInfo.ts +++ b/plugins/user-settings/src/components/useUserProfileInfo.ts @@ -14,13 +14,46 @@ * limitations under the License. */ -import { useApi, identityApiRef } from '@backstage/core-plugin-api'; +import { + alertApiRef, + identityApiRef, + ProfileInfo, + useApi, +} from '@backstage/core-plugin-api'; +import { useEffect } from 'react'; +import { useAsync } from 'react-use'; export const useUserProfile = () => { const identityApi = useApi(identityApiRef); - const userId = identityApi.getUserId(); - const profile = identityApi.getProfile(); - const displayName = profile.displayName ?? userId; + const alertApi = useApi(alertApiRef); - return { profile, displayName }; + const { value, loading, error } = useAsync(async () => { + return { + profile: await identityApi.getProfileInfo(), + identity: await identityApi.getBackstageIdentity(), + }; + }, []); + + useEffect(() => { + if (error) { + alertApi.post({ + message: `Failed to load user identity: ${error}`, + severity: 'error', + }); + } + }, [error, alertApi]); + + if (loading || error) { + return { + profile: {} as ProfileInfo, + displayName: '', + loading, + }; + } + + return { + profile: value!.profile, + displayName: value!.profile.displayName ?? value!.identity.userEntityRef, + loading, + }; }; diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index 55488aa8f4..26fcc28f9f 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -21,8 +21,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-components": "^0.8.0", - "@backstage/core-plugin-api": "^0.3.0", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", "@backstage/errors": "^0.1.3", "@backstage/theme": "^0.2.14", "@material-ui/core": "^4.12.2", @@ -37,8 +37,8 @@ "react": "^16.13.1 || ^17.0.0" }, "devDependencies": { - "@backstage/cli": "^0.10.1", - "@backstage/core-app-api": "^0.2.0", + "@backstage/cli": "^0.10.2", + "@backstage/core-app-api": "^0.2.1", "@backstage/dev-utils": "^0.2.14", "@backstage/test-utils": "^0.1.24", "@testing-library/jest-dom": "^5.10.1", diff --git a/scripts/api-extractor.ts b/scripts/api-extractor.ts index 8ca7743cf2..22eae267ee 100644 --- a/scripts/api-extractor.ts +++ b/scripts/api-extractor.ts @@ -35,7 +35,6 @@ import { import { Program } from 'typescript'; import { DocNode, - DocSection, IDocNodeContainerParameters, TSDocTagSyntaxKind, } from '@microsoft/tsdoc'; @@ -51,7 +50,6 @@ import { DocHeading } from '@microsoft/api-documenter/lib/nodes/DocHeading'; import { CustomMarkdownEmitter } from '@microsoft/api-documenter/lib/markdown/CustomMarkdownEmitter'; import { IMarkdownEmitterContext } from '@microsoft/api-documenter/lib/markdown/MarkdownEmitter'; import { AstDeclaration } from '@microsoft/api-extractor/lib/analyzer/AstDeclaration'; -import { DocTableCell } from '@microsoft/api-documenter/lib/nodes/DocTableCell'; const tmpDir = resolvePath(__dirname, '../node_modules/.cache/api-extractor'); @@ -605,18 +603,9 @@ async function buildDocs({ }); for (const apiMember of apiModel.members) { - // This is a workaround for this check failing: https://github.com/microsoft/rushstack/blob/915aca8d8847b65981892f44f0544ccb00752792/apps/api-documenter/src/documenters/MarkdownDocumenter.ts#L991 - const description = new DocSection({ configuration }); - if (apiMember.tsdocComment !== undefined) { - this._appendAndMergeSection( - description, - apiMember.tsdocComment.summarySection, - ); - } - const row = new DocTableRow({ configuration }, [ this._createTitleCell(apiMember), - new DocTableCell({ configuration }, description.nodes), + this._createDescriptionCell(apiMember), ]); if (apiMember.kind === 'Package') { diff --git a/scripts/list-deprecations.js b/scripts/list-deprecations.js index 920b01c7b5..9a159927fc 100755 --- a/scripts/list-deprecations.js +++ b/scripts/list-deprecations.js @@ -129,7 +129,7 @@ async function main() { const srcDir = resolvePath(rootPath, packageDir, 'src'); if (await fs.pathExists(srcDir)) { - const files = await globby(['**/*.{js,jsx,ts,tsx}'], { + const files = await globby(['**/*.{js,jsx,ts,tsx,mjs,cjs}'], { cwd: srcDir, }); fileQueue.push( diff --git a/yarn.lock b/yarn.lock index 7a599498d0..e300785cd4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4846,54 +4846,45 @@ resolved "https://registry.npmjs.org/@mdx-js/util/-/util-1.6.22.tgz#219dfd89ae5b97a8801f015323ffa4b62f45718b" integrity sha512-H1rQc1ZOHANWBvPcW+JpGwr+juXSxM8Q8YCkm3GhZd8REu1fHR3z99CErO1p9pkcfcxZnMdIZdIsXkOHY0NilA== -"@microsoft/api-documenter@^7.13.68": - version "7.13.68" - resolved "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.13.68.tgz#c1e144764cac0684adefe78fd848d78c3f374681" - integrity sha512-cRjwK1TDyGxFGgCsRG8G0Yi3Z4akvfWgw1pWAxKFbm7ajlQQGZcHPnb+n4lKlSeQ5g/cxc7hcdw54Mvisne9Bg== +"@microsoft/api-documenter@^7.13.77": + version "7.13.77" + resolved "https://registry.npmjs.org/@microsoft/api-documenter/-/api-documenter-7.13.77.tgz#9760b42553679695620b578436ce14c357454e88" + integrity sha512-kot4VJlQUd7i0mFSM7IjyOhF0WWyVrmN9x3sXmd72imgCg9w795QHSkphJ7Qc6ZjhHcolaJvBByEPTKG4exZmA== dependencies: - "@microsoft/api-extractor-model" "7.13.16" + "@microsoft/api-extractor-model" "7.15.1" "@microsoft/tsdoc" "0.13.2" - "@rushstack/node-core-library" "3.43.2" - "@rushstack/ts-command-line" "4.10.4" + "@rushstack/node-core-library" "3.44.2" + "@rushstack/ts-command-line" "4.10.5" colors "~1.2.1" js-yaml "~3.13.1" resolve "~1.17.0" -"@microsoft/api-extractor-model@7.13.16": - version "7.13.16" - resolved "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.16.tgz#1d67541ebbcea32672c5fdd9392dc1579b2fc23a" - integrity sha512-ttdxVXsTWL5dd26W1YNLe3LgDsE0EE273aZlcLe58W0opymBybCYU1Mn+OHQM8BuErrdvdN8LdpWAAbkiOEN/Q== +"@microsoft/api-extractor-model@7.15.1", "@microsoft/api-extractor-model@^7.15.1": + version "7.15.1" + resolved "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.15.1.tgz#e52d68676e846d8b86e3b18e2654af39fba6e4a1" + integrity sha512-DWfS1o3oMY0mzdO3OuQbD/9vzn80jwM6tFd7XbiYnkpxwhD83LMGXz7NZWwSh+IaA+9w3LF4w62fT31Qq+dAMw== dependencies: "@microsoft/tsdoc" "0.13.2" "@microsoft/tsdoc-config" "~0.15.2" - "@rushstack/node-core-library" "3.43.2" + "@rushstack/node-core-library" "3.44.2" -"@microsoft/api-extractor-model@7.13.5", "@microsoft/api-extractor-model@^7.13.5": - version "7.13.5" - resolved "https://registry.npmjs.org/@microsoft/api-extractor-model/-/api-extractor-model-7.13.5.tgz#7836a81ba47b9a654062ed0361e4eee69afae51e" - integrity sha512-il6AebNltYo5hEtqXZw4DMvrwBPn6+F58TxwqmsLY+U+sSJNxaYn2jYksArrjErXVPR3gUgRMqD6zsdIkg+WEQ== +"@microsoft/api-extractor@^7.19.2": + version "7.19.2" + resolved "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.19.2.tgz#8c546003523163c1432f6e19506065f790d6182c" + integrity sha512-LxSa9lwp7eYtM4i5y/1n79QpotPKlmpCrVQbkb0LAHE1sCRHpZDTb6p3cMJthDhYPMjAYKOLfq639GwtZrg23Q== dependencies: + "@microsoft/api-extractor-model" "7.15.1" "@microsoft/tsdoc" "0.13.2" "@microsoft/tsdoc-config" "~0.15.2" - "@rushstack/node-core-library" "3.40.0" - -"@microsoft/api-extractor@^7.18.7": - version "7.18.7" - resolved "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.18.7.tgz#851d2413a3c5d696f7cc914eb59de7a7882b2e8b" - integrity sha512-JhtV8LoyLuIecbgCPyZQg08G1kngIRWpai2UzwNil9mGVGYiDZVeeKx8c2phmlPcogmMDm4oQROxyuiYt5sJiw== - dependencies: - "@microsoft/api-extractor-model" "7.13.5" - "@microsoft/tsdoc" "0.13.2" - "@microsoft/tsdoc-config" "~0.15.2" - "@rushstack/node-core-library" "3.40.0" - "@rushstack/rig-package" "0.3.0" - "@rushstack/ts-command-line" "4.9.0" + "@rushstack/node-core-library" "3.44.2" + "@rushstack/rig-package" "0.3.6" + "@rushstack/ts-command-line" "4.10.5" colors "~1.2.1" lodash "~4.17.15" resolve "~1.17.0" semver "~7.3.0" source-map "~0.6.1" - typescript "~4.3.5" + typescript "~4.5.2" "@microsoft/fetch-event-source@2.0.1": version "2.0.1" @@ -5649,25 +5640,10 @@ estree-walker "^2.0.1" picomatch "^2.2.2" -"@rushstack/node-core-library@3.40.0": - version "3.40.0" - resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.40.0.tgz#2551915ea34e34ec2abb7172b9d7f4546144d9d4" - integrity sha512-P6uMPI7cqTdawLSPAG5BQrBu1MHlGRPqecp7ruIRgyukIEzkmh0QAnje4jAL/l1r3hw0qe4e+Dz5ZSnukT/Egg== - dependencies: - "@types/node" "10.17.13" - colors "~1.2.1" - fs-extra "~7.0.1" - import-lazy "~4.0.0" - jju "~1.4.0" - resolve "~1.17.0" - semver "~7.3.0" - timsort "~0.3.0" - z-schema "~3.18.3" - -"@rushstack/node-core-library@3.43.2": - version "3.43.2" - resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.43.2.tgz#f067371a94fd92ed8f9d9aa8201c5e9e17a19f0f" - integrity sha512-b7AEhSf6CvZgvuDcWMFDeKx2mQSn9AVnMQVyxNxFeHCtLz3gJicqCOlw2GOXM8HKh6PInLdil/NVCDcstwSrIw== +"@rushstack/node-core-library@3.44.2": + version "3.44.2" + resolved "https://registry.npmjs.org/@rushstack/node-core-library/-/node-core-library-3.44.2.tgz#4fcd8f76887ae5968f2796f633a7e48f5ebd2271" + integrity sha512-lQ8Ct267UKkNSJSDxpBWn7SyyITWQ9l3Xqww0V+YY0rMt02r9eiGvwwPaU1ugJW7IMVo6r/HXvgbmpOSPyzGyg== dependencies: "@types/node" "12.20.24" colors "~1.2.1" @@ -5677,30 +5653,20 @@ resolve "~1.17.0" semver "~7.3.0" timsort "~0.3.0" - z-schema "~3.18.3" + z-schema "~5.0.2" -"@rushstack/rig-package@0.3.0": - version "0.3.0" - resolved "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.0.tgz#334ad2846797861361b3445d4cc9ae9164b1885c" - integrity sha512-Lj6noF7Q4BBm1hKiBDw94e6uZvq1xlBwM/d2cBFaPqXeGdV+G6r3qaCWfRiSXK0pcHpGGpV5Tb2MdfhVcO6G/g== +"@rushstack/rig-package@0.3.6": + version "0.3.6" + resolved "https://registry.npmjs.org/@rushstack/rig-package/-/rig-package-0.3.6.tgz#a57b53db59106fb93bcda36cad4f8602f508ebc6" + integrity sha512-H/uFsAT6cD4JCYrlQXYMZg+wPVECByFoJLGqfGRiTwSS5ngQw9QxnFV2mPG2LrxFUsMjLQ2lsrYr523700XzfA== dependencies: resolve "~1.17.0" strip-json-comments "~3.1.1" -"@rushstack/ts-command-line@4.10.4": - version "4.10.4" - resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.4.tgz#05142b74e5cb207d3dd9b935c82f80d7fcb68042" - integrity sha512-4T5ao4UgDb6LmiRj4GumvG3VT/p6RSMgl7TN7S58ifaAGN2GeTNBajFCDdJs9QQP0d/4tA5p0SFzT7Ps5Byirg== - dependencies: - "@types/argparse" "1.0.38" - argparse "~1.0.9" - colors "~1.2.1" - string-argv "~0.3.1" - -"@rushstack/ts-command-line@4.9.0": - version "4.9.0" - resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.9.0.tgz#781ba42cff73cae097b6d5241b6441e7cc2fe6e0" - integrity sha512-kmT8t+JfnvphISF1C5WwY56RefjwgajhSjs9J4ckvAFXZDXR6F5cvF5/RTh7fGCzIomg8esy2PHO/b52zFoZvA== +"@rushstack/ts-command-line@4.10.5": + version "4.10.5" + resolved "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.10.5.tgz#a31a44ddd24fe3a594e4ad91c22f3ea7668b43a9" + integrity sha512-5fVlTDbKsJ5WyT6L7NrnOlLG3uoITKxoqTPP2j0QZEi95kPbVT4+VPZaXXDJtkrao9qrIyig8pLK9WABY1bb3w== dependencies: "@types/argparse" "1.0.38" argparse "~1.0.9" @@ -7327,9 +7293,9 @@ "@types/d3-selection" "*" "@types/dagre@^0.7.44": - version "0.7.44" - resolved "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.44.tgz#8f4b796b118ca29c132da7068fbc0d0351ee5851" - integrity sha512-N6HD+79w77ZVAaVO7JJDW5yJ9LAxM62FpgNGO9xEde+KVYjDRyhIMzfiErXpr1g0JPon9kwlBzoBK6s4fOww9Q== + version "0.7.46" + resolved "https://registry.npmjs.org/@types/dagre/-/dagre-0.7.46.tgz#c51ffcc62ca8a9affa07f5e06842acd840d3d442" + integrity sha512-ku3y+F8sPqmiB5Ugl22NpukI2dLAViJiWwdtueXLeuF4fxZozl+bytPSFVlLu/gDgaKiwobu3LBXXRWktIMiIA== "@types/debug@^4.0.0": version "4.1.7" @@ -7582,14 +7548,6 @@ resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.25.1.tgz#b6140d5fc00ff3917b3f521784abef4bc0387ccc" integrity sha512-WZU/4bb+lvzyDmZzjJtp++9mfKy6B3lH6gGISgkcz6SU8hMILKRM0vi08TxIsb0dQB4Gzo68MWLmctu6xqUi9g== -"@types/inquirer@^7.3.1": - version "7.3.1" - resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" - integrity sha512-osD38QVIfcdgsPCT0V3lD7eH0OFurX71Jft18bZrsVQWVRt6TuxRzlr0GJLrxoHZR2V5ph7/qP8se/dcnI7o0g== - dependencies: - "@types/through" "*" - rxjs "^6.4.0" - "@types/inquirer@^7.3.3": version "7.3.3" resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.3.tgz#92e6676efb67fa6925c69a2ee638f67a822952ac" @@ -7598,6 +7556,14 @@ "@types/through" "*" rxjs "^6.4.0" +"@types/inquirer@^8.1.3": + version "8.1.3" + resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-8.1.3.tgz#dfda4c97cdbe304e4dceb378a80f79448ea5c8fe" + integrity sha512-AayK4ZL5ssPzR1OtnOLGAwpT0Dda3Xi/h1G0l1oJDNrowp7T1423q4Zb8/emr7tzRlCy4ssEri0LWVexAqHyKQ== + dependencies: + "@types/through" "*" + rxjs "^7.2.0" + "@types/is-function@^1.0.0": version "1.0.0" resolved "https://registry.npmjs.org/@types/is-function/-/is-function-1.0.0.tgz#1b0b819b1636c7baf0d6785d030d12edf70c3e83" @@ -7756,9 +7722,9 @@ "@types/node" "*" "@types/ldapjs@^2.2.0": - version "2.2.0" - resolved "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.0.tgz#43ff78668c420b69a61b93e0acde71ddc0c7766d" - integrity sha512-L6ObSryRGOw0qYmN9B4w4s8UkoyRbxoWh7C1vc/Z6O/VXJQJqGBSyZPXRDmgKF+TZVGq/F7H37t0Cxrm+hLiRA== + version "2.2.2" + resolved "https://registry.npmjs.org/@types/ldapjs/-/ldapjs-2.2.2.tgz#cf79510d8dc34e5579442c2743f8a228427eb99c" + integrity sha512-U5HdnwIZ5uZa+f3usxdqgyfNmOROxOxXvQdQtsu6sKo8fte5vej9br2csHxPvXreAbAO1bs8/rdEzvCLpi67nQ== dependencies: "@types/node" "*" @@ -7897,36 +7863,31 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32": - version "14.17.8" - resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" - integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== +"@types/node@*", "@types/node@>= 8", "@types/node@>=12.12.47", "@types/node@>=13.7.0", "@types/node@^16.9.2": + version "16.11.6" + resolved "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" + integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== -"@types/node@10.17.13", "@types/node@^10.1.0", "@types/node@^10.12.0": - version "10.17.13" - resolved "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" - integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== - -"@types/node@12.20.24": +"@types/node@12.20.24", "@types/node@^12.7.1": version "12.20.24" resolved "https://registry.npmjs.org/@types/node/-/node-12.20.24.tgz#c37ac69cb2948afb4cef95f424fa0037971a9a5c" integrity sha512-yxDeaQIAJlMav7fH5AQqPH1u8YIuhYJXYBzxaQ4PifsU0GDO38MSdmEDeRlIxrKbC6NbEaaEHDanWb+y30U8SQ== -"@types/node@^12.7.1": - version "12.12.58" - resolved "https://registry.npmjs.org/@types/node/-/node-12.12.58.tgz#46dae9b2b9ee5992818c8f7cee01ff4ce03ab44c" - integrity sha512-Be46CNIHWAagEfINOjmriSxuv7IVcqbGe+sDSg2SYCEz/0CRBy7LRASGfRbD8KZkqoePU73Wsx3UvOSFcq/9hA== +"@types/node@^10.1.0", "@types/node@^10.12.0": + version "10.17.13" + resolved "https://registry.npmjs.org/@types/node/-/node-10.17.13.tgz#ccebcdb990bd6139cd16e84c39dc2fb1023ca90c" + integrity sha512-pMCcqU2zT4TjqYFrWtYHKal7Sl30Ims6ulZ4UFXxI4xbtQqK/qqKwkDoBFCfooRqqmRu9vY3xaJRwxSh673aYg== + +"@types/node@^14.0.10", "@types/node@^14.14.31", "@types/node@^14.14.32": + version "14.17.8" + resolved "https://registry.npmjs.org/@types/node/-/node-14.17.8.tgz#813b73ab7d82ac06ddfd2458b13c88459a3b319f" + integrity sha512-0CHLt50GbUmH/6MrlBIKNdWCglvlyQKkorRf08/0DIi0ryuTPP+ijWLSI19SbDTHSKaagGDELiImY4BSikt61w== "@types/node@^15.6.1": version "15.14.9" resolved "https://registry.npmjs.org/@types/node/-/node-15.14.9.tgz#bc43c990c3c9be7281868bbc7b8fdd6e2b57adfa" integrity sha512-qjd88DrCxupx/kJD5yQgZdcYKZKSIGBVDIBE1/LTGcNm3d2Np/jxojkdePDdfnBHJc5W7vSMpbJ1aB7p/Py69A== -"@types/node@^16.9.2": - version "16.11.6" - resolved "https://registry.npmjs.org/@types/node/-/node-16.11.6.tgz#6bef7a2a0ad684cf6e90fcfe31cecabd9ce0a3ae" - integrity sha512-ua7PgUoeQFjmWPcoo9khiPum3Pd60k4/2ZGXt18sm2Slk0W0xZTqt5Y0Ny1NyBiN1EVQ/+FaF9NcY4Qe6rwk5w== - "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -8382,6 +8343,14 @@ "@types/minipass" "*" "@types/node" "*" +"@types/tar@^6.1.1": + version "6.1.1" + resolved "https://registry.npmjs.org/@types/tar/-/tar-6.1.1.tgz#ab341ec1f149d7eb2a4f4ded56ff85f0d4fe7cb5" + integrity sha512-8mto3YZfVpqB1CHMaYz1TUYIQfZFbh/QbEq5Hsn6D0ilCfqRVCdalmc89B7vi3jhl9UYIk+dWDABShNfOkv5HA== + dependencies: + "@types/minipass" "*" + "@types/node" "*" + "@types/tern@*": version "0.23.3" resolved "https://registry.npmjs.org/@types/tern/-/tern-0.23.3.tgz#4b54538f04a88c9ff79de1f6f94f575a7f339460" @@ -9749,13 +9718,14 @@ array-unique@^0.3.2: resolved "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz#a894b75d4bc4f6cd679ef3244a9fd8f46ae2d428" integrity sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg= -array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.3: - version "1.2.3" - resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.3.tgz#0de82b426b0318dbfdb940089e38b043d37f6c7b" - integrity sha512-gBlRZV0VSmfPIeWfuuy56XZMvbVfbEUnOXUvt3F/eUUUSyzlgLxhEX4YAEpxNAogRGehPSnfXyPtYyKAhkzQhQ== +array.prototype.flat@^1.2.1, array.prototype.flat@^1.2.5: + version "1.2.5" + resolved "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.5.tgz#07e0975d84bbc7c48cd1879d609e682598d33e13" + integrity sha512-KaYU+S+ndVqyUnignHftkwc58o3uVU1jzczILJ1tN2YaIZpFIKBiP/x/j97E5MVPsaCloPbqWLB/8qCTVvT2qg== dependencies: + call-bind "^1.0.2" define-properties "^1.1.3" - es-abstract "^1.17.0-next.1" + es-abstract "^1.19.0" array.prototype.flatmap@^1.2.1, array.prototype.flatmap@^1.2.4, array.prototype.flatmap@^1.2.5: version "1.2.5" @@ -11931,11 +11901,6 @@ constants-browserify@^1.0.0: resolved "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz#c20b96d8c617748aaf1c16021760cd27fcb8cb75" integrity sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U= -contains-path@^0.1.0: - version "0.1.0" - resolved "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz#fe8cf184ff6670b6baef01a9d4861a5cbec4120a" - integrity sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo= - content-disposition@0.5.2: version "0.5.2" resolved "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" @@ -13034,10 +12999,10 @@ debug@4.3.1: dependencies: ms "2.1.2" -debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6: - version "3.2.6" - resolved "https://registry.npmjs.org/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" - integrity sha512-mel+jf7nrtEl5Pn1Qx46zARXKDpBbvzezse7p7LqINmdoIk8PYP5SySaxEmYv6TZ0JyEKA1hsCId6DIhgITtWQ== +debug@^3.0.0, debug@^3.1.0, debug@^3.1.1, debug@^3.2.6, debug@^3.2.7: + version "3.2.7" + resolved "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a" + integrity sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== dependencies: ms "^2.1.1" @@ -13428,14 +13393,6 @@ dockerode@^3.3.1: docker-modem "^3.0.0" tar-fs "~2.0.1" -doctrine@1.5.0: - version "1.5.0" - resolved "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz#379dce730f6166f76cefa4e6707a159b02c5a6fa" - integrity sha1-N53Ocw9hZvds76TmcHoVmwLFpvo= - dependencies: - esutils "^2.0.2" - isarray "^1.0.0" - doctrine@^2.1.0: version "2.1.0" resolved "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz#5cd01fc101621b42c4cd7f5d1a66243716d3f39d" @@ -14226,20 +14183,21 @@ eslint-formatter-friendly@^7.0.0: strip-ansi "5.2.0" text-table "0.2.0" -eslint-import-resolver-node@^0.3.4: - version "0.3.4" - resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" - integrity sha512-ogtf+5AB/O+nM6DIeBUNr2fuT7ot9Qg/1harBfBtaP13ekEWFQEEMP94BCB7zaNW3gyY+8SHYF00rnqYwXKWOA== +eslint-import-resolver-node@^0.3.6: + version "0.3.6" + resolved "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz#4048b958395da89668252001dbd9eca6b83bacbd" + integrity sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw== dependencies: - debug "^2.6.9" - resolve "^1.13.1" + debug "^3.2.7" + resolve "^1.20.0" -eslint-module-utils@^2.1.1, eslint-module-utils@^2.6.0: - version "2.6.0" - resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.0.tgz#579ebd094f56af7797d19c9866c9c9486629bfa6" - integrity sha512-6j9xxegbqe8/kZY8cYpcp0xhbK0EgJlg3g9mib3/miLaExuuwc3n5UEfSnU6hWMbT0FAYVvDbL9RrRgpUeQIvA== +eslint-module-utils@^2.1.1, eslint-module-utils@^2.7.1: + version "2.7.1" + resolved "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.7.1.tgz#b435001c9f8dd4ab7f6d0efcae4b9696d4c24b7c" + integrity sha512-fjoetBXQZq2tSTWZ9yWVl2KuFrTZZH3V+9iD1V1RfpDgxzJR+mPd/KZmMiA8gbPqdBzpNiEHOuT7IYEWxrH0zQ== dependencies: - debug "^2.6.9" + debug "^3.2.7" + find-up "^2.1.0" pkg-dir "^2.0.0" eslint-plugin-cypress@^2.10.3: @@ -14260,23 +14218,23 @@ eslint-plugin-graphql@^4.0.0: lodash.without "^4.4.0" eslint-plugin-import@^2.20.2: - version "2.22.1" - resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" - integrity sha512-8K7JjINHOpH64ozkAhpT3sd+FswIZTfMZTjdx052pnWrgRCVfp8op9tbjpAk3DdUeI/Ba4C8OjdC0r90erHEOw== + version "2.25.3" + resolved "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.25.3.tgz#a554b5f66e08fb4f6dc99221866e57cfff824766" + integrity sha512-RzAVbby+72IB3iOEL8clzPLzL3wpDrlwjsTBAQXgyp5SeTqqY+0bFubwuo+y/HLhNZcXV4XqTBO4LGsfyHIDXg== dependencies: - array-includes "^3.1.1" - array.prototype.flat "^1.2.3" - contains-path "^0.1.0" + array-includes "^3.1.4" + array.prototype.flat "^1.2.5" debug "^2.6.9" - doctrine "1.5.0" - eslint-import-resolver-node "^0.3.4" - eslint-module-utils "^2.6.0" + doctrine "^2.1.0" + eslint-import-resolver-node "^0.3.6" + eslint-module-utils "^2.7.1" has "^1.0.3" + is-core-module "^2.8.0" + is-glob "^4.0.3" minimatch "^3.0.4" - object.values "^1.1.1" - read-pkg-up "^2.0.0" - resolve "^1.17.0" - tsconfig-paths "^3.9.0" + object.values "^1.1.5" + resolve "^1.20.0" + tsconfig-paths "^3.11.0" eslint-plugin-jest@^24.1.0: version "24.3.6" @@ -15919,19 +15877,7 @@ glob@^6.0.1: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7: - version "7.1.7" - resolved "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz#3b193e9233f01d42d0b3f78294bbeeb418f94a90" - integrity sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ== - dependencies: - fs.realpath "^1.0.0" - inflight "^1.0.4" - inherits "2" - minimatch "^3.0.4" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.2.0: +glob@^7.0.0, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4, glob@^7.1.6, glob@^7.1.7, glob@^7.2.0: version "7.2.0" resolved "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz#d15535af7732e02e948f4c41628bd910293f6023" integrity sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q== @@ -16991,9 +16937,9 @@ human-signals@^2.1.0: integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== humanize-duration@^3.25.1, humanize-duration@^3.26.0, humanize-duration@^3.27.0: - version "3.27.0" - resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.0.tgz#3f781b7cf8022ad587f76b9839b60bc2b29636b2" - integrity sha512-qLo/08cNc3Tb0uD7jK0jAcU5cnqCM0n568918E7R2XhMr/+7F37p4EY062W/stg7tmzvknNn9b/1+UhVRzsYrQ== + version "3.27.1" + resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.27.1.tgz#2cd4ea4b03bd92184aee6d90d77a8f3d7628df69" + integrity sha512-jCVkMl+EaM80rrMrAPl96SGG4NRac53UyI1o/yAzebDntEY6K6/Fj2HOjdPg8omTqIe5Y0wPBai2q5xXrIbarA== humanize-ms@^1.2.1: version "1.2.1" @@ -17516,10 +17462,10 @@ is-ci@^3.0.0: dependencies: ci-info "^3.1.1" -is-core-module@^2.1.0, is-core-module@^2.2.0: - version "2.4.0" - resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.4.0.tgz#8e9fc8e15027b011418026e98f0e6f4d86305cc1" - integrity sha512-6A2fkfq1rfeQZjxrZJGerpLCTHRNEBiSgnu0+obeJpEPZRUooHgsizvzv0ZjJwOz3iWIHdJtVWJ/tmPr3D21/A== +is-core-module@^2.1.0, is-core-module@^2.2.0, is-core-module@^2.8.0: + version "2.8.0" + resolved "https://registry.npmjs.org/is-core-module/-/is-core-module-2.8.0.tgz#0321336c3d0925e497fd97f5d95cb114a5ccd548" + integrity sha512-vd15qHsaqrRL7dtH6QNuy0ndJmRDrS9HAM1CAiSifNUFv4x1a0CCVsj18hJ1mShxIG6T2i1sO78MkP56r0nYRw== dependencies: has "^1.0.3" @@ -17642,7 +17588,7 @@ is-generator-function@^1.0.7: resolved "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.8.tgz#dfb5c2b120e02b0a8d9d2c6806cd5621aa922f7b" integrity sha512-2Omr/twNtufVZFr1GhxjOMFPAj2sjc/dKaIqBhvo4qciXfJmITGH6ZGd8eZYNHza8t1y0e01AuqRhJwfWp26WQ== -is-glob@4.0.1, is-glob@^4.0.0, is-glob@^4.0.1, is-glob@~4.0.1: +is-glob@4.0.1: version "4.0.1" resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz#7567dbe9f2f5e2467bc77ab83c4a29482407a5dc" integrity sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg== @@ -17663,6 +17609,13 @@ is-glob@^3.0.0, is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + is-hexadecimal@^1.0.0: version "1.0.4" resolved "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz#cc35c97588da4bd49a8eedd6bc4082d44dcb23a7" @@ -19660,16 +19613,6 @@ load-json-file@^1.0.0: pinkie-promise "^2.0.0" strip-bom "^2.0.0" -load-json-file@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-2.0.0.tgz#7947e42149af80d696cbf797bcaabcfe1fe29ca8" - integrity sha1-eUfkIUmvgNaWy/eXvKq8/h/inKg= - dependencies: - graceful-fs "^4.1.2" - parse-json "^2.2.0" - pify "^2.0.0" - strip-bom "^3.0.0" - load-json-file@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz#2f5f45ab91e33216234fd53adab668eb4ec0993b" @@ -19808,7 +19751,7 @@ lodash.flattendeep@^4.0.0: resolved "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz#fb030917f86a3134e5bc9bec0d69e0013ddfedb2" integrity sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI= -lodash.get@^4, lodash.get@^4.0.0, lodash.get@^4.4.2: +lodash.get@^4, lodash.get@^4.4.2: version "4.4.2" resolved "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz#2d177f652fa31e939b4438d5341499dfa3825e99" integrity sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= @@ -19843,7 +19786,7 @@ lodash.isempty@^4.4.0: resolved "https://registry.npmjs.org/lodash.isempty/-/lodash.isempty-4.4.0.tgz#6f86cbedd8be4ec987be9aaf33c9684db1b31e7e" integrity sha1-b4bL7di+TsmHvpqvM8loTbGzHn4= -lodash.isequal@^4.0.0: +lodash.isequal@^4.5.0: version "4.5.0" resolved "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" integrity sha1-QVxEePK8wwEgwizhDtMib30+GOA= @@ -21327,12 +21270,7 @@ mkdirp@^1.0.3, mkdirp@^1.0.4: resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== -mock-fs@^5.1.0: - version "5.1.0" - resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-5.1.0.tgz#a9aebd4e6d74a626f84b86eae8a372bd061754e8" - integrity sha512-wXdQ2nIk81TYIGLphUnbXl8akQpjb9ItfZefMcTxZcoe+djMkd5POU8fQdSEErxVAeT4CgDHWveYquys4H6Cmw== - -mock-fs@^5.1.1: +mock-fs@^5.1.0, mock-fs@^5.1.1: version "5.1.2" resolved "https://registry.npmjs.org/mock-fs/-/mock-fs-5.1.2.tgz#6fa486e06d00f8793a8d2228de980eff93ce6db7" integrity sha512-YkjQkdLulFrz0vD4BfNQdQRVmgycXTV7ykuHMlyv+C8WCHazpkiQRDthwa02kSyo8wKnY9wRptHfQLgmf0eR+A== @@ -22223,7 +22161,7 @@ object.pick@^1.3.0: dependencies: isobject "^3.0.1" -object.values@^1.1.0, object.values@^1.1.1, object.values@^1.1.5: +object.values@^1.1.0, object.values@^1.1.5: version "1.1.5" resolved "https://registry.npmjs.org/object.values/-/object.values-1.1.5.tgz#959f63e3ce9ef108720333082131e4a459b716ac" integrity sha512-QUZRW0ilQ3PnPpbNtgdNV1PDbEqLIiSFB3l+EnGtBQ/8SUTLj1PZwtQHABZtLgwpJZTSZhuGLOGk57Drx2IvYg== @@ -23058,13 +22996,6 @@ path-type@^1.0.0: pify "^2.0.0" pinkie-promise "^2.0.0" -path-type@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/path-type/-/path-type-2.0.0.tgz#f012ccb8415b7096fc2daa1054c3d72389594c73" - integrity sha1-8BLMuEFbcJb8LaoQVMPXI4lZTHM= - dependencies: - pify "^2.0.0" - path-type@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz#cef31dc8e0a1a3bb0d105c0cd97cf3bf47f4e36f" @@ -24922,14 +24853,6 @@ read-pkg-up@^1.0.1: find-up "^1.0.0" read-pkg "^1.0.0" -read-pkg-up@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-2.0.0.tgz#6b72a8048984e0c41e79510fd5e9fa99b3b549be" - integrity sha1-a3KoBImE4MQeeVEP1en6mbO1Sb4= - dependencies: - find-up "^2.0.0" - read-pkg "^2.0.0" - read-pkg-up@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz#3ed496685dba0f8fe118d0691dc51f4a1ff96f07" @@ -24956,15 +24879,6 @@ read-pkg@^1.0.0: normalize-package-data "^2.3.2" path-type "^1.0.0" -read-pkg@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-2.0.0.tgz#8ef1c0623c6a6db0dc6713c4bfac46332b2368f8" - integrity sha1-jvHAYjxqbbDcZxPEv6xGMysjaPg= - dependencies: - load-json-file "^2.0.0" - normalize-package-data "^2.3.2" - path-type "^2.0.0" - read-pkg@^3.0.0: version "3.0.0" resolved "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz#9cbc686978fee65d16c00e2b19c237fcf6e38389" @@ -25604,7 +25518,7 @@ resolve-url@^0.2.1: resolved "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz#2c637fe77c893afd2a663fe21aa9080068e2052a" integrity sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo= -resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.13.1, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.3.2, resolve@^1.9.0: +resolve@^1.1.6, resolve@^1.10.0, resolve@^1.12.0, resolve@^1.14.2, resolve@^1.17.0, resolve@^1.18.1, resolve@^1.19.0, resolve@^1.20.0, resolve@^1.3.2, resolve@^1.9.0: version "1.20.0" resolved "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz#629a013fb3f70755d6f0b7935cc1c2c5378b1975" integrity sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A== @@ -25856,21 +25770,7 @@ run-script-webpack-plugin@^0.0.11: resolved "https://registry.npmjs.org/run-script-webpack-plugin/-/run-script-webpack-plugin-0.0.11.tgz#04c510bed06b907fa2285e75feece71a25691171" integrity sha512-QmuBhiqBPmhQLpO5vMBHVTAGyoPBnrCM5gQ3IzgieiImBXiBbXcIv4kysCT1gilFNFxQk22oKQfiIhWbT/zXCw== -rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.0: - version "6.6.2" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz#8096a7ac03f2cc4fe5860ef6e572810d9e01c0d2" - integrity sha512-BHdBMVoWC2sL26w//BCu3YzKT4s2jip/WhwsGEDmeKYBhKDZeYezVUnHatYB7L85v5xs0BAQmg6BEYJEKxBabg== - dependencies: - tslib "^1.9.0" - -rxjs@^6.6.3: - version "6.6.6" - resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz#14d8417aa5a07c5e633995b525e1e3c0dec03b70" - integrity sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg== - dependencies: - tslib "^1.9.0" - -rxjs@^6.6.7: +rxjs@^6.3.3, rxjs@^6.4.0, rxjs@^6.6.0, rxjs@^6.6.3, rxjs@^6.6.7: version "6.6.7" resolved "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" integrity sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ== @@ -27317,9 +27217,9 @@ subscriptions-transport-ws@^0.9.18, subscriptions-transport-ws@^0.9.19: ws "^5.2.0 || ^6.0.0 || ^7.0.0" sucrase@^3.18.0, sucrase@^3.20.2: - version "3.20.2" - resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.20.2.tgz#28a28dc58a55be0d6916d5c9b2440d203e9ffe62" - integrity sha512-EdJ5M6VEvToIZwIWiZ71cxe4CklDRG8PdSjUSst+BZCUGlaEhnrdQo/LOXsuq3MjWRbfepg1XTffClK0Tmo0HQ== + version "3.20.3" + resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.20.3.tgz#424f1e75b77f955724b06060f1ae708f5f0935cf" + integrity sha512-azqwq0/Bs6RzLAdb4dXxsCgMtAaD2hzmUr4UhSfsxO46JFPAwMnnb441B/qsudZiS6Ylea3JXZe3Q497lsgXzQ== dependencies: commander "^4.0.0" glob "7.1.6" @@ -28269,10 +28169,10 @@ ts-pnp@^1.1.6: resolved "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" integrity sha512-csd+vJOb/gkzvcCHgTGSChYpy5f1/XKNsmvBGO4JXS+z1v2HobugDz4s1IeFXM3wZB44uczs+eazB5Q/ccdhQw== -tsconfig-paths@^3.9.0: - version "3.9.0" - resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz#098547a6c4448807e8fcb8eae081064ee9a3c90b" - integrity sha512-dRcuzokWhajtZWkQsDVKbWyY+jgcLC5sqJhg2PSgf4ZkH2aHPvaOY8YWGhmjb68b5qqTfasSsDO9k7RUiEmZAw== +tsconfig-paths@^3.11.0: + version "3.12.0" + resolved "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.12.0.tgz#19769aca6ee8f6a1a341e38c8fa45dd9fb18899b" + integrity sha512-e5adrnOYT6zqVnWqZu7i/BQ3BnhzvGbjEjejFXO20lKIKpwTaupkCPgEfv4GZK1IBciJUEhYs3J3p75FdaTFVg== dependencies: "@types/json5" "^0.0.29" json5 "^1.0.1" @@ -28476,6 +28376,11 @@ typescript@~4.3.5: resolved "https://registry.npmjs.org/typescript/-/typescript-4.3.5.tgz#4d1c37cc16e893973c45a06886b7113234f119f4" integrity sha512-DqQgihaQ9cUrskJo9kIyW/+g0Vxsk8cDtZ52a3NGh0YNTfpUSArXSohyUGnvbPazEPLu398C0UxmKSOrPumUzA== +typescript@~4.5.2: + version "4.5.4" + resolved "https://registry.npmjs.org/typescript/-/typescript-4.5.4.tgz#a17d3a0263bf5c8723b9c52f43c5084edf13c2e8" + integrity sha512-VgYs2A2QIRuGphtzFV7aQJduJ2gyfTljngLzjpfW9FoYZF6xuw1W0vW9ghCKLfcWrCFxK81CSGRAvS1pn4fIUg== + ua-parser-js@^0.7.18: version "0.7.28" resolved "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz#8ba04e653f35ce210239c64661685bf9121dec31" @@ -29163,10 +29068,10 @@ validate.io-number@^1.0.3: resolved "https://registry.npmjs.org/validate.io-number/-/validate.io-number-1.0.3.tgz#f63ffeda248bf28a67a8d48e0e3b461a1665baf8" integrity sha1-9j/+2iSL8opnqNSODjtGGhZluvg= -validator@^8.0.0: - version "8.2.0" - resolved "https://registry.npmjs.org/validator/-/validator-8.2.0.tgz#3c1237290e37092355344fef78c231249dab77b9" - integrity sha512-Yw5wW34fSv5spzTXNkokD6S6/Oq92d8q/t14TqsS3fAiA1RYnxSFSIZ+CY3n6PGGRCq5HhJTSepQvFUS2QUDxA== +validator@^13.7.0: + version "13.7.0" + resolved "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz#4f9658ba13ba8f3d82ee881d3516489ea85c0857" + integrity sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw== value-or-promise@1.0.11: version "1.0.11" @@ -30205,14 +30110,14 @@ yup@^0.32.9: property-expr "^2.0.4" toposort "^2.0.2" -z-schema@~3.18.3: - version "3.18.4" - resolved "https://registry.npmjs.org/z-schema/-/z-schema-3.18.4.tgz#ea8132b279533ee60be2485a02f7e3e42541a9a2" - integrity sha512-DUOKC/IhbkdLKKiV89gw9DUauTV8U/8yJl1sjf6MtDmzevLKOF2duNJ495S3MFVjqZarr+qNGCPbkg4mu4PpLw== +z-schema@~5.0.2: + version "5.0.2" + resolved "https://registry.npmjs.org/z-schema/-/z-schema-5.0.2.tgz#f410394b2c9fcb9edaf6a7511491c0bb4e89a504" + integrity sha512-40TH47ukMHq5HrzkeVE40Ad7eIDKaRV2b+Qpi2prLc9X9eFJFzV7tMe5aH12e6avaSS/u5l653EQOv+J9PirPw== dependencies: - lodash.get "^4.0.0" - lodash.isequal "^4.0.0" - validator "^8.0.0" + lodash.get "^4.4.2" + lodash.isequal "^4.5.0" + validator "^13.7.0" optionalDependencies: commander "^2.7.1"