diff --git a/.changeset/afraid-carpets-know.md b/.changeset/afraid-carpets-know.md new file mode 100644 index 0000000000..94a5452606 --- /dev/null +++ b/.changeset/afraid-carpets-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Add Optional Props to Override Icon for SidebarSearch and SidebarSearchModal Component 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/cyan-seahorses-film.md b/.changeset/cyan-seahorses-film.md new file mode 100644 index 0000000000..80a27baec0 --- /dev/null +++ b/.changeset/cyan-seahorses-film.md @@ -0,0 +1,11 @@ +--- +'@backstage/core-plugin-api': minor +--- + +**BREAKING CHANGE** The `StorageApi` has received several updates that fills in gaps for some use-cases and makes it easier to avoid mistakes: + +- The `StorageValueChange` type has been renamed to `StorageValueSnapshot`, the `newValue` property has been renamed to `value`, the stored value type has been narrowed to `JsonValue`, and it has received a new `presence` property that is `'unknown'`, `'absent'`, or `'present'`. +- The `get` method has been deprecated in favor of a new `snapshot` method, which returns a `StorageValueSnapshot`. +- The `observe$` method has had its contract changed. It should now emit values when the `presence` of a key changes, this may for example happen when remotely stored values are requested on page load and the presence switches from `'unknown'` to either `'absent'` or `'present'`. + +The above changes have been made with deprecations in place to maintain much of the backwards compatibility for consumers of the `StorageApi`. The only breaking change is the narrowing of the stored value type, which may in some cases require the addition of an explicit type parameter to the `get` and `observe$` methods. diff --git a/.changeset/dirty-ways-learn.md b/.changeset/dirty-ways-learn.md new file mode 100644 index 0000000000..e83fbbdfa2 --- /dev/null +++ b/.changeset/dirty-ways-learn.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-config-schema': patch +'@backstage/plugin-scaffolder': patch +--- + +Fix a small browser console warning 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/expor-k8s-backend-client.md b/.changeset/expor-k8s-backend-client.md new file mode 100644 index 0000000000..b4b7bd9973 --- /dev/null +++ b/.changeset/expor-k8s-backend-client.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': minor +--- + +Includes `KubernetesBackendClient` in the export to allow developers to use it externally. diff --git a/.changeset/fluffy-grapes-decide.md b/.changeset/fluffy-grapes-decide.md new file mode 100644 index 0000000000..17a6cc965f --- /dev/null +++ b/.changeset/fluffy-grapes-decide.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added peerPluginDependencies option to experimentalInstallationRecipe for install command to install plugins it depends on. diff --git a/.changeset/fresh-months-approve.md b/.changeset/fresh-months-approve.md new file mode 100644 index 0000000000..c2da09daf6 --- /dev/null +++ b/.changeset/fresh-months-approve.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-tech-radar': minor +--- + +Add new property to enable open links in a new window/tab 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/good-fans-dance.md b/.changeset/good-fans-dance.md new file mode 100644 index 0000000000..a24b467c01 --- /dev/null +++ b/.changeset/good-fans-dance.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-bazaar': patch +'@backstage/plugin-bazaar-backend': patch +--- + +made the linkage between a Bazaar project to a catalog Entity optional diff --git a/.changeset/gorgeous-beers-teach.md b/.changeset/gorgeous-beers-teach.md new file mode 100644 index 0000000000..014398a5be --- /dev/null +++ b/.changeset/gorgeous-beers-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': minor +--- + +Updated `WebStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`. diff --git a/.changeset/green-timers-film.md b/.changeset/green-timers-film.md new file mode 100644 index 0000000000..be94b13f0d --- /dev/null +++ b/.changeset/green-timers-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': minor +--- + +Updated `MockStorageApi` to reflect the `StorageApi` changes in `@backstage/core-plugin-api`. 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/many-trees-happen.md b/.changeset/many-trees-happen.md new file mode 100644 index 0000000000..f29c11fb22 --- /dev/null +++ b/.changeset/many-trees-happen.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': minor +--- + +Removed deprecated option `env` from `LoadConfigOptions` and associated tests diff --git a/.changeset/modern-planets-fry.md b/.changeset/modern-planets-fry.md new file mode 100644 index 0000000000..2da39473a1 --- /dev/null +++ b/.changeset/modern-planets-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-newrelic-dashboard': patch +--- + +Created a New Plugin , which imports dashboard links and snapshots from New Relic diff --git a/.changeset/moody-scissors-yell.md b/.changeset/moody-scissors-yell.md new file mode 100644 index 0000000000..deb06b3c6f --- /dev/null +++ b/.changeset/moody-scissors-yell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-home': patch +--- + +Fix undefined identity bug in `WelcomeTitle` caused by using deprecated methods of the IdentityApi 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/new-suns-agree.md b/.changeset/new-suns-agree.md new file mode 100644 index 0000000000..c93bb1ca12 --- /dev/null +++ b/.changeset/new-suns-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-rollbar-backend': patch +--- + +Bump `axios` diff --git a/.changeset/new-waves-rule.md b/.changeset/new-waves-rule.md new file mode 100644 index 0000000000..7470a1d08e --- /dev/null +++ b/.changeset/new-waves-rule.md @@ -0,0 +1,7 @@ +--- +'@backstage/core-app-api': minor +--- + +- Removed deprecated definition `createApp` from `@backstage/core-app-api` which has been replaced by `@backstage/app-defaults#createApp` +- Removed deprecated type `BackstagePluginWithAnyOutput` +- Removed deprecated constructors for `GithubAuth`, `OAuth2`, and `SamlAuth` as the `create` method should be used instead diff --git a/.changeset/nice-apricots-grow.md b/.changeset/nice-apricots-grow.md new file mode 100644 index 0000000000..290041091d --- /dev/null +++ b/.changeset/nice-apricots-grow.md @@ -0,0 +1,8 @@ +--- +'@backstage/core-plugin-api': minor +--- + +- Removed deprecated option `description` from `ApiRefConfig` +- Removed descriptions from all plugin API refs +- Removed deprecated parameters `path`, `icon`, and `title` in `createRouteRef` +- Removed deprecated types `Error` and `ErrorContext` from `ErrorApi` diff --git a/.changeset/nice-feet-hope.md b/.changeset/nice-feet-hope.md new file mode 100644 index 0000000000..cfc660020f --- /dev/null +++ b/.changeset/nice-feet-hope.md @@ -0,0 +1,30 @@ +--- +'@backstage/create-app': patch +--- + +The `index.html` template of the app has been updated to use the new `config` global provided by the Backstage CLI. + +To apply this change to an existing app, make the following changes to `packages/app/public/index.html`: + +```diff +- <%= app.title %> ++ <%= config.getString('app.title') %> +``` + +```diff +- <% if (app.googleAnalyticsTrackingId && typeof app.googleAnalyticsTrackingId === 'string') { %> ++ <% if (config.has('app.googleAnalyticsTrackingId')) { %> + +``` + +```diff +- gtag('config', '<%= app.googleAnalyticsTrackingId %>'); ++ gtag( ++ 'config', ++ '<%= config.getString("app.googleAnalyticsTrackingId") %>', ++ ); +``` diff --git a/.changeset/olive-cameras-rush.md b/.changeset/olive-cameras-rush.md new file mode 100644 index 0000000000..9a83aea699 --- /dev/null +++ b/.changeset/olive-cameras-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': minor +--- + +Removed deprecated `msw` definition which was replaced by calling `setupRequestMockHandlers` directly 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/purple-grapes-kick.md b/.changeset/purple-grapes-kick.md new file mode 100644 index 0000000000..9d64d4e17c --- /dev/null +++ b/.changeset/purple-grapes-kick.md @@ -0,0 +1,15 @@ +--- +'@backstage/cli': patch +--- + +The frontend configuration is now available as a `config` global during templating of the `index.html` file. This allows for much more flexibility as the values available during templating is not longer hardcoded to a fixed set of values. + +For example, to access the app title, you would now do the following: + +```html +<%= config.getString('app.title') %> +``` + +Along with this change, usage of the existing `app.` values has been deprecated and will be removed in a future release. The general pattern for migrating existing usage is to replace `<%= app. %>` with `<%= config.getString('app.') %>`, although in some cases you may need to use for example `config.has('app.')` or `config.getOptionalString('app.')` instead. + +The [`@backstage/create-app` changelog](https://github.com/backstage/backstage/blob/master/packages/create-app/CHANGELOG.md#049) also contains more details how to migrate existing usage. 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/short-rockets-remain.md b/.changeset/short-rockets-remain.md new file mode 100644 index 0000000000..603caec71a --- /dev/null +++ b/.changeset/short-rockets-remain.md @@ -0,0 +1,9 @@ +--- +'@backstage/app-defaults': patch +'@backstage/core-app-api': minor +'@backstage/core-plugin-api': minor +--- + +- Removed deprecation configuration option `theme` from `AppTheme` of the `AppThemeApi` +- Removed reference to `theme` in the `app-defaults` default `AppTheme` +- Removed logic in `AppThemeProvider` that creates `ThemeProvider` from `appTheme.theme` 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/smooth-news-melt.md b/.changeset/smooth-news-melt.md new file mode 100644 index 0000000000..deb745a46a --- /dev/null +++ b/.changeset/smooth-news-melt.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Do not `setState` when unmounted in `OverflowTooltip` diff --git a/.changeset/soft-scissors-own.md b/.changeset/soft-scissors-own.md new file mode 100644 index 0000000000..e57a018d50 --- /dev/null +++ b/.changeset/soft-scissors-own.md @@ -0,0 +1,5 @@ +--- +'embedded-techdocs-app': patch +--- + +The `index.html` template was updated to use the new `config` global. diff --git a/.changeset/techdocs-sakta-vi-gar.md b/.changeset/techdocs-sakta-vi-gar.md new file mode 100644 index 0000000000..183d90300b --- /dev/null +++ b/.changeset/techdocs-sakta-vi-gar.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Fixed a bug preventing cache from being enabled in TechDocs "recommended" deployment mode. 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/thick-deers-trade.md b/.changeset/thick-deers-trade.md new file mode 100644 index 0000000000..9b023d709a --- /dev/null +++ b/.changeset/thick-deers-trade.md @@ -0,0 +1,17 @@ +--- +'@backstage/core-components': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-circleci': patch +'@backstage/plugin-cloudbuild': patch +'@backstage/plugin-github-actions': patch +'@backstage/plugin-gitops-profiles': patch +'@backstage/plugin-jenkins': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-org': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-search': patch +'@backstage/plugin-sentry': patch +'@backstage/plugin-sonarqube': patch +--- + +Prefer using `Link` from `@backstage/core-components` rather than material-UI. 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/unlucky-wolves-reply.md b/.changeset/unlucky-wolves-reply.md new file mode 100644 index 0000000000..ac42b59abb --- /dev/null +++ b/.changeset/unlucky-wolves-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Add DescriptionField override to support Markdown 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/wise-melons-hope.md b/.changeset/wise-melons-hope.md new file mode 100644 index 0000000000..24a615a7bc --- /dev/null +++ b/.changeset/wise-melons-hope.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-app-api': patch +--- + +Schema-validate local storage cached session info on load 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/CODEOWNERS b/.github/CODEOWNERS index ed4b61099f..9cc5a2378e 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -42,3 +42,4 @@ /.changeset/search-* @backstage/techdocs-core /.changeset/techdocs-* @backstage/techdocs-core /cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core +/plugins/apache-airflow @backstage/reviewers @cmpadden diff --git a/.github/labeler.yml b/.github/labeler.yml new file mode 100644 index 0000000000..f141107482 --- /dev/null +++ b/.github/labeler.yml @@ -0,0 +1,24 @@ +catalog: + - plugins/catalog/**/* + - plugins/catalog-*/**/* + - packages/catalog-*/**/* +scaffolder: + - plugins/scaffolder/**/* + - plugins/scaffolder-*/**/* +search: + - plugins/search/**/* + - plugins/search-*/**/* + - packages/search-*/**/* +docs-like-code: + - plugins/techdocs/**/* + - plugins/techdocs-*/**/* + - packages/techdocs-*/**/* +documentation: + - docs/**/* +microsite: + - microsite/**/* +auth: + - plugins/auth-backend/**/* + - packages/core-app-api/src/apis/implementations/auth/**/* + - packages/core-app-api/src/lib/Auth*/**/* + - packages/core-plugin-api/src/apis/definitions/auth.ts diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index 4ec62923d8..8199855c9e 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 @@ -90,10 +92,12 @@ Figma firehydrant FireHydrant Firekube +Firestore Fiverr gitbeaker GitHub GitLab +Gource Grafana graphql GraphQL @@ -187,6 +191,7 @@ OAuth octokit oidc Okta +Oldsberg onboarding Onboarding OpenShift @@ -194,6 +199,8 @@ orgs pagerduty pageview parallelization +Patrik +Peloton plantuml Platformize Podman @@ -293,6 +300,7 @@ truthy typeahead ui unbreak +Unconference unmanaged unregister unregistration @@ -304,6 +312,7 @@ utils validator validators varchar +Wayfair Weaveworks Webpack winston @@ -316,4 +325,3 @@ Zalando Zhou zoomable zsh -Firestore 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/app-config.yaml b/app-config.yaml index 6cd05f44e8..2837d97d2f 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -74,6 +74,11 @@ proxy: headers: X-Api-Key: ${NEW_RELIC_REST_API_KEY} + '/newrelic/api': + target: https://api.newrelic.com + headers: + X-Api-Key: ${NEW_RELIC_USER_KEY} + '/pagerduty': target: https://api.pagerduty.com headers: 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/api/utility-apis.md b/docs/api/utility-apis.md index 14920513af..9ebcfc0945 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -128,8 +128,8 @@ the core APIs. The core APIs are the ones exported by [configApiRef](../reference/core-plugin-api.configapiref.md). The core APIs are loaded for any app created with -[createApp](../reference/core-app-api.createapp.md) from -[@backstage/core-plugin-api](../reference/core-plugin-api.md), which means that +[createApp](../reference/app-defaults.createapp.md) from +[@backstage/core-plugin-api](../reference/app-defaults.md), which means that there is no step that needs to be taken to include these APIs in an app. ### Plugin APIs @@ -168,7 +168,7 @@ Lastly, the app itself is the final point where APIs can be added, and what has the final say in what APIs will be loaded at runtime. The app may override the factories for any of the core or plugin APIs, with the exception of the config, app theme, and identity APIs. These are static APIs that are tied into the -[createApp](../reference/core-app-api.createapp.md) implementation, and +[createApp](../reference/app-defaults.createapp.md) implementation, and therefore not possible to override. Overriding APIs is useful for apps that want to switch out behavior to tailor it @@ -313,7 +313,7 @@ The common development environment for plugins is included in [createDevApp](../reference/dev-utils.createdevapp.md) function creates an application with implementations for all core APIs already present. Contrary to the method for wiring up Utility API implementations in an app created with -[createApp](../reference/core-app-api.createapp.md), +[createApp](../reference/app-defaults.createapp.md), [createDevApp](../reference/dev-utils.createdevapp.md) uses automatic dependency injection. This is to make it possible to replace any API implementation, and having that be reflected in dependents of that API. diff --git a/docs/architecture-decisions/adr000-template.md b/docs/architecture-decisions/adr000-template.md index 5e6edd8dba..f16ae7d39f 100644 --- a/docs/architecture-decisions/adr000-template.md +++ b/docs/architecture-decisions/adr000-template.md @@ -1,6 +1,7 @@ --- id: adrs-adr000 -title: ADR000: [TITLE] +title: 'ADR000: [TITLE]' +# prettier-ignore description: Architecture Decision Record (ADR) for [TITLE] [DESCRIPTION] --- diff --git a/docs/architecture-decisions/adr001-add-adr-log.md b/docs/architecture-decisions/adr001-add-adr-log.md index 872b3311a5..d1bf0f83f5 100644 --- a/docs/architecture-decisions/adr001-add-adr-log.md +++ b/docs/architecture-decisions/adr001-add-adr-log.md @@ -1,6 +1,7 @@ --- id: adrs-adr001 -title: ADR001: Architecture Decision Record (ADR) log +title: 'ADR001: Architecture Decision Record (ADR) log' +# prettier-ignore description: Architecture Decision Record (ADR) logs as a reference point for the team --- diff --git a/docs/architecture-decisions/adr002-default-catalog-file-format.md b/docs/architecture-decisions/adr002-default-catalog-file-format.md index 013e2cbf39..d0a657fe46 100644 --- a/docs/architecture-decisions/adr002-default-catalog-file-format.md +++ b/docs/architecture-decisions/adr002-default-catalog-file-format.md @@ -1,6 +1,7 @@ --- id: adrs-adr002 -title: ADR002: Default Software Catalog File Format +title: 'ADR002: Default Software Catalog File Format' +# prettier-ignore description: Architecture Decision Record (ADR) log on Default Software Catalog File Format --- diff --git a/docs/architecture-decisions/adr003-avoid-default-exports.md b/docs/architecture-decisions/adr003-avoid-default-exports.md index 878d0d701c..34e5cd5411 100644 --- a/docs/architecture-decisions/adr003-avoid-default-exports.md +++ b/docs/architecture-decisions/adr003-avoid-default-exports.md @@ -1,6 +1,7 @@ --- id: adrs-adr003 -title: ADR003: Avoid Default Exports and Prefer Named Exports +title: 'ADR003: Avoid Default Exports and Prefer Named Exports' +# prettier-ignore description: Architecture Decision Record (ADR) log on Avoid Default Exports and Prefer Named Exports --- diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md index e6e378da9d..95d0f27811 100644 --- a/docs/architecture-decisions/adr004-module-export-structure.md +++ b/docs/architecture-decisions/adr004-module-export-structure.md @@ -1,6 +1,7 @@ --- id: adrs-adr004 -title: ADR004: Module Export Structure +title: 'ADR004: Module Export Structure' +# prettier-ignore description: Architecture Decision Record (ADR) log on Module Export Structure --- diff --git a/docs/architecture-decisions/adr005-catalog-core-entities.md b/docs/architecture-decisions/adr005-catalog-core-entities.md index 83196c12e2..1206d9e83d 100644 --- a/docs/architecture-decisions/adr005-catalog-core-entities.md +++ b/docs/architecture-decisions/adr005-catalog-core-entities.md @@ -1,6 +1,7 @@ --- id: adrs-adr005 -title: ADR005: Catalog Core Entities +title: 'ADR005: Catalog Core Entities' +# prettier-ignore description: Architecture Decision Record (ADR) log on Catalog Core Entities --- diff --git a/docs/architecture-decisions/adr006-avoid-react-fc.md b/docs/architecture-decisions/adr006-avoid-react-fc.md index eecb4ad87a..ea43cb215d 100644 --- a/docs/architecture-decisions/adr006-avoid-react-fc.md +++ b/docs/architecture-decisions/adr006-avoid-react-fc.md @@ -1,6 +1,7 @@ --- id: adrs-adr006 -title: ADR006: Avoid React.FC and React.SFC +title: 'ADR006: Avoid React.FC and React.SFC' +# prettier-ignore description: Architecture Decision Record (ADR) log on Avoid React.FC and React.SFC --- diff --git a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md index 68394de17c..a61bb78787 100644 --- a/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md +++ b/docs/architecture-decisions/adr007-use-msw-to-mock-service-requests.md @@ -1,6 +1,7 @@ --- id: adrs-adr007 -title: ADR007: Use MSW to mock http requests +title: 'ADR007: Use MSW to mock http requests' +# prettier-ignore description: Architecture Decision Record (ADR) log on Use MSW to mock http requests --- diff --git a/docs/architecture-decisions/adr008-default-catalog-file-name.md b/docs/architecture-decisions/adr008-default-catalog-file-name.md index 6794dc87cc..fca65c8355 100644 --- a/docs/architecture-decisions/adr008-default-catalog-file-name.md +++ b/docs/architecture-decisions/adr008-default-catalog-file-name.md @@ -1,6 +1,7 @@ --- id: adrs-adr008 -title: ADR008: Default Catalog File Name +title: 'ADR008: Default Catalog File Name' +# prettier-ignore description: Architecture Decision Record (ADR) log on Default Catalog File Name --- diff --git a/docs/architecture-decisions/adr009-entity-references.md b/docs/architecture-decisions/adr009-entity-references.md index a6fe3ee583..254ae7ac1d 100644 --- a/docs/architecture-decisions/adr009-entity-references.md +++ b/docs/architecture-decisions/adr009-entity-references.md @@ -1,6 +1,7 @@ --- id: adrs-adr009 -title: ADR009: Entity References +title: 'ADR009: Entity References' +# prettier-ignore description: Architecture Decision Record (ADR) log on Entity References --- diff --git a/docs/architecture-decisions/adr010-luxon-date-library.md b/docs/architecture-decisions/adr010-luxon-date-library.md index 43f18f0f69..65cabd256c 100644 --- a/docs/architecture-decisions/adr010-luxon-date-library.md +++ b/docs/architecture-decisions/adr010-luxon-date-library.md @@ -1,6 +1,7 @@ --- id: adrs-adr010 -title: ADR010: Use the Luxon Date Library +title: 'ADR010: Use the Luxon Date Library' +# prettier-ignore description: Architecture Decision Record (ADR) for Luxon Date Library --- diff --git a/docs/architecture-decisions/adr011-plugin-package-structure.md b/docs/architecture-decisions/adr011-plugin-package-structure.md index 374a3ed546..363cae01a5 100644 --- a/docs/architecture-decisions/adr011-plugin-package-structure.md +++ b/docs/architecture-decisions/adr011-plugin-package-structure.md @@ -1,6 +1,7 @@ --- id: adrs-adr011 -title: ADR011: Plugin Package Structure +title: 'ADR011: Plugin Package Structure' +# prettier-ignore description: Architecture Decision Record (ADR) for Plugin Package Structure --- diff --git a/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md b/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md index 0893c105f4..25bb6c1833 100644 --- a/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md +++ b/docs/architecture-decisions/adr012-use-luxon-locale-and-date-presets.md @@ -1,6 +1,7 @@ --- id: adrs-adr012 -title: ADR012: Use Luxon.toLocaleString and date/time presets +title: 'ADR012: Use Luxon.toLocaleString and date/time presets' +# prettier-ignore description: Architecture Decision Record (ADR) for using Luxon's toLocaleString method and date/time presets for displaying dates and times --- diff --git a/docs/architecture-decisions/index.md b/docs/architecture-decisions/index.md index f852170b1e..15d1b0e9d5 100644 --- a/docs/architecture-decisions/index.md +++ b/docs/architecture-decisions/index.md @@ -2,6 +2,7 @@ id: adrs-overview title: Architecture Decision Records (ADR) sidebar_label: Overview +# prettier-ignore description: Overview of Architecture Decision Records (ADR) --- 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/docs/getting-started/app-custom-theme.md b/docs/getting-started/app-custom-theme.md index 241ae7ef03..a4ca0771df 100644 --- a/docs/getting-started/app-custom-theme.md +++ b/docs/getting-started/app-custom-theme.md @@ -54,7 +54,7 @@ For example, adding the theme that we created in the previous section can be done like this: ```ts -import { createApp } from '@backstage/core-app-api'; +import { createApp } from '@backstage/app-defaults'; const app = createApp({ apis: ..., diff --git a/docs/plugins/composability.md b/docs/plugins/composability.md index 33568ac561..05af512f92 100644 --- a/docs/plugins/composability.md +++ b/docs/plugins/composability.md @@ -368,7 +368,7 @@ The following is an example of creation and usage of a parameterized route: ```tsx // Creation of a parameterized route const myRouteRef = createRouteRef({ - title: 'My Named Route', + id: 'myroute', params: ['name'] }) diff --git a/docs/tutorials/journey.md b/docs/tutorials/journey.md index 249507ddf0..126bb5ae0d 100644 --- a/docs/tutorials/journey.md +++ b/docs/tutorials/journey.md @@ -48,7 +48,6 @@ that implements the `OAuthApi` type, it's now working in the frontend too. ```ts const spotifyAuthApiRef = createApiRef({ id: 'core.auth.spotify', - description: 'Provides authentication towards Spotify APIs', }); ``` diff --git a/docs/tutorials/using-backstage-proxy-within-plugin.md b/docs/tutorials/using-backstage-proxy-within-plugin.md index b0c6bef8ef..771aa7a652 100644 --- a/docs/tutorials/using-backstage-proxy-within-plugin.md +++ b/docs/tutorials/using-backstage-proxy-within-plugin.md @@ -99,7 +99,6 @@ export interface MyAwesomeApi { export const myAwesomeApiRef = createApiRef({ id: 'plugin.my-awesome-api.service', - description: 'Example API definition', }); ``` diff --git a/microsite/blog/2021-12-20-backstage-wrapped-2021.md b/microsite/blog/2021-12-20-backstage-wrapped-2021.md new file mode 100644 index 0000000000..2a6234a9f6 --- /dev/null +++ b/microsite/blog/2021-12-20-backstage-wrapped-2021.md @@ -0,0 +1,145 @@ +--- +title: Backstage Wrapped 2021 +author: Lee Mills, Spotify +authorURL: https://github.com/leemills83 +authorImageURL: https://avatars1.githubusercontent.com/u/1236238?s=460&v=4 +--- + +![Backstage in 2021: 75+ public adopters, 500+ contributors, almost 2,000 forks, 9,000+ contributions, 4,000+ Discord members](assets/21-12-20/backstage-wrapped-2021.png) + +What a year 2021 has been for the Backstage project and community. As the year comes to a close, we can look back at some of the happenings, the successes, and welcome all the newcomers to our growing community. + +In a year that has had its fair share of ups and downs, this community has been something that has brought me so much in the support you provide each other, your eagerness, passion, and willingness to share. Thank you to all of you for creating such a great community, and welcome to everyone joining us. I can’t wait to meet you all and see what great things we can create together in the new year. + +In 2021 we’ve seen the project and community grow in every dimension, reaching over 75 public adopters, over 500 contributors from around the world all bringing new ideas and perspectives. We’ve had so many of you sharing and demoing your hard work in our monthly community sessions, added new plugins, built new features and really started to see Backstage have an impact for engineering teams across so many of our users. + +Now let’s hear from the maintainers and look at the stats! + + + +## The community grew…and grew + +This year, Netflix, [Expedia][metrics], Wayfair, Peloton, [Box][10], [DAZN][dazn], TELUS, and Unity all officially joined the community — along with more than 50 other public [adopters][md]. Contributors grew by leaps and bounds, too. + +Patrik Oldsberg ([@Rugvip][rugvip]): + +> It’s been awesome to see the engagement by the community grow even more in 2021, especially the way people within the community support each other. Thanks to our growing number of stable, experienced, and knowledgeable contributors it’s now possible to get help in places like Discord anytime, day or night. + +### More adopters, more contributors + +![Accelerated growth of Public Backstage Adopters, from July 2020–December 2021.](assets/21-12-20/public-backstage-adopters-2021.png) + +In January, Backstage had 17 public adopters. By September, [we had 50][50]. And the curve only got steeper from there. + +- **75+ public adopters** +- **500+ contributors** +- **4,000+ Discord members** +- **9,000+ PRs & issues** + +### A very busy repo + +Along with the growth in contributors, PRs, and issues, our active, thriving community has crossed (or is just about to cross) other big milestones on GitHub, too. + +- **Nearly 2,000 forks** +- **20,000+ commits** +- **14,000+ GitHub stars** + +### Where in the world is the Backstage community? + +Watch the visualization below to see where in the world all those contributions came from this past year. + + + +### What does 9,000 contributions look like? + + + +Can you spot your name in the video above? + +Picking up from the end of 2020, the video shows every single contribution to the Backstage source code over this past year. The visualization was built using [Gource.io][gource]: + +> Software projects are displayed by Gource as an animated tree with the root directory of the project at its centre. Directories appear as branches with files as leaves. Developers can be seen working on the tree at the times they contributed to the project. + +And if your name isn’t in there, it’s never too late to get started [contributing][contributing] — so that you too can become a bright, sparkling, laser-beam-shooting spider-node thing in 2022! + +## Other 2021 milestones + +As the community grew, so did the product — with the addition of new features as well as important performance and stability improvements to the core. + +Ben Lambert ([@benjdlambert][blam]): + +> We’ve had a lot of priorities this year. Our first priority was rebuilding the catalog to make it more efficient. Then, toward the later part of the year, we’ve been stabilizing the core with deprecations and API changes ahead of our 1.0 release of the Core Framework next year. +> +> It’s been a super productive year for us all as Core Maintainers, and the product has come on leaps and bounds with the help of everyone involved in the project! + +### New features + +These new features were built with the community, and made with customization and extensibility in mind. + +- **Kubernetes Service Monitoring ([RFC][k8s])** +- **Search Platform ([Alpha][search])** +- **Composable Homepage ([README][home])** +- **Authorization Framework ([RFC][auth])** + +### Major stability updates + +As we get the ball rolling for [CNCF Incubation][cncf], we’ve been busy updating, refining, documenting, deprecating, and stabilizing the core. + +- **Software Templates ([Beta][scaffolder])** +- **TechDocs ([Beta][techdocs])** +- **API Reference ([docs][api])** + +### 50+ open source plugins + +We reached [50+ plugins][plugins] built by the community this year, including official plugins (**Snyk**, **FireHydrant**), highly requested plugins (**PagerDuty**, **Prometheus**), and plugins based on new ideas and functionality (**XCMetrics**, **[Tech Insights][techrfc]**) — all tailor-made for Backstage. + +### In the spotlight + +This year Backstage was center stage at both community-organized events and at major conferences. + +- **New (and [expanded][09]) Community Sessions** +- **Backstage Users Unconference + [Open Mic][openmic]** +- **KubeCon + CloudNativeCon [Europe][europe] & [North America][na]** +- **GitHub Universe 2021 [Keynote][day2]** + +Find more talks, podcasts, blog posts, and Backstage in the news [here][news]. + +## Goodbye, 2021. Hello, 2022! + +Lastly, thanks to everyone in the global Backstage community who contributed to the open source project in 2021. + +This was the first year Spotify hosted [Backstage Community Sessions][meetups], and it’s been a great source of inspiration, motivation, and joy getting to know all the people behind the contributions. We look forward to seeing all your faces again in 2022 — and hopefully many new ones as well. + +Until then, [grab this Backstage Zoom background][zoom] to use at the next Community Session. + +![Backstage Zoom Background](assets/21-12-20/Backstage-background-02-2021.png) + +See you then! Happy holidays from everyone on the Backstage team at Spotify! + +[metrics]: https://backstage.spotify.com/blog/measuring-backstage-proof-of-value-at-expedia/ +[10]: https://backstage.spotify.com/blog/community-session/10-box-and-spotify-paid-plugins/ +[dazn]: https://backstage.spotify.com/blog/adopter-spotlight/dazn/ +[md]: https://github.com/backstage/backstage/blob/master/ADOPTERS.md +[rugvip]: https://github.com/Rugvip +[50]: https://backstage.io/blog/2021/09/30/50-public-adopters +[gource]: https://gource.io +[contributing]: https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md +[blam]: https://github.com/benjdlambert +[k8s]: https://github.com/backstage/backstage/issues/2857 +[search]: https://backstage.io/blog/2021/06/24/announcing-backstage-search-platform +[home]: https://github.com/backstage/backstage/blob/master/plugins/home/README.md +[auth]: https://backstage.spotify.com/blog/authorization-framework-rfc/ +[scaffolder]: https://backstage.io/blog/2021/07/26/software-templates-are-now-in-beta +[techdocs]: https://backstage.io/blog/2021/09/16/the-techdocs-beta-has-landed +[api]: https://backstage.io/docs/reference/ +[cncf]: https://github.com/cncf/toc/pull/717 +[plugins]: https://backstage.io/plugins +[techrfc]: https://github.com/backstage/backstage/issues/7576 +[09]: https://backstage.spotify.com/blog/community-session/09-prometheus-homepage-templates/ +[openmic]: https://backstage-openmic.com +[europe]: https://www.youtube.com/watch?v=aIURaocR5D8 +[na]: https://backstage.spotify.com/blog/kubecon-na-2021/ +[day2]: https://www.youtube.com/watch?v=7yQJlYvpoYs&t=269s +[news]: https://github.com/backstage/community#newsletters +[meetups]: https://github.com/backstage/community#meetups +[zoom]: https://drive.google.com/file/d/14wOPXEBZ5Qmi29ms4Sqp1m56dM0AIHbB/view?usp=sharing diff --git a/microsite/blog/assets/21-12-20/Backstage-background-02-2021.png b/microsite/blog/assets/21-12-20/Backstage-background-02-2021.png new file mode 100644 index 0000000000..bf3c72ba70 Binary files /dev/null and b/microsite/blog/assets/21-12-20/Backstage-background-02-2021.png differ diff --git a/microsite/blog/assets/21-12-20/backstage-wrapped-2021.png b/microsite/blog/assets/21-12-20/backstage-wrapped-2021.png new file mode 100644 index 0000000000..6ac5d5f67c Binary files /dev/null and b/microsite/blog/assets/21-12-20/backstage-wrapped-2021.png differ diff --git a/microsite/blog/assets/21-12-20/public-backstage-adopters-2021.png b/microsite/blog/assets/21-12-20/public-backstage-adopters-2021.png new file mode 100644 index 0000000000..aac981d5f9 Binary files /dev/null and b/microsite/blog/assets/21-12-20/public-backstage-adopters-2021.png differ 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-defaults/src/defaults/components.tsx b/packages/app-defaults/src/defaults/components.tsx index 139bea4862..93a5d9cd50 100644 --- a/packages/app-defaults/src/defaults/components.tsx +++ b/packages/app-defaults/src/defaults/components.tsx @@ -73,7 +73,7 @@ const DefaultErrorBoundaryFallback = ({ }; /** - * Creates a set of default components to pass along to {@link @backstage/core-app-api#createApp}. + * Creates a set of default components to pass along to {@link @backstage/core-app-api#createSpecializedApp}. * * @public */ diff --git a/packages/app-defaults/src/defaults/themes.tsx b/packages/app-defaults/src/defaults/themes.tsx index 90a4a0f47f..397c160168 100644 --- a/packages/app-defaults/src/defaults/themes.tsx +++ b/packages/app-defaults/src/defaults/themes.tsx @@ -28,7 +28,6 @@ export const themes: AppTheme[] = [ title: 'Light Theme', variant: 'light', icon: , - theme: lightTheme, Provider: ({ children }) => ( {children} @@ -40,7 +39,6 @@ export const themes: AppTheme[] = [ title: 'Dark Theme', variant: 'dark', icon: , - theme: darkTheme, Provider: ({ children }) => ( {children} 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..1f609858f6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,39 +1,40 @@ { "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-newrelic-dashboard": "^0.1.0", + "@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 +42,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/public/index.html b/packages/app/public/index.html index d86f09f0b0..8273576b01 100644 --- a/packages/app/public/index.html +++ b/packages/app/public/index.html @@ -47,13 +47,12 @@ min-height: 100%; } - <%= app.title %> + <%= config.getString('app.title') %> - <% if (app.googleAnalyticsTrackingId && typeof - app.googleAnalyticsTrackingId==='string' ) { %> + <% if (config.has('app.googleAnalyticsTrackingId')) { %> - <% } %> <% if (app.datadogRum.clientToken && app.datadogRum.applicationId ) - { %> + <% } %> <% if (config.has('app.datadogRum')) { %> <% } %> diff --git a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx index 43eaa6218c..9229bc61dc 100644 --- a/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx +++ b/packages/dev-utils/src/devApp/SidebarThemeSwitcher.test.tsx @@ -16,7 +16,6 @@ import { AppThemeApi, appThemeApiRef } from '@backstage/core-plugin-api'; import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; -import { BackstageTheme } from '@backstage/theme'; import userEvent from '@testing-library/user-event'; import React from 'react'; import ObservableImpl from 'zen-observable'; @@ -41,13 +40,13 @@ describe('SidebarThemeSwitcher', () => { id: 'dark', title: 'Dark Theme', variant: 'dark', - theme: {} as unknown as BackstageTheme, + Provider: jest.fn(), }, { id: 'light', title: 'Light Theme', variant: 'light', - theme: {} as unknown as BackstageTheme, + Provider: jest.fn(), }, ]); }); diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index adea9e14f2..36d6abcfd1 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -159,7 +159,7 @@ export class DevAppBuilder { * Build a DevApp component using the resources registered so far */ build(): ComponentType<{}> { - const dummyRouteRef = createRouteRef({ title: 'Page of another plugin' }); + const dummyRouteRef = createRouteRef({ id: 'dummy' }); const DummyPage = () => Page belonging to another plugin.; attachComponentData(DummyPage, 'core.mountPoint', dummyRouteRef); diff --git a/packages/embedded-techdocs-app/public/index.html b/packages/embedded-techdocs-app/public/index.html index a6102f010d..fb874df173 100644 --- a/packages/embedded-techdocs-app/public/index.html +++ b/packages/embedded-techdocs-app/public/index.html @@ -47,7 +47,7 @@ min-height: 100%; } - <%= app.title %> + <%= config.getString('app.title') %> diff --git a/packages/integration-react/src/api/ScmIntegrationsApi.ts b/packages/integration-react/src/api/ScmIntegrationsApi.ts index 06f28a6c4d..ec2185701b 100644 --- a/packages/integration-react/src/api/ScmIntegrationsApi.ts +++ b/packages/integration-react/src/api/ScmIntegrationsApi.ts @@ -45,5 +45,4 @@ export class ScmIntegrationsApi { export const scmIntegrationsApiRef: ApiRef = createApiRef({ id: 'integration.scmintegrations', - description: 'All of the registered SCM integrations of your config', }); 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..a75a6d958e 100644 --- a/packages/test-utils/api-report.md +++ b/packages/test-utils/api-report.md @@ -12,13 +12,14 @@ import { ErrorApi } from '@backstage/core-plugin-api'; import { ErrorApiError } from '@backstage/core-plugin-api'; import { ErrorApiErrorContext } from '@backstage/core-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; +import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; import { ReactElement } from 'react'; import { ReactNode } from 'react'; import { RenderResult } from '@testing-library/react'; import { RouteRef } from '@backstage/core-plugin-api'; import { StorageApi } from '@backstage/core-plugin-api'; -import { StorageValueChange } from '@backstage/core-plugin-api'; +import { StorageValueSnapshot } from '@backstage/core-plugin-api'; // @public export type AsyncLogCollector = () => Promise; @@ -34,52 +35,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; @@ -127,11 +82,13 @@ export class MockStorageApi implements StorageApi { // (undocumented) get(key: string): T | undefined; // (undocumented) - observe$(key: string): Observable>; + observe$(key: string): Observable>; // (undocumented) remove(key: string): Promise; // (undocumented) set(key: string, data: T): Promise; + // (undocumented) + snapshot(key: string): StorageValueSnapshot; } // @public @@ -139,15 +96,6 @@ export type MockStorageBucket = { [key: string]: any; }; -// @public @deprecated (undocumented) -export const msw: { - setupDefaultHandlers: (worker: { - listen: (t: any) => void; - close: () => void; - resetHandlers: () => void; - }) => void; -}; - // @public export function renderInTestApp( Component: ComponentType | ReactNode, 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/apis/StorageApi/MockStorageApi.test.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts index a3028f8b57..a8187cd849 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.test.ts @@ -25,6 +25,12 @@ describe('WebStorage Storage API', () => { const storage = createMockStorage(); expect(storage.get('myfakekey')).toBeUndefined(); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'absent', + value: undefined, + newValue: undefined, + }); }); it('should allow the setting and getting of the simple data structures', async () => { @@ -36,6 +42,24 @@ describe('WebStorage Storage API', () => { expect(storage.get('myfakekey')).toBe('helloimastring'); expect(storage.get('mysecondfakekey')).toBe(1234); expect(storage.get('mythirdfakekey')).toBe(true); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'present', + value: 'helloimastring', + newValue: 'helloimastring', + }); + expect(storage.snapshot('mysecondfakekey')).toEqual({ + key: 'mysecondfakekey', + presence: 'present', + value: 1234, + newValue: 1234, + }); + expect(storage.snapshot('mythirdfakekey')).toEqual({ + key: 'mythirdfakekey', + presence: 'present', + value: true, + newValue: true, + }); }); it('should allow setting of complex datastructures', async () => { @@ -49,6 +73,12 @@ describe('WebStorage Storage API', () => { await storage.set('myfakekey', mockData); expect(storage.get('myfakekey')).toEqual(mockData); + expect(storage.snapshot('myfakekey')).toEqual({ + key: 'myfakekey', + presence: 'present', + value: mockData, + newValue: mockData, + }); }); it('should subscribe to key changes when setting a new value', async () => { @@ -59,7 +89,7 @@ describe('WebStorage Storage API', () => { const mockData = { hello: 'im a great new value' }; await new Promise(resolve => { - storage.observe$('correctKey').subscribe({ + storage.observe$('correctKey').subscribe({ next: (...args) => { selectedKeyNextHandler(...args); resolve(); @@ -75,6 +105,8 @@ describe('WebStorage Storage API', () => { expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); expect(selectedKeyNextHandler).toHaveBeenCalledWith({ key: 'correctKey', + presence: 'present', + value: mockData, newValue: mockData, }); }); @@ -105,6 +137,8 @@ describe('WebStorage Storage API', () => { expect(selectedKeyNextHandler).toHaveBeenCalledTimes(1); expect(selectedKeyNextHandler).toHaveBeenCalledWith({ key: 'correctKey', + presence: 'absent', + value: undefined, newValue: undefined, }); }); @@ -122,6 +156,21 @@ describe('WebStorage Storage API', () => { expect(firstStorage.get(keyName)).not.toBe(secondStorage.get(keyName)); expect(firstStorage.get(keyName)).toBe('boop'); expect(secondStorage.get(keyName)).toBe('deerp'); + expect(firstStorage.snapshot(keyName)).not.toEqual( + secondStorage.snapshot(keyName), + ); + expect(firstStorage.snapshot(keyName)).toEqual({ + key: keyName, + presence: 'present', + value: 'boop', + newValue: 'boop', + }); + expect(secondStorage.snapshot(keyName)).toEqual({ + key: keyName, + presence: 'present', + value: 'deerp', + newValue: 'deerp', + }); }); it('should not clash with other namespaces when creating buckets', async () => { @@ -138,6 +187,9 @@ describe('WebStorage Storage API', () => { await firstStorage.set('test2', { error: true }); expect(secondStorage.get('deep/test2')).toBe(undefined); + expect(secondStorage.snapshot('deep/test2')).toMatchObject({ + presence: 'absent', + }); }); it('should not reuse storage instances between different rootStorages', async () => { @@ -151,5 +203,17 @@ describe('WebStorage Storage API', () => { expect(firstStorage.get('test2')).toBe(true); expect(secondStorage.get('test2')).toBe(undefined); + expect(firstStorage.snapshot('test2')).toEqual({ + key: 'test2', + presence: 'present', + value: true, + newValue: true, + }); + expect(secondStorage.snapshot('test2')).toEqual({ + key: 'test2', + presence: 'absent', + value: undefined, + newValue: undefined, + }); }); }); diff --git a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts index 6378a77474..f71d2602b5 100644 --- a/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts +++ b/packages/test-utils/src/testUtils/apis/StorageApi/MockStorageApi.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { StorageApi, StorageValueChange } from '@backstage/core-plugin-api'; -import { Observable } from '@backstage/types'; +import { StorageApi, StorageValueSnapshot } from '@backstage/core-plugin-api'; +import { JsonValue, Observable } from '@backstage/types'; import ObservableImpl from 'zen-observable'; /** @@ -62,20 +62,48 @@ export class MockStorageApi implements StorageApi { } get(key: string): T | undefined { - return this.data[this.getKeyName(key)]; + return this.snapshot(key).value as T | undefined; + } + + snapshot(key: string): StorageValueSnapshot { + if (this.data.hasOwnProperty(this.getKeyName(key))) { + const data = this.data[this.getKeyName(key)]; + return { + key, + presence: 'present', + value: data, + newValue: data, + }; + } + return { + key, + presence: 'absent', + value: undefined, + newValue: undefined, + }; } async set(key: string, data: T): Promise { this.data[this.getKeyName(key)] = data; - this.notifyChanges({ key, newValue: data }); + this.notifyChanges({ + key, + presence: 'present', + value: data, + newValue: data, + }); } async remove(key: string): Promise { delete this.data[this.getKeyName(key)]; - this.notifyChanges({ key, newValue: undefined }); + this.notifyChanges({ + key, + presence: 'absent', + value: undefined, + newValue: undefined, + }); } - observe$(key: string): Observable> { + observe$(key: string): Observable> { return this.observable.filter(({ key: messageKey }) => messageKey === key); } @@ -83,22 +111,22 @@ export class MockStorageApi implements StorageApi { return `${this.namespace}/${encodeURIComponent(key)}`; } - private notifyChanges(message: StorageValueChange) { + private notifyChanges(message: StorageValueSnapshot) { for (const subscription of this.subscribers) { subscription.next(message); } } private subscribers = new Set< - ZenObservable.SubscriptionObserver + ZenObservable.SubscriptionObserver> >(); - private readonly observable = new ObservableImpl( - subscriber => { - this.subscribers.add(subscriber); - return () => { - this.subscribers.delete(subscriber); - }; - }, - ); + private readonly observable = new ObservableImpl< + StorageValueSnapshot + >(subscriber => { + this.subscribers.add(subscriber); + return () => { + this.subscribers.delete(subscriber); + }; + }); } 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/packages/test-utils/src/testUtils/logCollector.ts b/packages/test-utils/src/testUtils/logCollector.ts index 25629f2c13..b9fa6220ed 100644 --- a/packages/test-utils/src/testUtils/logCollector.ts +++ b/packages/test-utils/src/testUtils/logCollector.ts @@ -73,7 +73,6 @@ export function withLogCollector( /** * Log collector that collect logs either from a sync or async collector. * @public - * @deprecated import from test-utils instead * */ export function withLogCollector( logsToCollect: LogFuncs[] | LogCollector, diff --git a/packages/test-utils/src/testUtils/msw/index.ts b/packages/test-utils/src/testUtils/msw/index.ts index 40bc47e8f0..625ef0e0fc 100644 --- a/packages/test-utils/src/testUtils/msw/index.ts +++ b/packages/test-utils/src/testUtils/msw/index.ts @@ -14,20 +14,6 @@ * limitations under the License. */ -/** - * @deprecated use {@link setupRequestMockHandlers} instead which can be called directly with the worker. - * @public - */ -export const msw = { - setupDefaultHandlers: (worker: { - listen: (t: any) => void; - close: () => void; - resetHandlers: () => void; - }) => { - setupRequestMockHandlers(worker); - }, -}; - /** * Sets up handlers for request mocking * @public 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/apache-airflow/src/api/ApacheAirflowApi.ts b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts index b0a0229df4..dde8c57802 100644 --- a/plugins/apache-airflow/src/api/ApacheAirflowApi.ts +++ b/plugins/apache-airflow/src/api/ApacheAirflowApi.ts @@ -19,7 +19,6 @@ import { Dag, InstanceStatus, InstanceVersion } from './types'; export const apacheAirflowApiRef = createApiRef({ id: 'plugin.apacheairflow.service', - description: 'Used by the Apache Airflow plugin to make requests', }); export type ApacheAirflowApi = { 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/api-docs/src/config.ts b/plugins/api-docs/src/config.ts index e035bcfbd1..979340164b 100644 --- a/plugins/api-docs/src/config.ts +++ b/plugins/api-docs/src/config.ts @@ -20,7 +20,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const apiDocsConfigRef = createApiRef({ id: 'plugin.api-docs.config', - description: 'Used to configure api-docs widgets', }); export interface ApiDocsConfig { 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/api/AzureDevOpsApi.ts b/plugins/azure-devops/src/api/AzureDevOpsApi.ts index 80d36f0adf..d6e926b9a5 100644 --- a/plugins/azure-devops/src/api/AzureDevOpsApi.ts +++ b/plugins/azure-devops/src/api/AzureDevOpsApi.ts @@ -27,8 +27,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const azureDevOpsApiRef = createApiRef({ id: 'plugin.azure-devops.service', - description: - 'Used by the Azure DevOps plugin to make requests to accompanying backend', }); export interface AzureDevOpsApi { 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/bazaar/src/components/AlertBanner/index.ts b/plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/index.ts similarity index 93% rename from plugins/bazaar/src/components/AlertBanner/index.ts rename to plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/index.ts index 1728cab9fc..2e87dfea0c 100644 --- a/plugins/bazaar/src/components/AlertBanner/index.ts +++ b/plugins/azure-devops/src/components/PullRequestsPage/lib/hooks/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { AlertBanner } from './AlertBanner'; +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/badges/src/api/types.ts b/plugins/badges/src/api/types.ts index 499c144fca..884daa08ea 100644 --- a/plugins/badges/src/api/types.ts +++ b/plugins/badges/src/api/types.ts @@ -19,7 +19,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const badgesApiRef = createApiRef({ id: 'plugin.badges.client', - description: 'Used to make requests to the badges backend', }); export type BadgeStyle = diff --git a/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js new file mode 100644 index 0000000000..03ed0a8d05 --- /dev/null +++ b/plugins/bazaar-backend/migrations/20211117092217_optional_entity_ref.js @@ -0,0 +1,171 @@ +/* + * 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. + */ + +exports.up = async function up(knex) { + if (knex.client.config.client === 'sqlite3') { + await knex.schema.dropTable('metadata'); + await knex.schema.createTable('metadata', table => { + table.increments('id').comment('Automatically generated unique ID'); + table.text('entity_ref').unique().comment('The ref of the entity'); + table.text('name').notNullable().comment('The name of the entity'); + table + .text('community') + .comment('Link to where the community can discuss ideas'); + table + .text('description') + .notNullable() + .comment('The description of the Bazaar project'); + table + .text('status') + .defaultTo('proposed') + .notNullable() + .comment('The status of the Bazaar project'); + table + .text('updated_at') + .notNullable() + .comment('Timestamp on ISO 8601 format when entity was last updated'); + table + .text('size') + .defaultTo('medium') + .notNullable() + .comment('The estimated magnitude of the project'); + table + .text('start_date') + .comment('Optional start date of the project (ISO 8601 format)'); + table + .text('end_date') + .comment('Optional end date of the project (ISO 8601 format)'); + table + .text('responsible') + .notNullable() + .comment('Contact person of the project'); + }); + + await knex.schema.dropTable('members'); + await knex.schema.createTable('members', table => { + table + .integer('item_id') + .references('metadata.id') + .onDelete('CASCADE') + .comment('Id of the associated item'); + table + .text('entity_ref') + .references('metadata.entity_ref') + .onDelete('CASCADE') + .comment('The ref of the entity'); + table.text('user_id').notNullable().comment('The user id of the member'); + table + .dateTime('join_date') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this member joined'); + table.text('picture').comment('Link to profile picture'); + }); + } else { + await knex.schema.alterTable('metadata', table => { + table.renameColumn('announcement', 'description'); + table.increments('id').comment('Automatically generated unique ID'); + table.string('entity_ref').nullable().alter(); + }); + + await knex.schema.alterTable('members', table => { + table + .integer('item_id') + .references('metadata.id') + .onDelete('CASCADE') + .comment('Id of the associated item'); + table.dropColumn('entity_ref'); + }); + } +}; + +exports.down = async function down(knex) { + if (knex.client.config.client === 'sqlite3') { + await knex.schema.dropTable('metadata'); + await knex.schema.createTable('metadata', table => { + table + .text('entity_ref') + .notNullable() + .unique() + .comment('The ref of the entity'); + table.text('name').notNullable().comment('The name of the entity'); + table + .text('community') + .comment('Link to where the community can discuss ideas'); + table + .text('announcement') + .notNullable() + .comment('The announcement of the Bazaar project'); + table + .text('status') + .defaultTo('proposed') + .notNullable() + .comment('The status of the Bazaar project'); + table + .text('updated_at') + .notNullable() + .comment('Timestamp on ISO 8601 format when entity was last updated'); + table + .text('size') + .defaultTo('medium') + .notNullable() + .comment('The estimated magnitude of the project'); + table + .text('start_date') + .comment('Optional start date of the project (ISO 8601 format)'); + table + .text('end_date') + .comment('Optional end date of the project (ISO 8601 format)'); + table + .text('responsible') + .notNullable() + .comment('Contact person of the project'); + }); + + await knex.schema.dropTable('members'); + await knex.schema.createTable('members', table => { + table + .text('entity_ref') + .notNullable() + .references('metadata.entity_ref') + .onDelete('CASCADE') + .comment('The ref of the entity'); + table.text('user_id').notNullable().comment('The user id of the member'); + table + .dateTime('join_date') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this member joined'); + table.text('picture').comment('Link to profile picture'); + }); + } else { + await knex.schema.alterTable('metadata', table => { + table.renameColumn('description', 'announcement'); + table.string('entity_ref').notNullable().alter(); + table.dropColumn('id'); + }); + + await knex.schema.alterTable('members', table => { + table.dropColumn('item_id'); + table + .text('entity_ref') + .notNullable() + .references('metadata.entity_ref') + .onDelete('CASCADE') + .comment('The ref of the entity'); + }); + } +}; diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts index 3afde44d5e..3099e5bbb2 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.test.ts @@ -22,13 +22,14 @@ const bazaarProject: any = { entityRef: 'ref1', community: '', status: 'proposed', - announcement: 'a', + description: 'a', membersCount: 0, startDate: '2021-11-07T13:27:00.000Z', endDate: null, size: 'small', responsible: 'r', }; + describe('DatabaseHandler', () => { const databases = TestDatabases.create({ ids: ['POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], @@ -50,7 +51,7 @@ describe('DatabaseHandler', () => { await knex('metadata').insert({ entity_ref: bazaarProject.entityRef, name: bazaarProject.name, - announcement: bazaarProject.announcement, + description: bazaarProject.description, community: bazaarProject.community, status: bazaarProject.status, updated_at: new Date().toISOString(), @@ -60,10 +61,10 @@ describe('DatabaseHandler', () => { responsible: bazaarProject.responsible, }); - const res = await dbHandler.getMetadata('ref1'); + const res = await dbHandler.getMetadataByRef('ref1'); expect(res).toHaveLength(1); - expect(res[0].announcement).toEqual('a'); + expect(res[0].description).toEqual('a'); expect(res[0].community).toEqual(''); expect(res[0].status).toEqual('proposed'); expect(res[0].start_date).toEqual('2021-11-07T13:27:00.000Z'); diff --git a/plugins/bazaar-backend/src/service/DatabaseHandler.ts b/plugins/bazaar-backend/src/service/DatabaseHandler.ts index 60f579af2d..7da06a54b8 100644 --- a/plugins/bazaar-backend/src/service/DatabaseHandler.ts +++ b/plugins/bazaar-backend/src/service/DatabaseHandler.ts @@ -44,10 +44,10 @@ export class DatabaseHandler { } private columns = [ - 'members.entity_ref', + 'metadata.id', 'metadata.entity_ref', 'metadata.name', - 'metadata.announcement', + 'metadata.description', 'metadata.status', 'metadata.updated_at', 'metadata.community', @@ -57,40 +57,52 @@ export class DatabaseHandler { 'metadata.responsible', ]; - async getMembers(entityRef: string) { + async getMembers(id: string) { return await this.database .select('*') .from('members') - .where({ entity_ref: entityRef }); + .where({ item_id: id }); } - async addMember(userId: string, entityRef: string, picture?: string) { + async addMember(id: number, userId: string, picture?: string) { await this.database .insert({ - entity_ref: entityRef, + item_id: id, user_id: userId, picture: picture, }) .into('members'); } - async deleteMember(userId: string, entityRef: string) { + async deleteMember(id: number, userId: string) { return await this.database('members') - .where({ entity_ref: decodeURIComponent(entityRef) }) + .where({ item_id: id }) .andWhere('user_id', userId) .del(); } - async getMetadata(entityRef: string) { + async getMetadataById(id: number) { const coalesce = this.database.raw( - 'coalesce(count(members.entity_ref), 0) as members_count', + 'coalesce(count(members.item_id), 0) as members_count', + ); + + return await this.database('metadata') + .select([...this.columns, coalesce]) + .where({ 'metadata.id': id }) + .groupBy(this.columns) + .leftJoin('members', 'metadata.id', '=', 'members.item_id'); + } + + async getMetadataByRef(entityRef: string) { + const coalesce = this.database.raw( + 'coalesce(count(members.item_id), 0) as members_count', ); return await this.database('metadata') .select([...this.columns, coalesce]) .where({ 'metadata.entity_ref': entityRef }) .groupBy(this.columns) - .leftJoin('members', 'metadata.entity_ref', '=', 'members.entity_ref'); + .leftJoin('members', 'metadata.id', '=', 'members.item_id'); } async insertMetadata(bazaarProject: any) { @@ -98,7 +110,7 @@ export class DatabaseHandler { name, entityRef, community, - announcement, + description, status, size, startDate, @@ -108,11 +120,11 @@ export class DatabaseHandler { await this.database .insert({ - name: name, + name, entity_ref: entityRef, - community: community, - announcement: announcement, - status: status, + community, + description, + status, updated_at: new Date().toISOString(), size, start_date: startDate, @@ -124,9 +136,11 @@ export class DatabaseHandler { async updateMetadata(bazaarProject: any) { const { + name, + id, entityRef, community, - announcement, + description, status, size, startDate, @@ -134,34 +148,32 @@ export class DatabaseHandler { responsible, } = bazaarProject; - return await this.database('metadata') - .where({ entity_ref: entityRef }) - .update({ - announcement: announcement, - community: community, - status: status, - updated_at: new Date().toISOString(), - size, - start_date: startDate, - end_date: endDate, - responsible, - }); + return await this.database('metadata').where({ id: id }).update({ + name, + entity_ref: entityRef, + description, + community, + status, + updated_at: new Date().toISOString(), + size, + start_date: startDate, + end_date: endDate, + responsible, + }); } - async deleteMetadata(entityRef: string) { - return await this.database('metadata') - .where({ entity_ref: entityRef }) - .del(); + async deleteMetadata(id: number) { + return await this.database('metadata').where({ id: id }).del(); } - async getEntities() { + async getProjects() { const coalesce = this.database.raw( - 'coalesce(count(members.entity_ref), 0) as members_count', + 'coalesce(count(members.item_id), 0) as members_count', ); return await this.database('metadata') .select([...this.columns, coalesce]) .groupBy(this.columns) - .leftJoin('members', 'metadata.entity_ref', '=', 'members.entity_ref'); + .leftJoin('members', 'metadata.id', '=', 'members.item_id'); } } diff --git a/plugins/bazaar-backend/src/service/router.ts b/plugins/bazaar-backend/src/service/router.ts index f0bc02c65b..730d958fcd 100644 --- a/plugins/bazaar-backend/src/service/router.ts +++ b/plugins/bazaar-backend/src/service/router.ts @@ -40,28 +40,27 @@ export async function createRouter( const router = Router(); router.use(express.json()); - router.get('/members/:ref', async (request, response) => { - const entity_ref = decodeURIComponent(request.params.ref); - const data = await dbHandler.getMembers(entity_ref); + router.get('/projects/:id/members', async (request, response) => { + const members = await dbHandler.getMembers(request.params.id); - if (data?.length) { - response.json({ status: 'ok', data: data }); + if (members?.length) { + response.json({ status: 'ok', data: members }); } else { response.json({ status: 'ok', data: [] }); } }); - router.put('/member', async (request, response) => { - const { user_id, entity_ref, picture } = request.body; - await dbHandler.addMember(user_id, entity_ref, picture); + router.put('/projects/:id/member/:userId', async (request, response) => { + const { id, userId } = request.params; + await dbHandler.addMember(parseInt(id, 10), userId, request.body?.picture); response.json({ status: 'ok' }); }); - router.delete('/member/:ref/:id', async (request, response) => { - const { ref, id } = request.params; + router.delete('/projects/:id/member/:userId', async (request, response) => { + const { id, userId } = request.params; - const count = await dbHandler.deleteMember(id, ref); + const count = await dbHandler.deleteMember(parseInt(id, 10), userId); if (count) { response.json({ status: 'ok' }); @@ -70,36 +69,48 @@ export async function createRouter( } }); - router.get('/metadata/:ref', async (request, response) => { + router.get('/projects/id/:id', async (request, response) => { + const id = decodeURIComponent(request.params.id); + + const data = await dbHandler.getMetadataById(parseInt(id, 10)); + + response.json({ status: 'ok', data: data }); + }); + + router.get('/projects/ref/:ref', async (request, response) => { const ref = decodeURIComponent(request.params.ref); - const data = await dbHandler.getMetadata(ref); + const data = await dbHandler.getMetadataByRef(ref); response.json({ status: 'ok', data: data }); }); - router.get('/entities', async (_, response) => { - const data = await dbHandler.getEntities(); + router.get('/projects', async (_, response) => { + const data = await dbHandler.getProjects(); response.json({ status: 'ok', data: data }); }); - router.put('/metadata', async (request, response) => { + router.put('/projects', async (request, response) => { const bazaarProject = request.body; const count = await dbHandler.updateMetadata(bazaarProject); if (count) { response.json({ status: 'ok' }); - } else { - await dbHandler.insertMetadata(bazaarProject); - response.json({ status: 'ok' }); } }); - router.delete('/metadata/:ref', async (request, response) => { - const ref = decodeURIComponent(request.params.ref); + router.post('/projects', async (request, response) => { + const bazaarProject = request.body; - const count = await dbHandler.deleteMetadata(ref); + await dbHandler.insertMetadata(bazaarProject); + response.json({ status: 'ok' }); + }); + + router.delete('/projects/:id', async (request, response) => { + const id = decodeURIComponent(request.params.id); + + const count = await dbHandler.deleteMetadata(parseInt(id, 10)); if (count) { response.json({ status: 'ok' }); diff --git a/plugins/bazaar/README.md b/plugins/bazaar/README.md index f07e751984..dea25727b0 100644 --- a/plugins/bazaar/README.md +++ b/plugins/bazaar/README.md @@ -69,61 +69,42 @@ const overviewContent = ( ### Layout -The latest modified Bazaar projects are displayed in the Bazaar landing page, located at the Bazaar icon in the sidebar. Each project is represented as a card containing its most relevant data to give an overview of the project. The list of project is paginated. +The latest modified Bazaar projects are displayed in the Bazaar landing page, located at the Bazaar icon in the sidebar. Each project is represented as a card containing its most relevant data to give an overview of the project. It is also possible to sort in alphabetical order or on the number of members. Here you can also search or add project to the Bazaar. -![home](media/bazaar_pr_fullscreen.png) +![home](media/layout.png) ### Workflow -To add a project to the Bazaar, you need to create a project with one of the templates in Backstage. Click the add project-button, choose the project and fill in the form. +To add a project to the bazaar, simply click on the `add-project` button and fill in the form. The following fields are mandatory: -- announcement - present your idea and what skills you are looking for +- name - name of the project on URL safe format +- description - present your idea and what skills you are looking for - status - whether or not the project has started - size - small, medium or large - responsible - main contact person of the project The other fields are: +- project - link Bazaar project to existing entity in the catalog +- community link - link to where the project members can communicate, e.g. Teams or Discord link - start date - end date -- community link - link where the project members can chat, e.g. Teams or Discord link -When the project is added, you will see the Bazaar information in the Bazaar card on the entity page. There you can join a project, edit or delete it. +When clicking on a Bazaar project a card containing the Bazaar information will show up. If the Bazaar project is linked to an entity, the card is also visible on that entity's EntityPage. From that card it is possible to either link or unlink an entity to a project, edit or delete the project and join the project if it seems interesting to you. Once you have joined a project, you will get access to the community link if it exists. -### Database - -The metadata related to the Bazaar is stored in a database. Right now there are two tables, one for storing the metadata and one for storing the members of a Bazaar project. - -**metadata**: - -- name - name of the entity -- entity_ref - namespace/kind/name of the entity -- announcement - announcement of the project and its current need of skills/team member -- status - status of the project, 'proposed' or 'ongoing' -- updated_at - date when the Bazaar information was last modified (ISO 8601 format) -- size - small, medium or large -- start_date - date when the project is estimated to start (ISO 8601 format) -- end_date - date when the project is estimated to end (ISO 8601 format) -- responsible - main contact person of the project - -**members**: - -- entity_ref - namespace/kind/name of the entity -- user_name -- join_date - date when the user joined the project (ISO 8601 format) +![home](media/demo.gif) ## Future work and ideas - Workflow - - Make it possible to add a Bazaar project without linking it to a Backstage entity, this would make it easier to just add an idea to the Bazaar. + - Make it possible for multiple Bazaar project to link to the same catalog entity - Bazaar landing page - Add a tab 'My page', where your personal data is displayed. For example: your projects and its latest activities etc. - - Make it possible to sort the project based on e.g. the number of members - Bazaar tab on the EntityPage @@ -133,6 +114,3 @@ The metadata related to the Bazaar is stored in a database. Right now there are - Dialogues - Extend the dialogue for adding a project with more fields, e.g. the possibility to add images - -- Testing - - Add tests to all the components diff --git a/plugins/bazaar/media/bazaar_pr_fullscreen.png b/plugins/bazaar/media/bazaar_pr_fullscreen.png deleted file mode 100644 index 6c5cf1277b..0000000000 Binary files a/plugins/bazaar/media/bazaar_pr_fullscreen.png and /dev/null differ diff --git a/plugins/bazaar/media/demo.gif b/plugins/bazaar/media/demo.gif new file mode 100644 index 0000000000..6369ce7551 Binary files /dev/null and b/plugins/bazaar/media/demo.gif differ diff --git a/plugins/bazaar/media/layout.png b/plugins/bazaar/media/layout.png new file mode 100644 index 0000000000..cc02a374a3 Binary files /dev/null and b/plugins/bazaar/media/layout.png differ diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index 3db79b9cdf..6ccda73f93 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", @@ -34,6 +34,7 @@ "@material-ui/pickers": "^3.3.10", "@testing-library/jest-dom": "^5.10.1", "luxon": "^2.0.2", + "material-ui-search-bar": "^1.0.0", "react-hook-form": "^7.13.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^17.2.4" @@ -42,7 +43,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/bazaar/plugin-bazaar.api.md b/plugins/bazaar/plugin-bazaar.api.md new file mode 100644 index 0000000000..33a674ffc4 --- /dev/null +++ b/plugins/bazaar/plugin-bazaar.api.md @@ -0,0 +1,25 @@ +## API Report File for "@backstage/plugin-bazaar" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import * as _backstage_core_plugin_api from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const BazaarPage: () => JSX.Element; + +// @public (undocumented) +export const bazaarPlugin: _backstage_core_plugin_api.BackstagePlugin< + { + root: _backstage_core_plugin_api.RouteRef; + }, + {} +>; + +// @public (undocumented) +export const EntityBazaarInfoCard: () => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/bazaar/src/api.ts b/plugins/bazaar/src/api.ts index db20e5e0cb..9f5010ec43 100644 --- a/plugins/bazaar/src/api.ts +++ b/plugins/bazaar/src/api.ts @@ -14,33 +14,34 @@ * limitations under the License. */ -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { createApiRef, DiscoveryApi, IdentityApi, } from '@backstage/core-plugin-api'; -import { BazaarProject } from './types'; export const bazaarApiRef = createApiRef({ id: 'bazaar', - description: 'Used to make requests towards the bazaar backend', }); export interface BazaarApi { - updateMetadata(bazaarProject: BazaarProject): Promise; + updateProject(bazaarProject: any): Promise; - getMetadata(entity: Entity): Promise; + addProject(bazaarProject: any): Promise; - getMembers(entity: Entity): Promise; + getProjectById(id: number): Promise; - deleteMember(entity: Entity): Promise; + getProjectByRef(entityRef: string): Promise; - addMember(entity: Entity): Promise; + getMembers(id: number): Promise; - getEntities(): Promise; + deleteMember(id: number, userId: string): Promise; - deleteEntity(bazaarProject: BazaarProject): Promise; + addMember(id: number, userId: string): Promise; + + getProjects(): Promise; + + deleteProject(id: number): Promise; } export class BazaarClient implements BazaarApi { @@ -55,10 +56,10 @@ export class BazaarClient implements BazaarApi { this.discoveryApi = options.discoveryApi; } - async updateMetadata(bazaarProject: BazaarProject): Promise { + async updateProject(bazaarProject: any): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - return await fetch(`${baseUrl}/metadata`, { + return await fetch(`${baseUrl}/projects`, { method: 'PUT', headers: { Accept: 'application/json', @@ -68,11 +69,34 @@ export class BazaarClient implements BazaarApi { }).then(resp => resp.json()); } - async getMetadata(entity: Entity): Promise { + async addProject(bazaarProject: any): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); + + return await fetch(`${baseUrl}/projects`, { + method: 'POST', + headers: { + Accept: 'application/json', + 'Content-Type': 'application/json', + }, + body: JSON.stringify(bazaarProject), + }).then(resp => resp.json()); + } + + async getProjectById(id: number): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); + + const response = await fetch(`${baseUrl}/projects/id/${id}`, { + method: 'GET', + }); + + return response.ok ? response : null; + } + + async getProjectByRef(entityRef: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); const response = await fetch( - `${baseUrl}/metadata/${encodeURIComponent(stringifyEntityRef(entity))}`, + `${baseUrl}/projects/ref/${encodeURIComponent(entityRef)}`, { method: 'GET', }, @@ -81,60 +105,49 @@ export class BazaarClient implements BazaarApi { return response.ok ? response : null; } - async getMembers(entity: Entity): Promise { + async getMembers(id: number): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - return await fetch( - `${baseUrl}/members/${encodeURIComponent(stringifyEntityRef(entity))}`, - { - method: 'GET', - }, - ).then(resp => resp.json()); + return await fetch(`${baseUrl}/projects/${id}/members`, { + method: 'GET', + }).then(resp => resp.json()); } - async addMember(entity: Entity): Promise { + async addMember(id: number, userId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - await fetch(`${baseUrl}/member`, { + await fetch(`${baseUrl}/projects/${id}/member/${userId}`, { method: 'PUT', headers: { Accept: 'application/json', 'Content-Type': 'application/json', }, body: JSON.stringify({ - entity_ref: stringifyEntityRef(entity), - user_id: this.identityApi.getUserId(), picture: this.identityApi.getProfile()?.picture, }), }); } - async deleteMember(entity: Entity): Promise { + async deleteMember(id: number, userId: string): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - await fetch( - `${baseUrl}/member/${encodeURIComponent( - stringifyEntityRef(entity), - )}/${this.identityApi.getUserId()}`, - { - method: 'DELETE', - }, - ); + await fetch(`${baseUrl}/projects/${id}/member/${userId}`, { + method: 'DELETE', + }); } - async getEntities(): Promise { + async getProjects(): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - return await fetch(`${baseUrl}/entities`, { + return await fetch(`${baseUrl}/projects`, { method: 'GET', }).then(resp => resp.json()); } - async deleteEntity(bazaarProject: BazaarProject): Promise { + async deleteProject(id: number): Promise { const baseUrl = await this.discoveryApi.getBaseUrl('bazaar'); - const entityRef = bazaarProject.entityRef as string; - await fetch(`${baseUrl}/metadata/${encodeURIComponent(entityRef)}`, { + await fetch(`${baseUrl}/projects/${id}`, { method: 'DELETE', }); } diff --git a/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx b/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx index 310ffe051d..4333ff11fd 100644 --- a/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx +++ b/plugins/bazaar/src/components/AddProjectDialog/AddProjectDialog.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useState, useEffect } from 'react'; +import React, { useState } from 'react'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { UseFormReset, UseFormGetValues } from 'react-hook-form'; import { useApi } from '@backstage/core-plugin-api'; @@ -39,18 +39,13 @@ export const AddProjectDialog = ({ fetchCatalogEntities, }: Props) => { const bazaarApi = useApi(bazaarApiRef); - const [selectedEntity, setSelectedEntity] = useState( - catalogEntities ? catalogEntities[0] : null, - ); - - useEffect(() => { - setSelectedEntity(catalogEntities ? catalogEntities[0] : null); - }, [catalogEntities]); + const [selectedEntity, setSelectedEntity] = useState(null); const defaultValues = { + name: '', title: 'Add project', community: '', - announcement: '', + description: '', status: 'proposed' as Status, size: 'medium' as Size, responsible: '', @@ -58,59 +53,48 @@ export const AddProjectDialog = ({ endDate: null, }; - const handleListItemClick = (entity: Entity) => { + const handleEntityClick = (entity: Entity) => { setSelectedEntity(entity); }; - const handleCloseDialog = () => { - setSelectedEntity(catalogEntities ? catalogEntities[0] : null); - handleClose(); - }; - - const handleSave: any = async ( + const handleSubmit: any = async ( getValues: UseFormGetValues, reset: UseFormReset, ) => { const formValues = getValues(); + const response = await bazaarApi.addProject({ + ...formValues, + entityRef: selectedEntity ? stringifyEntityRef(selectedEntity) : null, + startDate: formValues.startDate ?? null, + endDate: formValues.endDate ?? null, + } as BazaarProject); - if (selectedEntity) { - await bazaarApi.updateMetadata({ - name: selectedEntity.metadata.name, - entityRef: stringifyEntityRef(selectedEntity), - announcement: formValues.announcement, - status: formValues.status, - community: formValues.community, - membersCount: 0, - size: formValues.size, - startDate: formValues.startDate ?? null, - endDate: formValues.endDate ?? null, - responsible: formValues.responsible, - } as BazaarProject); - + if (response.status === 'ok') { fetchBazaarProjects(); fetchCatalogEntities(); - - handleClose(); - reset(defaultValues); } + + handleClose(); + reset(defaultValues); }; return ( } - handleClose={handleCloseDialog} + handleClose={handleClose} /> ); }; diff --git a/plugins/bazaar/src/components/AlertBanner/AlertBanner.tsx b/plugins/bazaar/src/components/AlertBanner/AlertBanner.tsx deleted file mode 100644 index 1b6c1331c3..0000000000 --- a/plugins/bazaar/src/components/AlertBanner/AlertBanner.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2021 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { Snackbar, IconButton } from '@material-ui/core'; -import CloseIcon from '@material-ui/icons/Close'; -import { Alert } from '@material-ui/lab'; - -type Props = { - open: boolean; - message: JSX.Element; - handleClose: () => void; -}; - -export const AlertBanner = ({ open, message, handleClose }: Props) => { - return ( - - - - - } - > - {message} - - - ); -}; diff --git a/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx b/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx new file mode 100644 index 0000000000..5145dc54b8 --- /dev/null +++ b/plugins/bazaar/src/components/CardContentFields/CardContentFields.tsx @@ -0,0 +1,168 @@ +/* + * 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 React from 'react'; +import { + Grid, + makeStyles, + Card, + CardContent, + Typography, + Link, + GridSize, +} from '@material-ui/core'; +import { Avatar } from '@backstage/core-components'; +import { AboutField } from '@backstage/plugin-catalog'; +import { StatusTag } from '../StatusTag'; +import { Member, BazaarProject } from '../../types'; + +const useStyles = makeStyles({ + break: { + wordBreak: 'break-word', + }, +}); + +type Props = { + bazaarProject: BazaarProject; + members: Member[]; + descriptionSize: GridSize; + membersSize: GridSize; +}; + +export const CardContentFields = ({ + bazaarProject, + members, + descriptionSize, + membersSize, +}: Props) => { + const classes = useStyles(); + + return ( +
+ + + + + + {bazaarProject.description + .split('\n') + .map((str: string, i: number) => ( + + {str} + + ))} + + + + + + {members.length ? ( + members.slice(0, 7).map((member: Member) => { + return ( +
+ + + {member?.userId} + +
+ ); + }) + ) : ( +
+ )} + + + + + + + + + + + + {bazaarProject.size} + + + + + + + {bazaarProject.startDate?.substring(0, 10) || ''} + + + + + + + + {bazaarProject.endDate?.substring(0, 10) || ''} + + + + + + + + {bazaarProject.responsible || ''} + + + + + + +
+ ); +}; diff --git a/plugins/bazaar/src/components/DeleteProjectDialog/index.ts b/plugins/bazaar/src/components/CardContentFields/index.ts similarity index 90% rename from plugins/bazaar/src/components/DeleteProjectDialog/index.ts rename to plugins/bazaar/src/components/CardContentFields/index.ts index fdd488aeb9..e6f990322b 100644 --- a/plugins/bazaar/src/components/DeleteProjectDialog/index.ts +++ b/plugins/bazaar/src/components/CardContentFields/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { DeleteProjectDialog } from './DeleteProjectDialog'; +export { CardContentFields } from './CardContentFields'; diff --git a/plugins/bazaar/src/components/ConfirmationDialog/ConfirmationDialog.tsx b/plugins/bazaar/src/components/ConfirmationDialog/ConfirmationDialog.tsx new file mode 100644 index 0000000000..1e00ac5e30 --- /dev/null +++ b/plugins/bazaar/src/components/ConfirmationDialog/ConfirmationDialog.tsx @@ -0,0 +1,62 @@ +/* + * 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 React from 'react'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import { + CustomDialogTitle, + DialogActions, + DialogContent, +} from '../CustomDialogTitle'; + +type Props = { + open: boolean; + handleClose: () => void; + message: (string | JSX.Element)[]; + type: 'delete' | 'unlink'; + handleSubmit: () => void; +}; + +export const ConfirmationDialog = ({ + open, + handleClose, + message, + type, + handleSubmit, +}: Props) => { + return ( + + + {type.charAt(0).toLocaleUpperCase('en-US') + type.slice(1)} project + + + {message} + + + + + + ); +}; diff --git a/plugins/bazaar/src/components/ConfirmationDialog/index.ts b/plugins/bazaar/src/components/ConfirmationDialog/index.ts new file mode 100644 index 0000000000..a3252eb6a9 --- /dev/null +++ b/plugins/bazaar/src/components/ConfirmationDialog/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 { ConfirmationDialog } from './ConfirmationDialog'; diff --git a/plugins/bazaar/src/components/CustomDialogTitle/CustomDialogTitle.tsx b/plugins/bazaar/src/components/CustomDialogTitle/CustomDialogTitle.tsx new file mode 100644 index 0000000000..2cf8b13874 --- /dev/null +++ b/plugins/bazaar/src/components/CustomDialogTitle/CustomDialogTitle.tsx @@ -0,0 +1,87 @@ +/* + * 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 React from 'react'; +import IconButton from '@material-ui/core/IconButton'; +import CloseIcon from '@material-ui/icons/Close'; +import Typography from '@material-ui/core/Typography'; +import MuiDialogTitle from '@material-ui/core/DialogTitle'; +import { + Theme, + WithStyles, + withStyles, + createStyles, +} from '@material-ui/core/styles'; +import MuiDialogContent from '@material-ui/core/DialogContent'; +import MuiDialogActions from '@material-ui/core/DialogActions'; + +/* + DialogTitleProps, DialogTitle, DialogContent and DialogActions + are copied from the git-release plugin +*/ + +export interface DialogTitleProps extends WithStyles { + id: string; + children: React.ReactNode; + onClose: () => void; +} + +const styles = (theme: Theme) => + createStyles({ + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }); + +export const DialogContent = withStyles((theme: Theme) => ({ + root: { + padding: theme.spacing(2), + }, +}))(MuiDialogContent); + +export const DialogActions = withStyles((theme: Theme) => ({ + root: { + margin: 0, + padding: theme.spacing(1), + }, +}))(MuiDialogActions); + +export const CustomDialogTitle = withStyles(styles)( + (props: DialogTitleProps) => { + const { children, classes, onClose, ...other } = props; + return ( + + {children} + {onClose ? ( + + + + ) : null} + + ); + }, +); diff --git a/plugins/bazaar/src/components/CustomDialogTitle/index.ts b/plugins/bazaar/src/components/CustomDialogTitle/index.ts new file mode 100644 index 0000000000..8b615ac211 --- /dev/null +++ b/plugins/bazaar/src/components/CustomDialogTitle/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { + CustomDialogTitle, + DialogContent, + DialogActions, +} from './CustomDialogTitle'; diff --git a/plugins/bazaar/src/components/DateSelector/DateSelector.tsx b/plugins/bazaar/src/components/DateSelector/DateSelector.tsx index db564a7b68..600247a53f 100644 --- a/plugins/bazaar/src/components/DateSelector/DateSelector.tsx +++ b/plugins/bazaar/src/components/DateSelector/DateSelector.tsx @@ -45,7 +45,6 @@ export const DateSelector = ({ name, control, setValue }: Props) => { - createStyles({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, - }); - -/* - DialogTitleProps, DialogTitle, DialogContent and DialogActions - are copied from the git-release plugin -*/ -export interface DialogTitleProps extends WithStyles { - id: string; - children: React.ReactNode; - onClose: () => void; -} - -const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { - const { children, classes, onClose, ...other } = props; - return ( - - {children} - {onClose ? ( - - - - ) : null} - - ); -}); - -const DialogContent = withStyles((theme: Theme) => ({ - root: { - padding: theme.spacing(2), - }, -}))(MuiDialogContent); - -const DialogActions = withStyles((theme: Theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, -}))(MuiDialogActions); - -type Props = { - bazaarProject: BazaarProject; - openDelete: boolean; - handleClose: () => void; - setIsBazaar: Dispatch>; -}; - -export const DeleteProjectDialog = ({ - bazaarProject, - openDelete, - handleClose, - setIsBazaar, -}: Props) => { - const handleCloseAndClear = () => { - handleClose(); - }; - - const bazaarApi = useApi(bazaarApiRef); - - const handleSubmit = async () => { - await bazaarApi.deleteEntity(bazaarProject); - setIsBazaar(false); - handleCloseAndClear(); - }; - - return ( - - - Delete project - - - - Are you sure you want to delete this project from the Bazaar? - - - - - - - ); -}; diff --git a/plugins/bazaar/src/components/DoubleDateSelector/DoubleDateSelector.tsx b/plugins/bazaar/src/components/DoubleDateSelector/DoubleDateSelector.tsx index b37e734453..eb72ee67dc 100644 --- a/plugins/bazaar/src/components/DoubleDateSelector/DoubleDateSelector.tsx +++ b/plugins/bazaar/src/components/DoubleDateSelector/DoubleDateSelector.tsx @@ -17,49 +17,48 @@ import React from 'react'; import { Control, UseFormSetValue } from 'react-hook-form'; import { FormValues } from '../../types'; -import { Typography } from '@material-ui/core'; import { DateSelector } from '../DateSelector/DateSelector'; +import { Typography, makeStyles } from '@material-ui/core'; type Props = { control: Control; setValue: UseFormSetValue; }; +const useStyles = makeStyles({ + container: { + marginTop: '0.25rem', + textAlign: 'center', + display: 'flex', + flexDirection: 'row', + justifyContent: 'space-between', + }, + startDate: { + float: 'left', + }, + endDate: { + float: 'right', + }, + dash: { + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + fontSize: '1.5rem', + margin: '0 1rem', + }, +}); + export const DoubleDateSelector = ({ control, setValue }: Props) => { + const classes = useStyles(); + return ( -
-
+
+
- - - - -
+ - +
diff --git a/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx b/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx index 41c9e474d4..0b2a8ef391 100644 --- a/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx +++ b/plugins/bazaar/src/components/EditProjectDialog/EditProjectDialog.tsx @@ -15,81 +15,119 @@ */ import React, { useState, useEffect } from 'react'; -import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { useApi } from '@backstage/core-plugin-api'; import { ProjectDialog } from '../ProjectDialog'; -import { BazaarProject, FormValues, Size, Status } from '../../types'; +import { BazaarProject, FormValues } from '../../types'; import { bazaarApiRef } from '../../api'; import { UseFormGetValues } from 'react-hook-form'; +import { ConfirmationDialog } from '../ConfirmationDialog'; +import { Button, makeStyles } from '@material-ui/core'; type Props = { - entity: Entity; bazaarProject: BazaarProject; + openEdit: boolean; + handleEditClose: () => void; + handleCardClose?: () => void; fetchBazaarProject: () => Promise; - open: boolean; - handleClose: () => void; - isAddForm: boolean; }; +const useStyles = makeStyles({ + button: { + marginLeft: '0', + marginRight: 'auto', + }, +}); + export const EditProjectDialog = ({ - entity, bazaarProject, + openEdit, + handleEditClose, + handleCardClose, fetchBazaarProject, - open, - handleClose, }: Props) => { + const classes = useStyles(); + const bazaarApi = useApi(bazaarApiRef); + const [openDelete, setOpenDelete] = useState(false); const [defaultValues, setDefaultValues] = useState({ - announcement: bazaarProject.announcement, - community: bazaarProject.community, - status: bazaarProject.status, - size: bazaarProject.size, - startDate: bazaarProject?.startDate ?? null, - endDate: bazaarProject?.endDate ?? null, - responsible: bazaarProject.responsible, + ...bazaarProject, + startDate: bazaarProject.startDate ?? null, + endDate: bazaarProject.endDate ?? null, }); - const bazaarApi = useApi(bazaarApiRef); + const handleDeleteClose = () => { + setOpenDelete(false); + handleEditClose(); + + if (handleCardClose) handleCardClose(); + }; + + const handleDeleteSubmit = async () => { + await bazaarApi.deleteProject(bazaarProject.id); + + handleDeleteClose(); + fetchBazaarProject(); + }; useEffect(() => { setDefaultValues({ - announcement: bazaarProject.announcement, - community: bazaarProject.community, - status: bazaarProject.status, - size: bazaarProject.size, - startDate: bazaarProject?.startDate ?? null, - endDate: bazaarProject?.endDate ?? null, - responsible: bazaarProject.responsible, + ...bazaarProject, + startDate: bazaarProject.startDate ?? null, + endDate: bazaarProject.endDate ?? null, }); }, [bazaarProject]); - const handleSave: any = async (getValues: UseFormGetValues) => { + const handleEditSubmit: any = async ( + getValues: UseFormGetValues, + ) => { const formValues = getValues(); - const updateResponse = await bazaarApi.updateMetadata({ - name: entity.metadata.name, - entityRef: stringifyEntityRef(entity), - announcement: formValues.announcement, - status: formValues.status as Status, - community: formValues.community, + const updateResponse = await bazaarApi.updateProject({ + ...formValues, + id: bazaarProject.id, + entityRef: bazaarProject.entityRef, membersCount: bazaarProject.membersCount, - size: formValues.size as Size, startDate: formValues?.startDate ?? null, endDate: formValues?.endDate ?? null, - responsible: formValues.responsible, }); if (updateResponse.status === 'ok') fetchBazaarProject(); - handleClose(); + handleEditClose(); }; return ( - +
+ {bazaarProject.name}, + ' from the Bazaar?', + ]} + type="delete" + handleSubmit={handleDeleteSubmit} + /> + + { + setOpenDelete(true); + }} + > + Delete project + + } + /> +
); }; diff --git a/plugins/bazaar/src/components/EntityBazaarInfoCard/EntityBazaarInfoCard.tsx b/plugins/bazaar/src/components/EntityBazaarInfoCard/EntityBazaarInfoCard.tsx index acae8f5342..685d400d3e 100644 --- a/plugins/bazaar/src/components/EntityBazaarInfoCard/EntityBazaarInfoCard.tsx +++ b/plugins/bazaar/src/components/EntityBazaarInfoCard/EntityBazaarInfoCard.tsx @@ -15,347 +15,48 @@ */ import React, { useState, useEffect } from 'react'; -import { - Grid, - makeStyles, - Card, - CardContent, - CardHeader, - Typography, - Divider, - IconButton, - Popover, - MenuList, - MenuItem, - ListItemText, - Link, -} from '@material-ui/core'; -import { - Progress, - HeaderIconLinkRow, - IconLinkVerticalProps, - Avatar, -} from '@backstage/core-components'; -import { useEntity } from '@backstage/plugin-catalog-react'; -import { AboutField } from '@backstage/plugin-catalog'; -import { StatusTag } from '../StatusTag'; -import EditIcon from '@material-ui/icons/Edit'; -import ChatIcon from '@material-ui/icons/Chat'; -import PersonAddIcon from '@material-ui/icons/PersonAdd'; -import MoreVertIcon from '@material-ui/icons/MoreVert'; -import DeleteIcon from '@material-ui/icons/Delete'; -import { EditProjectDialog } from '../EditProjectDialog'; -import { DeleteProjectDialog } from '../DeleteProjectDialog'; -import ExitToAppIcon from '@material-ui/icons/ExitToApp'; -import { useApi, identityApiRef } from '@backstage/core-plugin-api'; -import { Member, BazaarProject } from '../../types'; -import { bazaarApiRef } from '../../api'; -import { Alert } from '@material-ui/lab'; import { useAsyncFn } from 'react-use'; - -const useStyles = makeStyles({ - description: { - wordBreak: 'break-word', - }, - icon: { - marginRight: '1.75rem', - }, - link: { - color: '#9cc9ff', - '&:hover': { - textDecoration: 'underline', - }, - }, - memberLink: { - display: 'block', - marginBottom: '0.3rem', - }, -}); - -const sortMembers = (m1: Member, m2: Member) => { - return new Date(m2.joinDate!).getTime() - new Date(m1.joinDate!).getTime(); -}; +import { useApi } from '@backstage/core-plugin-api'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { bazaarApiRef } from '../../api'; +import { EntityBazaarInfoContent } from '../EntityBazaarInfoContent'; +import { Card } from '@material-ui/core'; +import { parseBazaarResponse } from '../../util/parseMethods'; export const EntityBazaarInfoCard = () => { const { entity } = useEntity(); - const classes = useStyles(); const bazaarApi = useApi(bazaarApiRef); - const identity = useApi(identityApiRef); - const [anchorEl, setAnchorEl] = useState(); - const [open, setOpen] = useState(false); - const [popoverOpen, setPopoverOpen] = useState(false); - const [openDelete, setOpenDelete] = useState(false); - const [isMember, setIsMember] = useState(false); - const [isBazaar, setIsBazaar] = useState(false); - const [members, fetchMembers] = useAsyncFn(async () => { - const response = await bazaarApi.getMembers(entity); - - const dbMembers = response.data.map((obj: any) => { - const member: Member = { - userId: obj.user_id, - entityRef: obj.entity_ref, - joinDate: obj.join_date, - picture: obj.picture, - }; - - return member; - }); - - dbMembers.sort(sortMembers); - - return dbMembers; - }); const [bazaarProject, fetchBazaarProject] = useAsyncFn(async () => { - const response = await bazaarApi.getMetadata(entity); + const response = await bazaarApi.getProjectByRef( + stringifyEntityRef(entity), + ); - if (response) { - const metadata = await response.json().then((resp: any) => resp.data[0]); - - if (metadata) { - return { - entityRef: metadata.entity_ref, - name: metadata.name, - community: metadata.community, - announcement: metadata.announcement, - status: metadata.status, - updatedAt: metadata.updated_at, - membersCount: metadata.members_count, - size: metadata.size, - startDate: metadata.start_date, - endDate: metadata.end_date, - responsible: metadata.responsible, - } as BazaarProject; - } - } - return null; + return await parseBazaarResponse(response); }); + const [isBazaar, setIsBazaar] = useState(bazaarProject.value ?? false); + useEffect(() => { - fetchMembers(); fetchBazaarProject(); - }, [fetchMembers, fetchBazaarProject]); + }, [fetchBazaarProject]); useEffect(() => { - const isBazaarMember = - members?.value - ?.map((member: Member) => member.userId) - .indexOf(identity.getUserId()) >= 0; - const isBazaarProject = bazaarProject.value !== null; + const isBazaarProject = bazaarProject.value !== undefined; - setIsMember(isBazaarMember); setIsBazaar(isBazaarProject); - }, [bazaarProject, members, identity]); + }, [bazaarProject.value]); - const onOpen = (event: React.SyntheticEvent) => { - setAnchorEl(event.currentTarget); - setPopoverOpen(true); - }; - - const closeEdit = () => { - setOpen(false); - }; - - const closeDelete = () => { - setOpenDelete(false); - }; - - const popoverCloseHandler = () => { - setPopoverOpen(false); - }; - - const handleMembersClick = async () => { - if (!isMember) { - await bazaarApi.addMember(entity); - } else { - await bazaarApi.deleteMember(entity); - } - - fetchMembers(); - fetchBazaarProject(); - }; - - const links: IconLinkVerticalProps[] = [ - { - label: isMember ? 'Leave' : 'Join', - icon: isMember ? : , - href: '', - onClick: async () => { - handleMembersClick(); - }, - }, - { - label: 'Community', - icon: , - href: bazaarProject?.value?.community, - disabled: !bazaarProject?.value?.community || !isMember, - }, - ]; - - if (!isBazaar) { - return null; - } else if (bazaarProject.loading || members.loading) { - return ; - } else if (bazaarProject.error) { - return {bazaarProject?.error?.message}; - } else if (members.error) { - return {members?.error?.message}; - } - return ( - - {bazaarProject?.value && ( - + - )} - - {bazaarProject?.value && ( - - )} - - - - } - subheader={} - /> - - - - - { - setOpen(true); - setPopoverOpen(false); - }} - > - - - - - { - setOpenDelete(true); - setPopoverOpen(false); - }} - > - - - - - - - - - {bazaarProject?.value?.announcement - ? bazaarProject?.value?.announcement - .split('\n') - .map((str: string, i: number) => ( - - {str} - - )) - : 'No announcement'} - - - - - {' '} - - {members?.value?.length ? ( - members.value.slice(0, 7).map((member: Member) => { - return ( -
- - - {member?.userId} - -
- ); - }) - ) : ( -
- )} - - - - - - - - - - - - - {bazaarProject?.value?.size} - - - - - - - - {bazaarProject?.value?.startDate?.substring(0, 10) || ''} - - - - - - - - {bazaarProject?.value?.endDate?.substring(0, 10) || ''} - - - - - - - - {bazaarProject?.value?.responsible || ''} - - - - - - - ); + + ); + } + return null; }; diff --git a/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx b/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx new file mode 100644 index 0000000000..f2c005abd1 --- /dev/null +++ b/plugins/bazaar/src/components/EntityBazaarInfoContent/EntityBazaarInfoContent.tsx @@ -0,0 +1,200 @@ +/* + * 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 React, { useState, useEffect } from 'react'; +import { CardHeader, Divider, IconButton } from '@material-ui/core'; +import { + HeaderIconLinkRow, + IconLinkVerticalProps, +} from '@backstage/core-components'; +import EditIcon from '@material-ui/icons/Edit'; +import ChatIcon from '@material-ui/icons/Chat'; +import PersonAddIcon from '@material-ui/icons/PersonAdd'; +import DashboardIcon from '@material-ui/icons/Dashboard'; +import LinkOffIcon from '@material-ui/icons/LinkOff'; +import { EditProjectDialog } from '../EditProjectDialog'; +import { useApi, identityApiRef } from '@backstage/core-plugin-api'; +import { BazaarProject, Member } from '../../types'; +import { bazaarApiRef } from '../../api'; +import { Alert } from '@material-ui/lab'; +import { useAsyncFn } from 'react-use'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import { parseEntityRef } from '@backstage/catalog-model'; +import { ConfirmationDialog } from '../ConfirmationDialog'; +import { CardContentFields } from '../CardContentFields'; +import { fetchProjectMembers } from '../../util/fetchMethods'; + +type Props = { + bazaarProject: BazaarProject | null | undefined; + fetchBazaarProject: () => Promise; +}; + +export const EntityBazaarInfoContent = ({ + bazaarProject, + fetchBazaarProject, +}: Props) => { + const bazaarApi = useApi(bazaarApiRef); + const identity = useApi(identityApiRef); + const [openEdit, setOpenEdit] = useState(false); + const [isMember, setIsMember] = useState(false); + const [openUnlink, setOpenUnlink] = useState(false); + const [members, fetchMembers] = useAsyncFn(async () => { + return bazaarProject + ? await fetchProjectMembers(bazaarApi, bazaarProject) + : []; + }); + + const [userId, fetchUserId] = useAsyncFn(async () => { + return await ( + await identity.getProfileInfo() + ).displayName; + }); + + useEffect(() => { + fetchMembers(); + fetchUserId(); + }, [fetchMembers, fetchUserId]); + + useEffect(() => { + if (members.value && userId.value) { + setIsMember( + members.value + ?.map((member: Member) => member.userId) + .indexOf(userId.value) >= 0, + ); + } + }, [bazaarProject, members, identity, userId.value]); + + const handleMembersClick = async () => { + if (userId.value) { + if (!isMember) { + await bazaarApi.addMember(bazaarProject?.id!, userId.value); + } else { + await bazaarApi.deleteMember(bazaarProject!.id, userId.value); + } + setIsMember(!isMember); + fetchMembers(); + } + }; + + const links: IconLinkVerticalProps[] = [ + { + label: 'Entity page', + icon: , + disabled: true, + }, + { + label: 'Unlink project', + icon: , + disabled: false, + onClick: () => { + setOpenUnlink(true); + }, + }, + { + label: isMember ? 'Leave' : 'Join', + icon: isMember ? : , + href: '', + onClick: async () => { + handleMembersClick(); + }, + }, + { + label: 'Community', + icon: , + href: bazaarProject?.community, + disabled: bazaarProject?.community === '' || !isMember, + }, + ]; + + const handleEditClose = () => { + setOpenEdit(false); + }; + + const handleUnlinkClose = () => { + setOpenUnlink(false); + }; + + const handleUnlinkSubmit = async () => { + const updateResponse = await bazaarApi.updateProject({ + ...bazaarProject, + entityRef: null, + }); + + if (updateResponse.status === 'ok') { + handleUnlinkClose(); + fetchBazaarProject(); + } + }; + + if (members.error) { + return {members?.error?.message}; + } + + if (bazaarProject) { + return ( +
+ + + {openUnlink && ( + {parseEntityRef(bazaarProject.entityRef!).name}, + ' from ', + {bazaarProject.name}, + ' ?', + ]} + type="unlink" + handleSubmit={handleUnlinkSubmit} + /> + )} + + + { + setOpenEdit(true); + }} + > + + +
+ } + subheader={} + /> + + + +
+ ); + } + return null; +}; diff --git a/plugins/bazaar/src/components/EntityBazaarInfoContent/index.ts b/plugins/bazaar/src/components/EntityBazaarInfoContent/index.ts new file mode 100644 index 0000000000..6be5ad7dbf --- /dev/null +++ b/plugins/bazaar/src/components/EntityBazaarInfoContent/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 { EntityBazaarInfoContent } from './EntityBazaarInfoContent'; diff --git a/plugins/bazaar/src/components/HomePage/HomePage.tsx b/plugins/bazaar/src/components/HomePage/HomePage.tsx index 32a16291fc..b34b7ac949 100644 --- a/plugins/bazaar/src/components/HomePage/HomePage.tsx +++ b/plugins/bazaar/src/components/HomePage/HomePage.tsx @@ -36,11 +36,7 @@ export const HomePage = () => { return (
-
+
); diff --git a/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx b/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx new file mode 100644 index 0000000000..daca0aec1d --- /dev/null +++ b/plugins/bazaar/src/components/HomePageBazaarInfoCard/HomePageBazaarInfoCard.tsx @@ -0,0 +1,269 @@ +/* + * 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 React, { useState, useEffect } from 'react'; +import { Card, CardHeader, Divider, IconButton } from '@material-ui/core'; +import { + HeaderIconLinkRow, + IconLinkVerticalProps, +} from '@backstage/core-components'; +import EditIcon from '@material-ui/icons/Edit'; +import ChatIcon from '@material-ui/icons/Chat'; +import PersonAddIcon from '@material-ui/icons/PersonAdd'; +import InsertLinkIcon from '@material-ui/icons/InsertLink'; +import DashboardIcon from '@material-ui/icons/Dashboard'; +import CloseIcon from '@material-ui/icons/Close'; +import LinkOffIcon from '@material-ui/icons/LinkOff'; +import { EditProjectDialog } from '../EditProjectDialog'; +import ExitToAppIcon from '@material-ui/icons/ExitToApp'; +import { + useApi, + identityApiRef, + useRouteRef, +} from '@backstage/core-plugin-api'; +import { Member, BazaarProject } from '../../types'; +import { bazaarApiRef } from '../../api'; +import { Alert } from '@material-ui/lab'; +import { useAsyncFn } from 'react-use'; +import { + catalogApiRef, + catalogRouteRef, +} from '@backstage/plugin-catalog-react'; + +import { + parseEntityName, + stringifyEntityRef, + Entity, + parseEntityRef, +} from '@backstage/catalog-model'; + +import { ConfirmationDialog } from '../ConfirmationDialog/ConfirmationDialog'; +import { CardContentFields } from '../CardContentFields/CardContentFields'; +import { LinkProjectDialog } from '../LinkProjectDialog'; +import { + fetchCatalogItems, + fetchProjectMembers, +} from '../../util/fetchMethods'; +import { parseBazaarResponse } from '../../util/parseMethods'; + +type Props = { + initProject: BazaarProject; + handleClose: () => void; + initEntity: Entity; +}; + +export const HomePageBazaarInfoCard = ({ + initProject, + handleClose, + initEntity, +}: Props) => { + const catalogLink = useRouteRef(catalogRouteRef); + const bazaarApi = useApi(bazaarApiRef); + const identity = useApi(identityApiRef); + const catalogApi = useApi(catalogApiRef); + const [openEdit, setOpenEdit] = useState(false); + const [openProjectSelector, setOpenProjectSelector] = useState(false); + const [openUnlink, setOpenUnlink] = useState(false); + const [isMember, setIsMember] = useState(false); + + const [catalogEntities, fetchCatalogEntities] = useAsyncFn(async () => { + const entities = await fetchCatalogItems(catalogApi); + const bazaarProjects = await bazaarApi.getProjects(); + const bazaarLinkedRefs: string[] = bazaarProjects.data + .filter((entity: any) => entity.entity_ref !== null) + .map((entity: any) => entity.entity_ref); + + return entities.filter( + (entity: Entity) => + !bazaarLinkedRefs.includes(stringifyEntityRef(entity)), + ); + }); + + const [bazaarProject, fetchBazaarProject] = useAsyncFn(async () => { + const response = await bazaarApi.getProjectById(initProject.id); + return await parseBazaarResponse(response); + }); + + const [members, fetchMembers] = useAsyncFn(async () => { + return fetchProjectMembers(bazaarApi, bazaarProject.value ?? initProject); + }); + + const [userId, fetchUserId] = useAsyncFn(async () => { + return await ( + await identity.getProfileInfo() + ).displayName; + }); + + useEffect(() => { + fetchMembers(); + fetchBazaarProject(); + fetchCatalogEntities(); + fetchUserId(); + }, [fetchMembers, fetchBazaarProject, fetchCatalogEntities, fetchUserId]); + + useEffect(() => { + if (members.value && userId.value) { + setIsMember( + members.value + ?.map((member: Member) => member.userId) + .indexOf(userId.value) >= 0, + ); + } + }, [bazaarProject.value, members, identity, userId.value]); + + const handleMembersClick = async () => { + if (userId.value) { + if (!isMember) { + await bazaarApi.addMember(bazaarProject.value!.id, userId.value); + } else { + await bazaarApi.deleteMember(bazaarProject.value!.id, userId.value); + } + setIsMember(!isMember); + fetchMembers(); + } + }; + + const getEntityPageLink = () => { + if (bazaarProject?.value?.entityRef) { + const { name, kind, namespace } = parseEntityName( + bazaarProject.value.entityRef, + ); + return `${catalogLink()}/${namespace}/${kind}/${name}`; + } + return ''; + }; + + const handleLink = () => { + if (bazaarProject.value?.entityRef) { + setOpenUnlink(true); + } else { + fetchCatalogEntities(); + setOpenProjectSelector(true); + } + }; + + const links: IconLinkVerticalProps[] = [ + { + label: 'Entity page', + icon: , + href: bazaarProject.value?.entityRef ? getEntityPageLink() : '', + disabled: bazaarProject.value?.entityRef === null, + }, + { + label: bazaarProject.value?.entityRef ? 'Unlink project' : 'Link project', + icon: bazaarProject.value?.entityRef ? ( + + ) : ( + + ), + onClick: handleLink, + }, + { + label: isMember ? 'Leave' : 'Join', + icon: isMember ? : , + href: '', + onClick: async () => { + handleMembersClick(); + }, + }, + { + label: 'Community', + icon: , + href: bazaarProject.value?.community, + disabled: !bazaarProject.value?.community || !isMember, + }, + ]; + + const handleUnlinkSubmit = async () => { + const updateResponse = await bazaarApi.updateProject({ + ...bazaarProject.value, + entityRef: null, + }); + + if (updateResponse.status === 'ok') { + setOpenUnlink(false); + fetchBazaarProject(); + } + }; + + if (bazaarProject.error) { + return {bazaarProject?.error?.message}; + } else if (members.error) { + return {members?.error?.message}; + } + + return ( +
+ setOpenProjectSelector(false)} + catalogEntities={catalogEntities.value || []} + bazaarProject={bazaarProject.value || initProject} + fetchBazaarProject={fetchBazaarProject} + initEntity={initEntity} + /> + + {openUnlink && ( + setOpenUnlink(false)} + message={[ + 'Are you sure you want to unlink ', + {parseEntityRef(bazaarProject.value?.entityRef!).name}, + ' from ', + {bazaarProject.value?.name}, + ' ?', + ]} + type="unlink" + handleSubmit={handleUnlinkSubmit} + /> + )} + + + setOpenEdit(false)} + handleCardClose={handleClose} + fetchBazaarProject={fetchBazaarProject} + /> + + + setOpenEdit(true)}> + + + + + +
+ } + subheader={} + /> + + + +
+
+ ); +}; diff --git a/plugins/bazaar/src/components/HomePageBazaarInfoCard/index.ts b/plugins/bazaar/src/components/HomePageBazaarInfoCard/index.ts new file mode 100644 index 0000000000..0edfba0652 --- /dev/null +++ b/plugins/bazaar/src/components/HomePageBazaarInfoCard/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 { HomePageBazaarInfoCard } from './HomePageBazaarInfoCard'; diff --git a/plugins/bazaar/src/components/InputField/InputField.tsx b/plugins/bazaar/src/components/InputField/InputField.tsx index 9932e5b1fc..76eb28da6c 100644 --- a/plugins/bazaar/src/components/InputField/InputField.tsx +++ b/plugins/bazaar/src/components/InputField/InputField.tsx @@ -25,7 +25,7 @@ type Rules = { }; type Props = { - inputType: 'announcement' | 'community' | 'responsible'; + inputType: 'description' | 'community' | 'responsible' | 'name'; error?: FieldError | undefined; control: Control; helperText?: string; diff --git a/plugins/bazaar/src/components/InputSelector/InputSelector.tsx b/plugins/bazaar/src/components/InputSelector/InputSelector.tsx index 25ed459963..b7e9ecd96a 100644 --- a/plugins/bazaar/src/components/InputSelector/InputSelector.tsx +++ b/plugins/bazaar/src/components/InputSelector/InputSelector.tsx @@ -60,12 +60,7 @@ export const InputSelector = ({ name, options, control, error }: Props) => { > {options.map(option => { return ( - + {option} ); diff --git a/plugins/bazaar/src/components/LinkProjectDialog/LinkProjectDialog.tsx b/plugins/bazaar/src/components/LinkProjectDialog/LinkProjectDialog.tsx new file mode 100644 index 0000000000..92a7344dac --- /dev/null +++ b/plugins/bazaar/src/components/LinkProjectDialog/LinkProjectDialog.tsx @@ -0,0 +1,97 @@ +/* + * 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 React, { useState } from 'react'; +import { + Dialog, + DialogActions, + Button, + DialogContent, + makeStyles, +} from '@material-ui/core'; +import { ProjectSelector } from '../ProjectSelector'; +import { CustomDialogTitle } from '../CustomDialogTitle'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; + +import { bazaarApiRef } from '../../api'; +import { useApi } from '@backstage/core-plugin-api'; + +import { BazaarProject } from '../../types'; + +type Props = { + openProjectSelector: boolean; + handleProjectSelectorClose: () => void; + catalogEntities: Entity[]; + bazaarProject: BazaarProject; + fetchBazaarProject: () => Promise; + initEntity: Entity; +}; + +const useStyles = makeStyles({ + content: { padding: '0 1rem' }, +}); + +export const LinkProjectDialog = ({ + openProjectSelector, + handleProjectSelectorClose, + catalogEntities, + bazaarProject, + fetchBazaarProject, + initEntity, +}: Props) => { + const classes = useStyles(); + const bazaarApi = useApi(bazaarApiRef); + const [selectedEntity, setSelectedEntity] = useState(initEntity); + const handleEntityClick = (entity: Entity) => { + setSelectedEntity(entity); + }; + + const handleSubmit = async () => { + handleProjectSelectorClose(); + + const updateResponse = await bazaarApi.updateProject({ + ...bazaarProject, + entityRef: stringifyEntityRef(selectedEntity!), + }); + if (updateResponse.status === 'ok') fetchBazaarProject(); + }; + + return ( + + + Select entity + + + + + + + + + + ); +}; diff --git a/plugins/bazaar/src/components/LinkProjectDialog/index.ts b/plugins/bazaar/src/components/LinkProjectDialog/index.ts new file mode 100644 index 0000000000..68d8e7c89e --- /dev/null +++ b/plugins/bazaar/src/components/LinkProjectDialog/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 { LinkProjectDialog } from './LinkProjectDialog'; diff --git a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx index b3c8b3e2e7..a558d347a5 100644 --- a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx +++ b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx @@ -14,85 +14,94 @@ * limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { ItemCardHeader } from '@backstage/core-components'; import { Card, CardActionArea, CardContent, + Dialog, makeStyles, Typography, } from '@material-ui/core'; import { StatusTag } from '../StatusTag/StatusTag'; -import { Link as RouterLink } from 'react-router-dom'; -import { catalogRouteRef } from '@backstage/plugin-catalog-react'; -import { useRouteRef } from '@backstage/core-plugin-api'; import { BazaarProject } from '../../types'; -import { parseEntityName } from '@backstage/catalog-model'; import { DateTime } from 'luxon'; +import { HomePageBazaarInfoCard } from '../HomePageBazaarInfoCard'; +import { Entity } from '@backstage/catalog-model'; const useStyles = makeStyles({ statusTag: { display: 'inline-block', whiteSpace: 'nowrap', - marginBottom: '0.5rem', - }, - announcement: { - display: '-webkit-box', - WebkitLineClamp: 5, - WebkitBoxOrient: 'vertical', marginBottom: '0.8rem', + }, + description: { + display: '-webkit-box', + WebkitLineClamp: 7, + WebkitBoxOrient: 'vertical', overflow: 'hidden', + backgroundColor: '', }, memberCount: { float: 'right', }, + content: { height: '13rem', marginBottom: '-0.5rem' }, }); type Props = { - bazaarProject: BazaarProject; + project: BazaarProject; + fetchBazaarProjects: () => Promise; + catalogEntities: Entity[]; }; -export const ProjectCard = ({ bazaarProject }: Props) => { +export const ProjectCard = ({ + project, + fetchBazaarProjects, + catalogEntities, +}: Props) => { const classes = useStyles(); - const { entityRef, name, status, updatedAt, announcement, membersCount } = - bazaarProject; - const catalogLink = useRouteRef(catalogRouteRef); - const { namespace, kind } = parseEntityName(entityRef); + const [openCard, setOpenCard] = useState(false); + const { id, name, status, updatedAt, description, membersCount } = project; + + const handleClose = () => { + setOpenCard(false); + fetchBazaarProjects(); + }; return ( - - - + + - - - - {membersCount === 1 - ? `${membersCount} member` - : `${membersCount} members`} - -
- - {announcement} +
+ + + setOpenCard(true)}> + + + + + {Number(membersCount) === Number(1) + ? `${membersCount} member` + : `${membersCount} members`} -
- - - + + {description} + + + + +
); }; diff --git a/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx b/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx index a65122b26e..08c43d1c2e 100644 --- a/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx +++ b/plugins/bazaar/src/components/ProjectDialog/ProjectDialog.tsx @@ -15,17 +15,7 @@ */ import React from 'react'; -import { - createStyles, - Theme, - withStyles, - WithStyles, -} from '@material-ui/core/styles'; -import MuiDialogTitle from '@material-ui/core/DialogTitle'; -import MuiDialogContent from '@material-ui/core/DialogContent'; -import MuiDialogActions from '@material-ui/core/DialogActions'; -import CloseIcon from '@material-ui/icons/Close'; -import { Button, Dialog, Typography, IconButton } from '@material-ui/core'; +import { Button, Dialog } from '@material-ui/core'; import { useForm, SubmitHandler, @@ -36,61 +26,11 @@ import { InputField } from '../InputField/InputField'; import { InputSelector } from '../InputSelector/InputSelector'; import { FormValues } from '../../types'; import { DoubleDateSelector } from '../DoubleDateSelector/DoubleDateSelector'; - -const styles = (theme: Theme) => - createStyles({ - root: { - margin: 0, - padding: theme.spacing(2), - }, - closeButton: { - position: 'absolute', - right: theme.spacing(1), - top: theme.spacing(1), - color: theme.palette.grey[500], - }, - }); - -/* - DialogTitleProps, DialogTitle, DialogContent and DialogActions - are copied from the git-release plugin -*/ -export interface DialogTitleProps extends WithStyles { - id: string; - children: React.ReactNode; - onClose: () => void; -} - -const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { - const { children, classes, onClose, ...other } = props; - return ( - - {children} - {onClose ? ( - - - - ) : null} - - ); -}); - -const DialogContent = withStyles((theme: Theme) => ({ - root: { - padding: theme.spacing(2), - }, -}))(MuiDialogContent); - -const DialogActions = withStyles((theme: Theme) => ({ - root: { - margin: 0, - padding: theme.spacing(1), - }, -}))(MuiDialogActions); +import { + CustomDialogTitle, + DialogActions, + DialogContent, +} from '../CustomDialogTitle'; type Props = { handleSave: ( @@ -102,6 +42,7 @@ type Props = { defaultValues: FormValues; open: boolean; projectSelector?: JSX.Element; + deleteButton?: JSX.Element; handleClose: () => void; }; @@ -112,6 +53,7 @@ export const ProjectDialog = ({ defaultValues, open, projectSelector, + deleteButton, handleClose, }: Props) => { const { @@ -123,16 +65,16 @@ export const ProjectDialog = ({ setValue, } = useForm({ mode: 'onChange', - defaultValues: defaultValues, + defaultValues, }); - const handleCloseAndClear = () => { - handleClose(); - reset(defaultValues); + const handleSaveForm = () => { + handleSave(getValues, reset); }; - const handleSaveProject = () => { - handleSave(getValues, reset); + const handleCloseDialog = () => { + handleClose(); + reset(defaultValues); }; return ( @@ -140,25 +82,36 @@ export const ProjectDialog = ({ - + {title} - - - {isAddForm && projectSelector} + + + - + {isAddForm && projectSelector} + + + {!isAddForm && deleteButton}
- } - handleClose={handleCloseNoProjects} - /> - +
+ + + { + setSearchValue(newSortMethod); + }} + onCancelSearch={() => { + setSearchValue(''); + }} + /> { setOpenAdd(false); }} @@ -176,10 +191,11 @@ export const SortView = () => { fetchCatalogEntities={fetchCatalogEntities} /> - +
diff --git a/plugins/bazaar/src/types.ts b/plugins/bazaar/src/types.ts index 7d810c4eab..30c07286ac 100644 --- a/plugins/bazaar/src/types.ts +++ b/plugins/bazaar/src/types.ts @@ -17,7 +17,7 @@ import { EntityRef } from '@backstage/catalog-model'; export type Member = { - entityRef: EntityRef; + itemId: number; userId: string; joinDate?: string; picture?: string; @@ -29,10 +29,11 @@ export type Size = 'small' | 'medium' | 'large'; export type BazaarProject = { name: string; - entityRef: EntityRef; + id: number; + entityRef?: EntityRef; community: string; status: Status; - announcement: string; + description: string; updatedAt?: string; membersCount: number; size: Size; @@ -42,7 +43,8 @@ export type BazaarProject = { }; export type FormValues = { - announcement: string; + name: string; + description: string; community: string; status: string; size: Size; diff --git a/plugins/bazaar/src/util/fetchMethods.ts b/plugins/bazaar/src/util/fetchMethods.ts new file mode 100644 index 0000000000..015abae210 --- /dev/null +++ b/plugins/bazaar/src/util/fetchMethods.ts @@ -0,0 +1,49 @@ +/* + * 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 { sortMembers } from './sortMethods'; +import { parseMember } from './parseMethods'; +import { BazaarProject, Member } from '../types'; +import { BazaarApi } from '../api'; +import { Entity } from '@backstage/catalog-model'; + +export const fetchProjectMembers = async ( + bazaarApi: BazaarApi, + project: BazaarProject, +): Promise => { + const response = await bazaarApi.getMembers(project.id); + + if (response.data.length > 0) { + const dbMembers = response.data.map((member: any) => { + return parseMember(member); + }); + + dbMembers.sort(sortMembers); + return dbMembers; + } + return []; +}; + +export const fetchCatalogItems = async (catalogApi: any): Promise => { + const entities = await catalogApi.getEntities({ + filter: { + kind: ['Component', 'Resource'], + }, + fields: ['kind', 'metadata.name', 'metadata.namespace'], + }); + + return entities.items; +}; diff --git a/plugins/bazaar/src/util/parseMethods.ts b/plugins/bazaar/src/util/parseMethods.ts new file mode 100644 index 0000000000..1b4ebfd308 --- /dev/null +++ b/plugins/bazaar/src/util/parseMethods.ts @@ -0,0 +1,54 @@ +/* + * 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 { BazaarProject, Member } from '../types'; + +export const parseBazaarProject = (metadata: any): BazaarProject => { + return { + id: metadata.id, + entityRef: metadata.entity_ref, + name: metadata.name, + community: metadata.community, + description: metadata.description, + status: metadata.status, + updatedAt: metadata.updated_at, + membersCount: metadata.members_count, + size: metadata.size, + startDate: metadata.start_date, + endDate: metadata.end_date, + responsible: metadata.responsible, + } as BazaarProject; +}; + +export const parseMember = (member: any): Member => { + return { + itemId: member.item_id, + userId: member.user_id, + joinDate: member.join_date, + picture: member.picture, + } as Member; +}; + +export const parseBazaarResponse = async (response: any) => { + if (response) { + const metadata = await response.json().then((resp: any) => resp.data[0]); + + if (metadata) { + return parseBazaarProject(metadata); + } + } + return null; +}; diff --git a/plugins/bazaar/src/util/sortMethods.ts b/plugins/bazaar/src/util/sortMethods.ts new file mode 100644 index 0000000000..d75639edca --- /dev/null +++ b/plugins/bazaar/src/util/sortMethods.ts @@ -0,0 +1,40 @@ +/* + * 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 { BazaarProject, Member } from '../types'; + +export const sortMembers = (m1: Member, m2: Member) => { + return new Date(m2.joinDate!).getTime() - new Date(m1.joinDate!).getTime(); +}; + +export const sortByDate = (a: BazaarProject, b: BazaarProject): number => { + const dateA = new Date(a.updatedAt!).getTime(); + const dateB = new Date(b.updatedAt!).getTime(); + return dateB - dateA; +}; + +export const sortByName = (a: BazaarProject, b: BazaarProject) => { + if (a.name < b.name) { + return -1; + } else if (a.name > b.name) { + return 1; + } + return 0; +}; + +export const sortByMembers = (a: BazaarProject, b: BazaarProject) => { + return b.membersCount - a.membersCount; +}; 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/bitrise/src/plugin.ts b/plugins/bitrise/src/plugin.ts index a6f0706999..92beda287c 100644 --- a/plugins/bitrise/src/plugin.ts +++ b/plugins/bitrise/src/plugin.ts @@ -26,8 +26,6 @@ import { export const bitriseApiRef = createApiRef({ id: 'plugin.bitrise.service', - description: - 'Used by the BitriseCI plugin to retrieve information about builds.', }); export const bitrisePlugin = createPlugin({ 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-import/src/api/CatalogImportApi.ts b/plugins/catalog-import/src/api/CatalogImportApi.ts index 89c27f1875..5fae076479 100644 --- a/plugins/catalog-import/src/api/CatalogImportApi.ts +++ b/plugins/catalog-import/src/api/CatalogImportApi.ts @@ -20,7 +20,6 @@ import { PartialEntity } from '../types'; export const catalogImportApiRef = createApiRef({ id: 'plugin.catalog-import.service', - description: 'Used by the catalog import plugin to make requests', }); // result of the analyze state 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/apis/StarredEntitiesApi/migration.ts b/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts index 7147d449ab..8405d9e08d 100644 --- a/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts +++ b/plugins/catalog-react/src/apis/StarredEntitiesApi/migration.ts @@ -41,8 +41,7 @@ export async function performMigrationToTheNewBucket({ // nothing to do return; } - - const targetEntities = new Set(target.get('entityRefs') ?? []); + const targetEntities = new Set(target.get('entityRefs') ?? []); oldStarredEntities .filter(isString) 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/components/Router.tsx b/plugins/catalog/src/components/Router.tsx index 2a0eacf074..e8b00d8513 100644 --- a/plugins/catalog/src/components/Router.tsx +++ b/plugins/catalog/src/components/Router.tsx @@ -19,13 +19,13 @@ import { useEntity, useEntityFromUrl, } from '@backstage/plugin-catalog-react'; -import { Link, Typography } from '@material-ui/core'; +import { Typography } from '@material-ui/core'; import React, { ComponentType, ReactNode } from 'react'; import { Navigate, Route, Routes, useParams } from 'react-router'; import { CatalogPage } from './CatalogPage'; import { EntityNotFound } from './EntityNotFound'; import { EntityPageLayout } from './EntityPageLayout'; -import { Content } from '@backstage/core-components'; +import { Content, Link } from '@backstage/core-components'; const DefaultEntityPage = () => ( @@ -38,9 +38,7 @@ const DefaultEntityPage = () => ( To override this component with your custom implementation, read docs on{' '} - - backstage.io/docs - + backstage.io/docs } 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/circleci/src/api/CircleCIApi.ts b/plugins/circleci/src/api/CircleCIApi.ts index 498cbb588a..78b8b378d6 100644 --- a/plugins/circleci/src/api/CircleCIApi.ts +++ b/plugins/circleci/src/api/CircleCIApi.ts @@ -33,7 +33,6 @@ export type { BuildWithSteps, BuildStepAction, BuildSummary }; export const circleCIApiRef = createApiRef({ id: 'plugin.circleci.service', - description: 'Used by the CircleCI plugin to make requests', }); const DEFAULT_PROXY_PATH = '/circleci/api'; diff --git a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx index 53cbc754cf..adc8a569bb 100644 --- a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { Avatar, - Link, Typography, Box, IconButton, @@ -37,6 +36,7 @@ import { StatusRunning, Table, TableColumn, + Link, } from '@backstage/core-components'; import { useRouteRef } from '@backstage/core-plugin-api'; @@ -121,9 +121,7 @@ const SourceInfo = ({ build }: { build: CITableBuildInfo }) => { {source?.branchName} {source?.commit?.url !== undefined ? ( - - {source?.commit.shortHash} - + {source?.commit.shortHash} ) : ( source?.commit.shortHash )} @@ -169,7 +167,7 @@ const generatedColumns: TableColumn[] = [ field: 'buildName', highlight: true, render: (row: Partial) => ( - + 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/cloudbuild/src/api/CloudbuildApi.ts b/plugins/cloudbuild/src/api/CloudbuildApi.ts index 2b09d24e64..92ea8511fe 100644 --- a/plugins/cloudbuild/src/api/CloudbuildApi.ts +++ b/plugins/cloudbuild/src/api/CloudbuildApi.ts @@ -22,7 +22,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const cloudbuildApiRef = createApiRef({ id: 'plugin.cloudbuild.service', - description: 'Used by the Cloudbuild plugin to make requests', }); export type CloudbuildApi = { diff --git a/plugins/cloudbuild/src/components/Cards/Cards.tsx b/plugins/cloudbuild/src/components/Cards/Cards.tsx index a1c9b3c076..dad1f7d38f 100644 --- a/plugins/cloudbuild/src/components/Cards/Cards.tsx +++ b/plugins/cloudbuild/src/components/Cards/Cards.tsx @@ -19,12 +19,13 @@ import { WorkflowRunsTable } from '../WorkflowRunsTable'; import { Entity } from '@backstage/catalog-model'; import { useEntity } from '@backstage/plugin-catalog-react'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; -import { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core'; +import { Theme, makeStyles, LinearProgress } from '@material-ui/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; import { CLOUDBUILD_ANNOTATION } from '../useProjectName'; import { InfoCard, + Link, StructuredMetadataTable, WarningPanel, } from '@backstage/core-components'; @@ -62,7 +63,7 @@ const WidgetContent = ({ ), message: lastRun.message, url: ( - + See more on Google{' '} 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/code-coverage/src/api.ts b/plugins/code-coverage/src/api.ts index a4a7bba37c..61f920106b 100644 --- a/plugins/code-coverage/src/api.ts +++ b/plugins/code-coverage/src/api.ts @@ -34,7 +34,6 @@ export type CodeCoverageApi = { export const codeCoverageApiRef = createApiRef({ id: 'plugin.code-coverage.service', - description: 'Used by the code coverage plugin to make requests', }); export class CodeCoverageRestApi implements CodeCoverageApi { 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/config-schema/src/components/SchemaBrowser/SchemaBrowser.tsx b/plugins/config-schema/src/components/SchemaBrowser/SchemaBrowser.tsx index 9ed3e8fe61..73e4f15c0a 100644 --- a/plugins/config-schema/src/components/SchemaBrowser/SchemaBrowser.tsx +++ b/plugins/config-schema/src/components/SchemaBrowser/SchemaBrowser.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import { createStyles, fade, withStyles } from '@material-ui/core'; +import { createStyles, alpha, withStyles } from '@material-ui/core'; import ChevronRightIcon from '@material-ui/icons/ChevronRight'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { TreeItem, TreeItemProps, TreeView } from '@material-ui/lab'; @@ -30,7 +30,7 @@ const StyledTreeItem = withStyles(theme => group: { marginLeft: 7, paddingLeft: theme.spacing(1), - borderLeft: `1px solid ${fade(theme.palette.text.primary, 0.15)}`, + borderLeft: `1px solid ${alpha(theme.palette.text.primary, 0.15)}`, }, }), )((props: TreeItemProps) => ); 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/cost-insights/src/api/CostInsightsApi.ts b/plugins/cost-insights/src/api/CostInsightsApi.ts index 40882ebd95..58c8592273 100644 --- a/plugins/cost-insights/src/api/CostInsightsApi.ts +++ b/plugins/cost-insights/src/api/CostInsightsApi.ts @@ -148,5 +148,4 @@ export type CostInsightsApi = { export const costInsightsApiRef = createApiRef({ id: 'plugin.costinsights.service', - description: 'Provides cost data and alerts for the cost-insights plugin', }); diff --git a/plugins/explore-react/src/tools/api.ts b/plugins/explore-react/src/tools/api.ts index 33b4b7b66c..8a91b84785 100644 --- a/plugins/explore-react/src/tools/api.ts +++ b/plugins/explore-react/src/tools/api.ts @@ -18,7 +18,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const exploreToolsConfigRef = createApiRef({ id: 'plugin.explore.toolsconfig', - description: 'Used to configure tools displayed in the explore plugin', }); export type ExploreTool = { 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/firehydrant/src/api/index.ts b/plugins/firehydrant/src/api/index.ts index b78e81a024..7101b5dc62 100644 --- a/plugins/firehydrant/src/api/index.ts +++ b/plugins/firehydrant/src/api/index.ts @@ -39,7 +39,6 @@ export interface FireHydrantAPI { export const fireHydrantApiRef = createApiRef({ id: 'plugin.firehydrant.service', - description: 'Used by FireHydrant plugin for requests', }); export type Options = { 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/fossa/src/api/FossaApi.ts b/plugins/fossa/src/api/FossaApi.ts index 918eab62a4..9a3932fea0 100644 --- a/plugins/fossa/src/api/FossaApi.ts +++ b/plugins/fossa/src/api/FossaApi.ts @@ -26,7 +26,6 @@ export interface FindingSummary { export const fossaApiRef = createApiRef({ id: 'plugin.fossa.service', - description: 'Used by the Fossa plugin to make requests', }); export type FossaApi = { 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/gcp-projects/src/api/GcpApi.ts b/plugins/gcp-projects/src/api/GcpApi.ts index 5626796a05..9167d2983f 100644 --- a/plugins/gcp-projects/src/api/GcpApi.ts +++ b/plugins/gcp-projects/src/api/GcpApi.ts @@ -19,7 +19,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const gcpApiRef = createApiRef({ id: 'plugin.gcpprojects.service', - description: 'Used by the GCP Projects plugin to make requests', }); export type GcpApi = { 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/git-release-manager/src/api/serviceApiRef.test.ts b/plugins/git-release-manager/src/api/serviceApiRef.test.ts index 03a6bbae02..b27738671d 100644 --- a/plugins/git-release-manager/src/api/serviceApiRef.test.ts +++ b/plugins/git-release-manager/src/api/serviceApiRef.test.ts @@ -23,7 +23,6 @@ describe('gitReleaseManagerApiRef', () => { expect(result).toMatchInlineSnapshot(` ApiRefImpl { "config": Object { - "description": "Used by the Git Release Manager plugin to make requests", "id": "plugin.git-release-manager.service", }, } diff --git a/plugins/git-release-manager/src/api/serviceApiRef.ts b/plugins/git-release-manager/src/api/serviceApiRef.ts index 75f0203e47..f2c695f455 100644 --- a/plugins/git-release-manager/src/api/serviceApiRef.ts +++ b/plugins/git-release-manager/src/api/serviceApiRef.ts @@ -20,5 +20,4 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const gitReleaseManagerApiRef = createApiRef({ id: 'plugin.git-release-manager.service', - description: 'Used by the Git Release Manager plugin to make requests', }); 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-actions/src/api/GithubActionsApi.ts b/plugins/github-actions/src/api/GithubActionsApi.ts index e30145c4b3..fe516a0211 100644 --- a/plugins/github-actions/src/api/GithubActionsApi.ts +++ b/plugins/github-actions/src/api/GithubActionsApi.ts @@ -19,7 +19,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const githubActionsApiRef = createApiRef({ id: 'plugin.githubactions.service', - description: 'Used by the GitHub Actions plugin to make requests', }); export type GithubActionsApi = { diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx index 6f6531044e..cb4b2190fc 100644 --- a/plugins/github-actions/src/components/Cards/Cards.tsx +++ b/plugins/github-actions/src/components/Cards/Cards.tsx @@ -18,7 +18,6 @@ import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { useEntity } from '@backstage/plugin-catalog-react'; import { LinearProgress, - Link, makeStyles, Theme, Typography, @@ -34,6 +33,7 @@ import { configApiRef, errorApiRef, useApi } from '@backstage/core-plugin-api'; import { InfoCard, InfoCardVariants, + Link, StructuredMetadataTable, } from '@backstage/core-components'; @@ -71,7 +71,7 @@ const WidgetContent = ({ ), message: lastRun.message, url: ( - + See more on GitHub{' '} 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/github-deployments/src/api/index.ts b/plugins/github-deployments/src/api/index.ts index bec0167f63..b64d914612 100644 --- a/plugins/github-deployments/src/api/index.ts +++ b/plugins/github-deployments/src/api/index.ts @@ -91,7 +91,6 @@ export interface GithubDeploymentsApi { export const githubDeploymentsApiRef = createApiRef({ id: 'plugin.github-deployments.service', - description: 'Used by the GitHub Deployments plugin to make requests', }); export type Options = { 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/gitops-profiles/src/api.ts b/plugins/gitops-profiles/src/api.ts index 86cfd805c9..ca141140d8 100644 --- a/plugins/gitops-profiles/src/api.ts +++ b/plugins/gitops-profiles/src/api.ts @@ -113,7 +113,6 @@ export type GitOpsApi = { export const gitOpsApiRef = createApiRef({ id: 'plugin.gitops.service', - description: 'Used by the GitOps profiles plugin to make requests', }); export class GitOpsRestApi implements GitOpsApi { diff --git a/plugins/gitops-profiles/src/components/ClusterTable/ClusterTable.tsx b/plugins/gitops-profiles/src/components/ClusterTable/ClusterTable.tsx index 28c4bb65f3..9ac93a6d6c 100644 --- a/plugins/gitops-profiles/src/components/ClusterTable/ClusterTable.tsx +++ b/plugins/gitops-profiles/src/components/ClusterTable/ClusterTable.tsx @@ -14,10 +14,9 @@ * limitations under the License. */ import React from 'react'; -import { Link } from '@material-ui/core'; import { ClusterStatus } from '../../api'; import { transformStatus } from '../ProfileCatalog/ProfileCatalog'; -import { Table, TableColumn } from '@backstage/core-components'; +import { Link, Table, TableColumn } from '@backstage/core-components'; const columns: TableColumn[] = [ { @@ -25,7 +24,7 @@ const columns: TableColumn[] = [ field: 'name', highlight: true, render: (componentData: any) => ( - + {componentData.name} ), 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/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx index 04ec7e1673..9cb18bd430 100644 --- a/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx +++ b/plugins/home/src/homePageComponents/WelcomeTitle/WelcomeTitle.tsx @@ -13,20 +13,39 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { + alertApiRef, + identityApiRef, + useApi, +} from '@backstage/core-plugin-api'; import { Tooltip } from '@material-ui/core'; -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo } from 'react'; +import { useAsync } from 'react-use'; import { getTimeBasedGreeting } from './timeUtil'; export const WelcomeTitle = () => { const identityApi = useApi(identityApiRef); - const profile = identityApi.getProfile(); - const userId = identityApi.getUserId(); + const alertApi = useApi(alertApiRef); const greeting = useMemo(() => getTimeBasedGreeting(), []); + const { value: profile, error } = useAsync(() => + identityApi.getProfileInfo(), + ); + + useEffect(() => { + if (error) { + alertApi.post({ + message: `Failed to load user identity: ${error}`, + severity: 'error', + }); + } + }, [error, alertApi]); + return ( - {`${greeting.greeting}, ${profile.displayName || userId}!`} + {`${greeting.greeting}${ + profile?.displayName ? `, ${profile?.displayName}` : '' + }!`} ); }; 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/ilert/src/api/client.ts b/plugins/ilert/src/api/client.ts index f05e58878f..13de8c1f2c 100644 --- a/plugins/ilert/src/api/client.ts +++ b/plugins/ilert/src/api/client.ts @@ -41,7 +41,6 @@ import { export const ilertApiRef = createApiRef({ id: 'plugin.ilert.service', - description: 'Used to make requests towards iLert API', }); const DEFAULT_PROXY_PATH = '/ilert'; 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..492e7f5198 100644 --- a/plugins/jenkins/src/api/JenkinsApi.ts +++ b/plugins/jenkins/src/api/JenkinsApi.ts @@ -19,11 +19,11 @@ 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', - description: 'Used by the Jenkins plugin to make requests', }); export interface Build { @@ -140,7 +140,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 +151,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 +179,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 +207,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..2c464491c2 100644 --- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -13,17 +13,16 @@ * 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 { Link, Progress, Table, TableColumn } from '@backstage/core-components'; +import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api'; +import { Box, IconButton, 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 { default as React, useState } from 'react'; import { Project } from '../../../../api/JenkinsApi'; -import { useRouteRef } from '@backstage/core-plugin-api'; +import JenkinsLogo from '../../../../assets/JenkinsLogo.svg'; +import { buildRouteRef } from '../../../../plugin'; +import { useBuilds } from '../../../useBuilds'; +import { JenkinsRunStatus } from '../Status'; const FailCount = ({ count }: { count: number }): JSX.Element | null => { if (count !== 0) { @@ -107,7 +106,6 @@ const generatedColumns: TableColumn[] = [ return ( ) => ( <>

- + {row.lastBuild?.source?.branchName}

@@ -154,7 +152,7 @@ const generatedColumns: TableColumn[] = [ <>

{row.lastBuild?.tests && ( - + {row.lastBuild?.tests.passed} / {row.lastBuild?.tests.total}{' '} passed ) => ( - - - - - - ), + 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/jenkins/src/components/Cards/Cards.test.tsx b/plugins/jenkins/src/components/Cards/Cards.test.tsx index 0af91da179..97b920e666 100644 --- a/plugins/jenkins/src/components/Cards/Cards.test.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.test.tsx @@ -35,7 +35,7 @@ describe('', () => { const jenkinsApi: Partial = { getProjects: () => Promise.resolve([ - { lastBuild: { timestamp: 0, status: 'success' } }, + { lastBuild: { timestamp: 0, status: 'success', url: 'foo' } }, ] as Project[]), }; diff --git a/plugins/jenkins/src/components/Cards/Cards.tsx b/plugins/jenkins/src/components/Cards/Cards.tsx index f0b1ff2bca..cb84964124 100644 --- a/plugins/jenkins/src/components/Cards/Cards.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { LinearProgress, Link, makeStyles, Theme } from '@material-ui/core'; +import { LinearProgress, makeStyles, Theme } from '@material-ui/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; import { DateTime, Duration } from 'luxon'; import React from 'react'; @@ -22,6 +22,7 @@ import { ErrorType, useBuilds } from '../useBuilds'; import { InfoCard, InfoCardVariants, + Link, StructuredMetadataTable, WarningPanel, } from '@backstage/core-components'; @@ -66,7 +67,7 @@ const WidgetContent = ({ 'latest run': displayDate, duration: displayDuration, link: ( - + See more on Jenkins{' '} 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/kafka/src/api/types.ts b/plugins/kafka/src/api/types.ts index 80a0866290..7804574761 100644 --- a/plugins/kafka/src/api/types.ts +++ b/plugins/kafka/src/api/types.ts @@ -18,8 +18,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const kafkaApiRef = createApiRef({ id: 'plugin.kafka.service', - description: - 'Used by the Kafka plugin to make requests to accompanying backend', }); export type ConsumerGroupOffsetsResponse = { 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/api-report.md b/plugins/kubernetes/api-report.md index f7afc7af9d..33ac98fb76 100644 --- a/plugins/kubernetes/api-report.md +++ b/plugins/kubernetes/api-report.md @@ -7,7 +7,9 @@ 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 { IdentityApi } from '@backstage/core-plugin-api'; import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common'; import { OAuthApi } from '@backstage/core-plugin-api'; import { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common'; @@ -89,6 +91,27 @@ export interface KubernetesAuthProvidersApi { // @public (undocumented) export const kubernetesAuthProvidersApiRef: ApiRef; +// Warning: (ae-missing-release-tag) "KubernetesBackendClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export class KubernetesBackendClient implements KubernetesApi { + constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }); + // (undocumented) + getClusters(): Promise< + { + name: string; + authProvider: string; + }[] + >; + // (undocumented) + getObjectsByEntity( + requestBody: KubernetesRequestBody, + ): Promise; +} + // Warning: (ae-missing-release-tag) "kubernetesPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) 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/api/index.ts b/plugins/kubernetes/src/api/index.ts index 33c49db099..c21a258279 100644 --- a/plugins/kubernetes/src/api/index.ts +++ b/plugins/kubernetes/src/api/index.ts @@ -16,3 +16,4 @@ export { kubernetesApiRef } from './types'; export type { KubernetesApi } from './types'; +export { KubernetesBackendClient } from './KubernetesBackendClient'; diff --git a/plugins/kubernetes/src/api/types.ts b/plugins/kubernetes/src/api/types.ts index 5132c8794d..8b95dd820c 100644 --- a/plugins/kubernetes/src/api/types.ts +++ b/plugins/kubernetes/src/api/types.ts @@ -22,8 +22,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const kubernetesApiRef = createApiRef({ id: 'plugin.kubernetes.service', - description: - 'Used by the Kubernetes plugin to make requests to accompanying backend', }); export interface KubernetesApi { 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/kubernetes-auth-provider/types.ts b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts index 3c5564e7cc..ffa0fd5961 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/types.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/types.ts @@ -26,8 +26,6 @@ export interface KubernetesAuthProvider { export const kubernetesAuthProvidersApiRef = createApiRef({ id: 'plugin.kubernetes-auth-providers.service', - description: - 'Used by the Kubernetes plugin to fetch KubernetesAuthProviders', }); export interface KubernetesAuthProvidersApi { 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/lighthouse/src/api.ts b/plugins/lighthouse/src/api.ts index 7dbd48b960..b40b70356f 100644 --- a/plugins/lighthouse/src/api.ts +++ b/plugins/lighthouse/src/api.ts @@ -109,7 +109,6 @@ export type LighthouseApi = { export const lighthouseApiRef = createApiRef({ id: 'plugin.lighthouse.service', - description: 'Used by the Lighthouse plugin to make requests', }); export class LighthouseRestApi implements LighthouseApi { diff --git a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx index 10f29f3fd5..541bc0e22d 100644 --- a/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx +++ b/plugins/lighthouse/src/components/AuditList/AuditListTable.tsx @@ -22,10 +22,15 @@ import { CATEGORY_LABELS, buildSparklinesDataForItem, } from '../../utils'; -import { Link, generatePath } from 'react-router-dom'; +import { generatePath } from 'react-router-dom'; import AuditStatusIcon from '../AuditStatusIcon'; -import { Table, TableColumn, TrendLine } from '@backstage/core-components'; +import { + Link, + Table, + TableColumn, + TrendLine, +} from '@backstage/core-components'; import { useApi } from '@backstage/core-plugin-api'; const columns: TableColumn[] = [ diff --git a/plugins/newrelic-dashboard/.eslintrc.js b/plugins/newrelic-dashboard/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/newrelic-dashboard/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/newrelic-dashboard/README.md b/plugins/newrelic-dashboard/README.md new file mode 100644 index 0000000000..a31042b3fe --- /dev/null +++ b/plugins/newrelic-dashboard/README.md @@ -0,0 +1,79 @@ +# New Relic Dashboard Plugin + +Welcome to the newrelic-dashboard plugin! + +## Features + +- Adds New Relic Dashboard Pages Links to Overview section of the catalog +- Shows Snapshots of dashboards in New Relic + +## Getting started + +This plugin uses the Backstage proxy to securely communicate with New Relic's APIs. We use NerdGraph (New Relic's GraphQL API) + +To generate a New Relic API Key , you can visit this [link](https://one.newrelic.com/launcher/api-keys-ui.api-keys-launcher) + +1. Add the following to your app-config.yaml to enable this configuration: + +``` +// app-config.yaml +proxy: + '/newrelic/api': + target: https://api.newrelic.com + headers: + X-Api-Key: ${NEW_RELIC_USER_KEY} +``` + +2. Add the following to `EntityPage.tsx` to display New Relic Dashboard Tab + +``` +// In packages/app/src/components/catalog/EntityPage.tsx +import { + isNewRelicDashboardAvailable, + EntityNewRelicDashboardContent, + EntityNewRelicDashboardCard, +} from '@backstage/plugin-newrelic-dashboard'; + +const serviceEntityPage = ( + + {/* other tabs... */} + + + +``` + +3. Add the following in `EntityPage.tsx` to display dashboard links in overview page + +``` +const overviewContent = ( + {/* other tabs... */} + + + + + + + +``` + +4. Add `newrelic.com/dashboard-guid` annotation in catalog descriptor file + +To Obtain the dashboard's GUID: Click the info icon by the dashboard's name to access the See metadata and manage tags modal and see the dashboard's GUID. + +``` +// catalog-info.yaml +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + # ... + annotations: + newrelic.com/dashboard-guid: +spec: + type: service +``` + +All set , you will be able to see the plugin in action! diff --git a/plugins/newrelic-dashboard/api-report.md b/plugins/newrelic-dashboard/api-report.md new file mode 100644 index 0000000000..ffdd5a2833 --- /dev/null +++ b/plugins/newrelic-dashboard/api-report.md @@ -0,0 +1,38 @@ +## API Report File for "@backstage/plugin-newrelic-dashboard" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "EntityNewRelicDashboardCard" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityNewRelicDashboardCard: () => JSX.Element; + +// Warning: (ae-missing-release-tag) "EntityNewRelicDashboardContent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityNewRelicDashboardContent: () => JSX.Element; + +// Warning: (ae-missing-release-tag) "isNewRelicDashboardAvailable" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const isNewRelicDashboardAvailable: (entity: Entity) => boolean; + +// Warning: (ae-missing-release-tag) "newRelicDashboardPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const newRelicDashboardPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/newrelic-dashboard/dev/index.tsx b/plugins/newrelic-dashboard/dev/index.tsx new file mode 100644 index 0000000000..d0db8c4d96 --- /dev/null +++ b/plugins/newrelic-dashboard/dev/index.tsx @@ -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. + */ +import { createDevApp } from '@backstage/dev-utils'; +import { newRelicDashboardPlugin } from './../src/plugin'; + +createDevApp().registerPlugin(newRelicDashboardPlugin).render(); diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json new file mode 100644 index 0000000000..3da22d1c9e --- /dev/null +++ b/plugins/newrelic-dashboard/package.json @@ -0,0 +1,46 @@ +{ + "name": "@backstage/plugin-newrelic-dashboard", + "version": "0.1.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/catalog-model": "^0.9.8", + "@backstage/core-components": "^0.8.1", + "@backstage/core-plugin-api": "^0.3.1", + "@backstage/errors": "^0.1.5", + "@backstage/plugin-catalog-react": "^0.6.7", + "@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" + }, + "devDependencies": { + "@backstage/cli": "^0.10.2", + "@backstage/dev-utils": "^0.2.14", + "@testing-library/jest-dom": "^5.10.1", + "@types/react": "^16.13.1 || ^17.0.0", + "cross-fetch": "^3.0.6" + }, + "files": [ + "dist" + ], + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + } +} diff --git a/plugins/newrelic-dashboard/src/Router.tsx b/plugins/newrelic-dashboard/src/Router.tsx new file mode 100644 index 0000000000..24a5a1037f --- /dev/null +++ b/plugins/newrelic-dashboard/src/Router.tsx @@ -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 { Entity } from '@backstage/catalog-model'; +import React from 'react'; +import { MissingAnnotationEmptyState } from '@backstage/core-components'; +import { Button } from '@material-ui/core'; +import { NewRelicDashboard } from './components/NewRelicDashboard'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { NEWRELIC_GUID_ANNOTATION } from './constants'; + +export const isNewRelicDashboardAvailable = (entity: Entity) => + Boolean(entity?.metadata?.annotations?.[NEWRELIC_GUID_ANNOTATION]); + +export const Router = () => { + const { entity } = useEntity(); + + if (isNewRelicDashboardAvailable(entity)) { + return ; + } + + return ( + <> + + + + ); +}; diff --git a/plugins/newrelic-dashboard/src/api/NewRelicDashboardApi.ts b/plugins/newrelic-dashboard/src/api/NewRelicDashboardApi.ts new file mode 100644 index 0000000000..48c7a44c3a --- /dev/null +++ b/plugins/newrelic-dashboard/src/api/NewRelicDashboardApi.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 { createApiRef } from '@backstage/core-plugin-api'; +import { DashboardEntity } from '../types/DashboardEntity'; +import { DashboardSnapshot } from '../types/DashboardSnapshot'; +import { SnapshotDetails } from '../types/SnapshotDetails'; + +export interface DashboardEntitySummary { + getDashboardEntity: DashboardEntity; +} + +export interface DashboardSnapshotSummary { + getDashboardSnapshot: DashboardSnapshot; +} + +export interface SnapshotDetailsSummary { + getSnapshotDetails: SnapshotDetails[]; +} +export const newRelicDashboardApiRef = createApiRef({ + id: 'plugin.newrelicdashboard.service', +}); + +export type NewRelicDashboardApi = { + getDashboardEntity(guid: string): Promise; + getDashboardSnapshot( + guid: string, + duration: number, + ): Promise; +}; diff --git a/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts b/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts new file mode 100644 index 0000000000..b79cceeeda --- /dev/null +++ b/plugins/newrelic-dashboard/src/api/NewRelicDashboardClient.ts @@ -0,0 +1,99 @@ +/* + * 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 { + DashboardEntitySummary, + DashboardSnapshotSummary, + NewRelicDashboardApi, +} from './NewRelicDashboardApi'; +import { DiscoveryApi } from '@backstage/core-plugin-api'; +import { DashboardEntity } from '../types/DashboardEntity'; +import { DashboardSnapshot } from '../types/DashboardSnapshot'; +import { getDashboardParentGuidQuery } from '../queries/getDashboardParentGuidQuery'; +import { getDashboardSnapshotQuery } from '../queries/getDashboardSnapshotQuery'; +import { ResponseError } from '@backstage/errors'; + +export class NewRelicDashboardClient implements NewRelicDashboardApi { + private readonly discoveryApi: DiscoveryApi; + constructor({ + discoveryApi, + }: { + discoveryApi: DiscoveryApi; + baseUrl?: string; + }) { + this.discoveryApi = discoveryApi; + } + + private async callApi( + query: string, + variables: { [key in string]: string | number }, + ): Promise { + const myHeaders = new Headers(); + myHeaders.append('Content-Type', 'application/json'); + const graphql = JSON.stringify({ + query: query, + variables: variables, + }); + const requestOptions: RequestInit = { + method: 'POST', + headers: myHeaders, + body: graphql, + redirect: 'follow', + }; + + const apiUrl = `${await this.discoveryApi.getBaseUrl( + 'proxy', + )}/newrelic/api/graphql`; + const response = await fetch(apiUrl, requestOptions); + if (response.status === 200) { + return (await response.json()) as T; + } + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } else { + return undefined; + } + } + + async getDashboardEntity( + guid: string, + ): Promise { + const DashboardEntityList = await this.callApi( + getDashboardParentGuidQuery, + { + query: `parentId ='${guid}'`, + }, + ); + return { + getDashboardEntity: DashboardEntityList!, + }; + } + + async getDashboardSnapshot( + guid: string, + duration: number, + ): Promise { + const DashboardSnapshotValue = await this.callApi( + getDashboardSnapshotQuery, + { + guid: guid, + duration: duration, + }, + ); + return { + getDashboardSnapshot: DashboardSnapshotValue!, + }; + } +} diff --git a/plugins/newrelic-dashboard/src/api/index.ts b/plugins/newrelic-dashboard/src/api/index.ts new file mode 100644 index 0000000000..9b14160bcf --- /dev/null +++ b/plugins/newrelic-dashboard/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * 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 type { NewRelicDashboardApi } from './NewRelicDashboardApi'; +export { newRelicDashboardApiRef } from './NewRelicDashboardApi'; +export { NewRelicDashboardClient } from './NewRelicDashboardClient'; diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardEntityList.tsx b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardEntityList.tsx new file mode 100644 index 0000000000..005bc42151 --- /dev/null +++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardEntityList.tsx @@ -0,0 +1,89 @@ +/* + * 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 React from 'react'; +import { Box, makeStyles, Typography } from '@material-ui/core'; +import { newRelicDashboardApiRef } from '../../api'; +import { useApi } from '@backstage/core-plugin-api'; +import { useAsync } from 'react-use'; +import { + Progress, + InfoCard, + Link, + ErrorPanel, +} from '@backstage/core-components'; +import DesktopMac from '@material-ui/icons/DesktopMac'; +import { DashboardEntitySummary } from '../../api/NewRelicDashboardApi'; +import { ResultEntity } from '../../types/DashboardEntity'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { NEWRELIC_GUID_ANNOTATION } from './../../constants'; + +const useStyles = makeStyles({ + svgIcon: { + display: 'inline-block', + '& svg': { + display: 'inline-block', + fontSize: 'inherit', + verticalAlign: 'baseline', + }, + }, +}); +export const DashboardEntityList = () => { + const { entity } = useEntity(); + const classes = useStyles(); + const newRelicDashboardAPI = useApi(newRelicDashboardApiRef); + const { value, loading, error } = useAsync(async (): Promise< + DashboardEntitySummary | undefined + > => { + const dashboardObject: Promise = + newRelicDashboardAPI.getDashboardEntity( + String(entity.metadata.annotations?.[NEWRELIC_GUID_ANNOTATION]), + ); + return dashboardObject; + }, [entity.metadata.annotations?.[NEWRELIC_GUID_ANNOTATION]]); + if (loading) { + return ; + } + if (error) { + return ; + } + return ( + + {value?.getDashboardEntity === undefined && + 'Unauthorized Request , please check API Key'} + {value?.getDashboardEntity !== undefined && + value?.getDashboardEntity?.data.actor.entitySearch.results?.entities + ?.length <= 0 && ( + <>No Dashboard Pages found with the specified Dashboard GUID + )} + {value?.getDashboardEntity?.data.actor.entitySearch.results.entities?.map( + (entityResult: ResultEntity) => { + return ( + + + + + + + + {entityResult.name} + + + ); + }, + )} + + ); +}; diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshot.tsx b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshot.tsx new file mode 100644 index 0000000000..95de46df2c --- /dev/null +++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshot.tsx @@ -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 React from 'react'; +import { Box, Grid } from '@material-ui/core'; +import { useApi } from '@backstage/core-plugin-api'; +import { useAsync } from 'react-use'; +import { + InfoCard, + Progress, + ErrorPanel, + Link, +} from '@backstage/core-components'; +import { newRelicDashboardApiRef } from '../../../api'; +import { DashboardSnapshotSummary } from './../../../api/NewRelicDashboardApi'; + +type Props = { + guid: string; + name: string; + permalink: string; + duration: number; +}; + +export const DashboardSnapshot = ({ + guid, + name, + permalink, + duration, +}: Props) => { + const newRelicDashboardAPI = useApi(newRelicDashboardApiRef); + const { value, loading, error } = useAsync(async (): Promise< + DashboardSnapshotSummary | undefined + > => { + const dashboardObject: Promise = + newRelicDashboardAPI.getDashboardSnapshot(guid, duration); + return dashboardObject; + }, [guid, duration]); + if (loading) { + return ; + } + if (error) { + return ; + } + const url = + value?.getDashboardSnapshot?.data?.dashboardCreateSnapshotUrl?.replace( + /\pdf$/i, + 'png', + ); + return ( + + + + + + {url ? ( + {`${name} + ) : ( + 'Dashboard loading... , click here to open if it didnt render correctly' + )} + + + + + + ); +}; diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshotList.tsx b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshotList.tsx new file mode 100644 index 0000000000..c839a43ac2 --- /dev/null +++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/DashboardSnapshotList.tsx @@ -0,0 +1,148 @@ +/* + * 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 React, { useState } from 'react'; +import { Grid, Tab, Tabs, makeStyles } from '@material-ui/core'; +import { newRelicDashboardApiRef } from '../../../api'; +import { useApi } from '@backstage/core-plugin-api'; +import { useAsync } from 'react-use'; +import { Progress, ErrorPanel } from '@backstage/core-components'; +import { DashboardSnapshot } from './DashboardSnapshot'; +import { DashboardEntitySummary } from '../../../api/NewRelicDashboardApi'; +import { ResultEntity } from '../../../types/DashboardEntity'; + +interface TabPanelProps { + children?: React.ReactNode; + index: number; + value1: number; +} + +function TabPanel(props: TabPanelProps) { + const { children, value1, index, ...other } = props; + + return ( +

+ ); +} +function a11yProps(index: number) { + return { + id: `simple-tab-${index}`, + 'aria-controls': `simple-tabpanel-${index}`, + }; +} +type Props = { + guid: string; +}; +const useStyles = makeStyles( + theme => ({ + tabsWrapper: { + gridArea: 'pageSubheader', + backgroundColor: theme.palette.background.paper, + paddingLeft: theme.spacing(3), + }, + defaultTab: { + padding: theme.spacing(3, 3), + ...theme.typography.caption, + textTransform: 'uppercase', + fontWeight: 'bold', + color: theme.palette.text.secondary, + }, + selected: { + color: theme.palette.text.primary, + }, + tabRoot: { + '&:hover': { + backgroundColor: theme.palette.background.default, + color: theme.palette.text.primary, + }, + }, + }), + { name: 'DashboardHeaderTabs' }, +); +export const DashboardSnapshotList = ({ guid }: Props) => { + const styles = useStyles(); + const newRelicDashboardAPI = useApi(newRelicDashboardApiRef); + const { value, loading, error } = useAsync(async (): Promise< + DashboardEntitySummary | undefined + > => { + const dashboardObject: Promise = + newRelicDashboardAPI.getDashboardEntity(guid); + return dashboardObject; + }, [guid]); + const [value1, setValue1] = useState(0); + const handleChange = ({}: React.ChangeEvent<{}>, newValue: number) => { + setValue1(newValue); + }; + + if (loading) { + return ; + } + if (error) { + return ; + } + return ( + + + {value?.getDashboardEntity?.data?.actor.entitySearch.results.entities?.map( + (Entity: ResultEntity, index: number) => { + return ( + + ); + }, + )} + + {value?.getDashboardEntity?.data?.actor.entitySearch.results.entities?.map( + (Entity: ResultEntity, index: number) => { + return ( + + + + ); + }, + )} + + ); +}; diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/index.ts b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/index.ts new file mode 100644 index 0000000000..6b17582515 --- /dev/null +++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/DashboardSnapshotList/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 { DashboardSnapshot } from './DashboardSnapshot'; +export { DashboardSnapshotList } from './DashboardSnapshotList'; diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/NewRelicDashboard.tsx b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/NewRelicDashboard.tsx new file mode 100644 index 0000000000..c9cc9bf611 --- /dev/null +++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/NewRelicDashboard.tsx @@ -0,0 +1,44 @@ +/* + * 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 React from 'react'; +import { Grid } from '@material-ui/core'; +import { Page, Content } from '@backstage/core-components'; +import { DashboardEntityList } from './DashboardEntityList'; +import { DashboardSnapshotList } from './DashboardSnapshotList'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { NEWRELIC_GUID_ANNOTATION } from '../../constants'; + +export const NewRelicDashboard = () => { + const { entity } = useEntity(); + return ( + + + + + + + + + + + + + ); +}; diff --git a/plugins/newrelic-dashboard/src/components/NewRelicDashboard/index.ts b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/index.ts new file mode 100644 index 0000000000..668d76a059 --- /dev/null +++ b/plugins/newrelic-dashboard/src/components/NewRelicDashboard/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 { NewRelicDashboard } from './NewRelicDashboard'; diff --git a/plugins/newrelic-dashboard/src/constants.ts b/plugins/newrelic-dashboard/src/constants.ts new file mode 100644 index 0000000000..dd0dcff2d8 --- /dev/null +++ b/plugins/newrelic-dashboard/src/constants.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 const NEWRELIC_GUID_ANNOTATION = 'newrelic.com/dashboard-guid'; diff --git a/plugins/newrelic-dashboard/src/index.ts b/plugins/newrelic-dashboard/src/index.ts new file mode 100644 index 0000000000..821ccd4bda --- /dev/null +++ b/plugins/newrelic-dashboard/src/index.ts @@ -0,0 +1,21 @@ +/* + * 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 { + newRelicDashboardPlugin, + EntityNewRelicDashboardCard, + EntityNewRelicDashboardContent, +} from './plugin'; +export { isNewRelicDashboardAvailable } from './Router'; diff --git a/plugins/newrelic-dashboard/src/plugin.test.ts b/plugins/newrelic-dashboard/src/plugin.test.ts new file mode 100644 index 0000000000..e5eccd930b --- /dev/null +++ b/plugins/newrelic-dashboard/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { newRelicDashboardPlugin } from './plugin'; + +describe('new-relic-dashboard', () => { + it('should export plugin', () => { + expect(newRelicDashboardPlugin).toBeDefined(); + }); +}); diff --git a/plugins/newrelic-dashboard/src/plugin.ts b/plugins/newrelic-dashboard/src/plugin.ts new file mode 100644 index 0000000000..18397902f7 --- /dev/null +++ b/plugins/newrelic-dashboard/src/plugin.ts @@ -0,0 +1,62 @@ +/* + * 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 { + createPlugin, + configApiRef, + createApiFactory, + discoveryApiRef, + createComponentExtension, +} from '@backstage/core-plugin-api'; +import { newRelicDashboardApiRef, NewRelicDashboardClient } from './api'; +import { rootRouteRef } from './routes'; + +export const newRelicDashboardPlugin = createPlugin({ + id: 'new-relic-dashboard', + routes: { + root: rootRouteRef, + }, + apis: [ + createApiFactory({ + api: newRelicDashboardApiRef, + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => + new NewRelicDashboardClient({ + discoveryApi, + baseUrl: configApi.getOptionalString('newrelicdashboard.baseUrl'), + }), + }), + ], +}); +export const EntityNewRelicDashboardContent = newRelicDashboardPlugin.provide( + createComponentExtension({ + name: 'EntityNewRelicDashboardPage', + component: { + lazy: () => import('./Router').then(m => m.Router), + }, + }), +); + +export const EntityNewRelicDashboardCard = newRelicDashboardPlugin.provide( + createComponentExtension({ + name: 'EntityNewRelicDashboardListComponent', + component: { + lazy: () => + import('./components/NewRelicDashboard/DashboardEntityList').then( + m => m.DashboardEntityList, + ), + }, + }), +); diff --git a/plugins/newrelic-dashboard/src/queries/getDashboardParentGuidQuery.ts b/plugins/newrelic-dashboard/src/queries/getDashboardParentGuidQuery.ts new file mode 100644 index 0000000000..3a06384539 --- /dev/null +++ b/plugins/newrelic-dashboard/src/queries/getDashboardParentGuidQuery.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 const getDashboardParentGuidQuery = + 'query ($query: String) {\n actor {\n entitySearch(query: $query) {\n results {\n entities {\n name\n ... on DashboardEntityOutline {\n name\n guid\n }\n permalink\n }\n }\n }\n }\n}\n'; diff --git a/plugins/newrelic-dashboard/src/queries/getDashboardSnapshotQuery.ts b/plugins/newrelic-dashboard/src/queries/getDashboardSnapshotQuery.ts new file mode 100644 index 0000000000..be683ecd7b --- /dev/null +++ b/plugins/newrelic-dashboard/src/queries/getDashboardSnapshotQuery.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 const getDashboardSnapshotQuery = + 'mutation($guid: EntityGuid! , ,$duration: Milliseconds) {\n dashboardCreateSnapshotUrl(guid: $guid , params: {timeWindow: {duration: $duration}})\n}'; diff --git a/plugins/newrelic-dashboard/src/routes.ts b/plugins/newrelic-dashboard/src/routes.ts new file mode 100644 index 0000000000..5bc8b24457 --- /dev/null +++ b/plugins/newrelic-dashboard/src/routes.ts @@ -0,0 +1,20 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'new-relic-dashboard', +}); diff --git a/plugins/newrelic-dashboard/src/setupTests.ts b/plugins/newrelic-dashboard/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/newrelic-dashboard/src/setupTests.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. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/newrelic-dashboard/src/types/DashboardEntity.ts b/plugins/newrelic-dashboard/src/types/DashboardEntity.ts new file mode 100644 index 0000000000..78a1b7913d --- /dev/null +++ b/plugins/newrelic-dashboard/src/types/DashboardEntity.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. + */ + +export type DashboardEntity = { + data: { + actor: { + entitySearch: { + results: { + entities: Array; + }; + }; + }; + }; +}; + +export type ResultEntity = { + guid: string; + permalink: string; + name: string; +}; diff --git a/plugins/newrelic-dashboard/src/types/DashboardSnapshot.ts b/plugins/newrelic-dashboard/src/types/DashboardSnapshot.ts new file mode 100644 index 0000000000..6c11c6f6d7 --- /dev/null +++ b/plugins/newrelic-dashboard/src/types/DashboardSnapshot.ts @@ -0,0 +1,21 @@ +/* + * 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 type DashboardSnapshot = { + data: { + dashboardCreateSnapshotUrl: string; + }; +}; diff --git a/plugins/newrelic-dashboard/src/types/SnapshotDetails.ts b/plugins/newrelic-dashboard/src/types/SnapshotDetails.ts new file mode 100644 index 0000000000..e189ac52c4 --- /dev/null +++ b/plugins/newrelic-dashboard/src/types/SnapshotDetails.ts @@ -0,0 +1,20 @@ +/* + * 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 type SnapshotDetails = { + dashboardSnapshotUrl: string; + permalink: string; +}; 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/newrelic/src/api/index.ts b/plugins/newrelic/src/api/index.ts index 980a690d40..2734dd2b4f 100644 --- a/plugins/newrelic/src/api/index.ts +++ b/plugins/newrelic/src/api/index.ts @@ -55,7 +55,6 @@ export type NewRelicApplications = { export const newRelicApiRef = createApiRef({ id: 'plugin.newrelic.service', - description: 'Used by the NewRelic plugin to make requests', }); const DEFAULT_PROXY_PATH_BASE = '/newrelic'; 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/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 30709fe68b..298819ed09 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -22,7 +22,6 @@ import { import { Box, Grid, - Link, List, ListItem, ListItemIcon, @@ -34,7 +33,12 @@ import GroupIcon from '@material-ui/icons/Group'; import PersonIcon from '@material-ui/icons/Person'; import Alert from '@material-ui/lab/Alert'; import React from 'react'; -import { Avatar, InfoCard, InfoCardVariants } from '@backstage/core-components'; +import { + Avatar, + InfoCard, + InfoCardVariants, + Link, +} from '@backstage/core-components'; const CardTitle = ({ title }: { title?: string }) => title ? ( @@ -83,7 +87,7 @@ export const UserProfileCard = ({ - {profile.email} + {profile.email} )} 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/pagerduty/src/api/client.ts b/plugins/pagerduty/src/api/client.ts index 65ff83e7ba..81d3792ecf 100644 --- a/plugins/pagerduty/src/api/client.ts +++ b/plugins/pagerduty/src/api/client.ts @@ -35,7 +35,6 @@ export class UnauthorizedError extends Error {} export const pagerDutyApiRef = createApiRef({ id: 'plugin.pagerduty.api', - description: 'Used to fetch data from PagerDuty API', }); export class PagerDutyClient implements PagerDutyApi { diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 96c1002e5e..6fbecdbc3d 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -34,7 +34,7 @@ "@backstage/backend-common": "^0.9.12", "@backstage/config": "^0.1.10", "@types/express": "^4.17.6", - "axios": "^0.21.1", + "axios": "^0.24.0", "camelcase-keys": "^6.2.2", "compression": "^1.7.4", "cors": "^2.8.5", 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/rollbar/src/api/RollbarApi.ts b/plugins/rollbar/src/api/RollbarApi.ts index 4c28630db6..3da52338ec 100644 --- a/plugins/rollbar/src/api/RollbarApi.ts +++ b/plugins/rollbar/src/api/RollbarApi.ts @@ -23,8 +23,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const rollbarApiRef = createApiRef({ id: 'plugin.rollbar.service', - description: - 'Used by the Rollbar plugin to make requests to accompanying backend', }); export interface RollbarApi { 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/api.ts b/plugins/scaffolder/src/api.ts index 34e16708ea..d7696fadde 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -30,7 +30,6 @@ import { ListActionsResponse, ScaffolderTask, Status } from './types'; export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', - description: 'Used to make requests towards the scaffolder backend', }); type TemplateParameterSchema = { diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/DescriptionField.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/DescriptionField.tsx new file mode 100644 index 0000000000..6d116cc52b --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/DescriptionField.tsx @@ -0,0 +1,22 @@ +/* + * 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 React from 'react'; +import { MarkdownContent } from '@backstage/core-components'; +import { FieldProps } from '@rjsf/core'; + +export const DescriptionField = ({ description }: FieldProps) => + description && ; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/index.ts b/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/index.ts new file mode 100644 index 0000000000..d6d0bf3499 --- /dev/null +++ b/plugins/scaffolder/src/components/MultistepJsonForm/FieldOverrides/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 { DescriptionField } from './DescriptionField'; diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 738e1f894a..a9c8eaec1d 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -30,6 +30,7 @@ import { Theme as MuiTheme } from '@rjsf/material-ui'; import React, { useState } from 'react'; import { transformSchemaToProps } from './schema'; import { Content, StructuredMetadataTable } from '@backstage/core-components'; +import * as fieldOverrides from './FieldOverrides'; const Form = withTheme(MuiTheme); type Step = { @@ -152,7 +153,7 @@ export const MultistepJsonForm = ({
{groups && - groups.map(group => ( + groups.map((group, index) => ( ))} diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx index 9b66a0a9b6..2ac0def547 100644 --- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -24,11 +24,12 @@ import { Content, ContentHeader, ItemCardGrid, + Link, Progress, WarningPanel, } from '@backstage/core-components'; import { useEntityListProvider } from '@backstage/plugin-catalog-react'; -import { Link, Typography } from '@material-ui/core'; +import { Typography } from '@material-ui/core'; import { TemplateCard } from '../TemplateCard'; export type TemplateListProps = { @@ -73,7 +74,7 @@ export const TemplateList = ({ {!error && !loading && !entities.length && ( No templates found that match your filter. Learn more about{' '} - + adding templates . diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index f3a599de3e..c9c20e25eb 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, UiSchema } from '@rjsf/core'; -import React from 'react'; +import React, { useCallback, useEffect } from 'react'; import { useAsync } from 'react-use'; export const allowArbitraryValues = (uiSchema: UiSchema): boolean => @@ -51,9 +51,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/api-report.md b/plugins/search/api-report.md index 3db26579e3..13a9b15ea1 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -8,6 +8,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { AsyncState } from 'react-use/lib/useAsync'; import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { IconComponent } from '@backstage/core-plugin-api'; import { IndexableDocument } from '@backstage/search-common'; import { JsonObject } from '@backstage/types'; import { default as React_2 } from 'react'; @@ -213,12 +214,28 @@ export const SearchType: ({ // Warning: (ae-missing-release-tag) "SidebarSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SidebarSearch: () => JSX.Element; +export const SidebarSearch: (props: SidebarSearchProps) => JSX.Element; // Warning: (ae-missing-release-tag) "SidebarSearchModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const SidebarSearchModal: () => JSX.Element; +export const SidebarSearchModal: ( + props: SidebarSearchModalProps, +) => JSX.Element; + +// Warning: (ae-missing-release-tag) "SidebarSearchModalProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SidebarSearchModalProps = { + icon?: IconComponent; +}; + +// Warning: (ae-missing-release-tag) "SidebarSearchProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type SidebarSearchProps = { + icon?: IconComponent; +}; // Warning: (ae-forgotten-export) The symbol "SearchContextValue" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "useSearch" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) 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/search/src/apis.ts b/plugins/search/src/apis.ts index fa6d9ce908..4ce638fcca 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -25,7 +25,6 @@ import qs from 'qs'; export const searchApiRef = createApiRef({ id: 'plugin.search.queryservice', - description: 'Used to make requests against the search API', }); export interface SearchApi { diff --git a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx index bf83b8b1ad..aebaecb8b4 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.stories.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.stories.tsx @@ -15,9 +15,10 @@ */ import React from 'react'; -import { List, Link, ListItem } from '@material-ui/core'; +import { List, ListItem } from '@material-ui/core'; import { SearchResult, SearchContext, DefaultResultListItem } from '../index'; import { MemoryRouter } from 'react-router'; +import { Link } from '@backstage/core-components'; export default { title: 'Plugins/Search/SearchResult', @@ -79,7 +80,7 @@ export const Default = () => { default: return ( - + {document.title} - {document.text} diff --git a/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx b/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx index 883829d44e..9f6be2333f 100644 --- a/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx +++ b/plugins/search/src/components/SidebarSearch/SidebarSearch.tsx @@ -19,9 +19,13 @@ import { useNavigate } from 'react-router-dom'; import { rootRouteRef } from '../../plugin'; import { SidebarSearchField } from '@backstage/core-components'; -import { useRouteRef } from '@backstage/core-plugin-api'; +import { useRouteRef, IconComponent } from '@backstage/core-plugin-api'; -export const SidebarSearch = () => { +export type SidebarSearchProps = { + icon?: IconComponent; +}; + +export const SidebarSearch = (props: SidebarSearchProps) => { const searchRoute = useRouteRef(rootRouteRef); const navigate = useNavigate(); const handleSearch = useCallback( @@ -33,5 +37,11 @@ export const SidebarSearch = () => { [navigate, searchRoute], ); - return ; + return ( + + ); }; diff --git a/plugins/search/src/components/SidebarSearch/index.ts b/plugins/search/src/components/SidebarSearch/index.ts index 437234c5b4..1340bfaa9a 100644 --- a/plugins/search/src/components/SidebarSearch/index.ts +++ b/plugins/search/src/components/SidebarSearch/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { SidebarSearch } from './SidebarSearch'; +export type { SidebarSearchProps } from './SidebarSearch'; diff --git a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx index ae38526c9d..b8f20de67d 100644 --- a/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx +++ b/plugins/search/src/components/SidebarSearchModal/SidebarSearchModal.tsx @@ -16,17 +16,23 @@ import React from 'react'; import SearchIcon from '@material-ui/icons/Search'; import { SidebarItem } from '@backstage/core-components'; +import { IconComponent } from '@backstage/core-plugin-api'; import { SearchModal } from '../SearchModal'; import { useSearch } from '../SearchContext'; -export const SidebarSearchModal = () => { +export type SidebarSearchModalProps = { + icon?: IconComponent; +}; + +export const SidebarSearchModal = (props: SidebarSearchModalProps) => { const { open, toggleModal } = useSearch(); + const Icon = props.icon ? props.icon : SearchIcon; return ( <> diff --git a/plugins/search/src/components/SidebarSearchModal/index.ts b/plugins/search/src/components/SidebarSearchModal/index.ts index 849180878d..f667f20da2 100644 --- a/plugins/search/src/components/SidebarSearchModal/index.ts +++ b/plugins/search/src/components/SidebarSearchModal/index.ts @@ -14,3 +14,4 @@ * limitations under the License. */ export { SidebarSearchModal } from './SidebarSearchModal'; +export type { SidebarSearchModalProps } from './SidebarSearchModal'; diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index f5c58450fc..bfc9c52b74 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -36,7 +36,11 @@ export { SidebarSearch, useSearch, } from './components'; -export type { SearchModalProps } from './components'; +export type { + SearchModalProps, + SidebarSearchModalProps, + SidebarSearchProps, +} from './components'; export type { FiltersState } from './components'; export { DefaultResultListItem, 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/sentry/src/api/sentry-api.ts b/plugins/sentry/src/api/sentry-api.ts index d2177cde2a..0c5b709508 100644 --- a/plugins/sentry/src/api/sentry-api.ts +++ b/plugins/sentry/src/api/sentry-api.ts @@ -19,7 +19,6 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const sentryApiRef = createApiRef({ id: 'plugin.sentry.service', - description: 'Used by the Sentry plugin to make requests', }); export interface SentryApi { diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx index d2da63f01f..015cf96ed1 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx @@ -16,9 +16,10 @@ import React from 'react'; import { SentryIssue } from '../../api'; -import { Link, Typography } from '@material-ui/core'; +import { Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import { BackstageTheme } from '@backstage/theme'; +import { Link } from '@backstage/core-components'; function stripText(text: string, maxLength: number) { return text.length > maxLength ? `${text.substr(0, maxLength)}...` : text; @@ -53,7 +54,7 @@ export const ErrorCell = ({ sentryIssue }: { sentryIssue: SentryIssue }) => { return (
- + {stripText(issueType, 28)} 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/shortcuts/src/api/ShortcutApi.ts b/plugins/shortcuts/src/api/ShortcutApi.ts index dcecc9945b..893a2fd74b 100644 --- a/plugins/shortcuts/src/api/ShortcutApi.ts +++ b/plugins/shortcuts/src/api/ShortcutApi.ts @@ -20,7 +20,6 @@ import { Observable } from '@backstage/types'; export const shortcutsApiRef = createApiRef({ id: 'plugin.shortcuts.api', - description: 'API to handle shortcuts in a Backstage Sidebar', }); export interface ShortcutApi { 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/sonarqube/src/api/SonarQubeApi.ts b/plugins/sonarqube/src/api/SonarQubeApi.ts index 5bafd87ca8..3b39b30eb0 100644 --- a/plugins/sonarqube/src/api/SonarQubeApi.ts +++ b/plugins/sonarqube/src/api/SonarQubeApi.ts @@ -35,7 +35,6 @@ export interface FindingSummary { export const sonarQubeApiRef = createApiRef({ id: 'plugin.sonarqube.service', - description: 'Used by the SonarQube plugin to make requests', }); export type SonarQubeApi = { diff --git a/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx b/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx index e5bcfe4f86..547c3008c0 100644 --- a/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx +++ b/plugins/sonarqube/src/components/SonarQubeCard/RatingCard.tsx @@ -14,7 +14,8 @@ * limitations under the License. */ -import { Grid, Typography, Link } from '@material-ui/core'; +import { Link } from '@backstage/core-components'; +import { Grid, Typography } from '@material-ui/core'; import { makeStyles } from '@material-ui/core/styles'; import React, { ReactNode } from 'react'; @@ -62,7 +63,7 @@ export const RatingCard = ({ const classes = useStyles(); return ( - + 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/splunk-on-call/src/api/client.ts b/plugins/splunk-on-call/src/api/client.ts index a0256fcf07..437c6365ed 100644 --- a/plugins/splunk-on-call/src/api/client.ts +++ b/plugins/splunk-on-call/src/api/client.ts @@ -41,7 +41,6 @@ export class UnauthorizedError extends Error {} export const splunkOnCallApiRef = createApiRef({ id: 'plugin.splunk-on-call.api', - description: 'Used to fetch data from Splunk On-Call API', }); export class SplunkOnCallClient implements SplunkOnCallApi { 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-insights/src/api/TechInsightsApi.ts b/plugins/tech-insights/src/api/TechInsightsApi.ts index 2c55930f0a..d3ff9ac2ff 100644 --- a/plugins/tech-insights/src/api/TechInsightsApi.ts +++ b/plugins/tech-insights/src/api/TechInsightsApi.ts @@ -22,7 +22,6 @@ import { EntityName } from '@backstage/catalog-model'; export const techInsightsApiRef = createApiRef({ id: 'plugin.techinsights.service', - description: 'Used by the tech insights plugin to make requests', }); export interface TechInsightsApi { diff --git a/plugins/tech-radar/README.md b/plugins/tech-radar/README.md index 469a12e8e4..beaf2a0724 100644 --- a/plugins/tech-radar/README.md +++ b/plugins/tech-radar/README.md @@ -64,6 +64,10 @@ export interface TechRadarPageProps { } ``` +### Radar properties + +When defining the radar entries you can see the available properties on the file [api](./src/api.ts) + ## Frequently Asked Questions ### Who created the Tech Radar? 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/tech-radar/src/api.ts b/plugins/tech-radar/src/api.ts index db1be29f26..241f3dce85 100644 --- a/plugins/tech-radar/src/api.ts +++ b/plugins/tech-radar/src/api.ts @@ -21,7 +21,6 @@ import { ApiRef, createApiRef } from '@backstage/core-plugin-api'; export const techRadarApiRef: ApiRef = createApiRef( { id: 'plugin.techradar.service', - description: 'Used to populate data in the TechRadar plugin', }, ); diff --git a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx index 320e85f8bf..302e77c1ac 100644 --- a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx +++ b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx @@ -19,7 +19,8 @@ import Dialog from '@material-ui/core/Dialog'; import DialogTitle from '@material-ui/core/DialogTitle'; import { Button, DialogActions, DialogContent } from '@material-ui/core'; import LinkIcon from '@material-ui/icons/Link'; -import { MarkdownContent } from '@backstage/core-components'; +import { Link, MarkdownContent } from '@backstage/core-components'; +import { isValidUrl } from '../../utils/components'; export type Props = { open: boolean; @@ -32,13 +33,6 @@ export type Props = { const RadarDescription = (props: Props): JSX.Element => { const { open, onClose, title, description, url } = props; - const handleClick = () => { - onClose(); - if (url) { - window.location.href = url; - } - }; - return ( @@ -47,13 +41,14 @@ const RadarDescription = (props: Props): JSX.Element => { - {url && ( + {isValidUrl(url) && ( diff --git a/plugins/tech-radar/src/sample.ts b/plugins/tech-radar/src/sample.ts index 2b674f35c8..fee5f8cdbf 100644 --- a/plugins/tech-radar/src/sample.ts +++ b/plugins/tech-radar/src/sample.ts @@ -161,9 +161,10 @@ entries.push({ { ringId: 'use', date: new Date('2020-08-06'), + description: 'long description', }, ], - url: '#', + url: 'https://github.com', key: 'github-actions', id: 'github-actions', title: 'GitHub Actions', diff --git a/plugins/tech-radar/src/utils/components.tsx b/plugins/tech-radar/src/utils/components.tsx index bbfe046b69..53d64065f6 100644 --- a/plugins/tech-radar/src/utils/components.tsx +++ b/plugins/tech-radar/src/utils/components.tsx @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { Link } from '@backstage/core-components'; import React from 'react'; type WithLinkProps = { @@ -21,15 +23,19 @@ type WithLinkProps = { children: React.ReactNode; }; +export function isValidUrl(url: string | undefined): url is string { + return Boolean(url && url !== '#' && url.length > 0); +} + export const WithLink = ({ url, className, children, }: WithLinkProps): JSX.Element => - url ? ( - + isValidUrl(url) ? ( + {children} - + ) : ( <>{children} ); diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 8f48342683..b54714101f 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -60,6 +60,7 @@ type RecommendedDeploymentOptions = { logger: Logger; discovery: PluginEndpointDiscovery; config: Config; + cache?: PluginCacheManager; }; /** @@ -89,7 +90,7 @@ export async function createRouter( // Set up a cache client if configured. let cache: TechDocsCache | undefined; const defaultTtl = config.getOptionalNumber('techdocs.cache.ttl'); - if (isOutOfTheBoxOption(options) && options.cache && defaultTtl) { + if (options.cache && defaultTtl) { const cacheClient = options.cache.getClient({ defaultTtl }); cache = TechDocsCache.fromConfig(config, { cache: cacheClient, logger }); } 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/api.ts b/plugins/techdocs/src/api.ts index a23d9fc7f0..cd130f083d 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -20,12 +20,10 @@ import { createApiRef } from '@backstage/core-plugin-api'; export const techdocsStorageApiRef = createApiRef({ id: 'plugin.techdocs.storageservice', - description: 'Used to make requests towards the techdocs storage', }); export const techdocsApiRef = createApiRef({ id: 'plugin.techdocs.service', - description: 'Used to make requests towards techdocs API', }); export type SyncResult = 'cached' | 'updated'; 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/todo/src/api/types.ts b/plugins/todo/src/api/types.ts index f1f647f6e3..51b846a30f 100644 --- a/plugins/todo/src/api/types.ts +++ b/plugins/todo/src/api/types.ts @@ -107,5 +107,4 @@ export interface TodoApi { */ export const todoApiRef = createApiRef({ id: 'plugin.todo.api', - description: 'Lists TODOs', }); 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/General/UserSettingsThemeToggle.test.tsx b/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx index 83d9e23f83..09b27cb559 100644 --- a/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx +++ b/plugins/user-settings/src/components/General/UserSettingsThemeToggle.test.tsx @@ -21,6 +21,8 @@ import { wrapInTestApp, } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core/styles'; +import CssBaseline from '@material-ui/core/CssBaseline'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { UserSettingsThemeToggle } from './UserSettingsThemeToggle'; @@ -30,7 +32,11 @@ const mockTheme: AppTheme = { id: 'light-theme', title: 'Mock Theme', variant: 'light', - theme: lightTheme, + Provider: ({ children }) => ( + + {children} + + ), }; const apiRegistry = TestApiRegistry.from([ 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/plugins/xcmetrics/src/api/types.ts b/plugins/xcmetrics/src/api/types.ts index 6c7d133259..b4656e9478 100644 --- a/plugins/xcmetrics/src/api/types.ts +++ b/plugins/xcmetrics/src/api/types.ts @@ -193,5 +193,4 @@ export interface XcmetricsApi { export const xcmetricsApiRef = createApiRef({ id: 'plugin.xcmetrics.api', - description: 'Used by the XCMetrics plugin to make requests', }); 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..422531d3a4 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" @@ -10026,6 +9996,13 @@ axios@^0.21.1, axios@^0.21.4: dependencies: follow-redirects "^1.14.0" +axios@^0.24.0: + version "0.24.0" + resolved "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz#804e6fa1e4b9c5288501dd9dff56a7a0940d20d6" + integrity sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA== + dependencies: + follow-redirects "^1.14.4" + axobject-query@^2.2.0: version "2.2.0" resolved "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz#943d47e10c0b704aa42275e20edf3722648989be" @@ -11931,11 +11908,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 +13006,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 +13400,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 +14190,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 +14225,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" @@ -15181,9 +15146,9 @@ find-my-way@^2.2.2: semver-store "^0.3.0" find-process@^1.4.5: - version "1.4.5" - resolved "https://registry.npmjs.org/find-process/-/find-process-1.4.5.tgz#6a0e4c87a32ca927c05cbed7b9078d62ffaac1a4" - integrity sha512-v11rJYYISUWn+s8qZzgGnBvlzRKf3bOtlGFM8H0kw56lGQtOmLuLCzuclA5kehA2j7S5sioOWdI4woT3jDavAw== + version "1.4.7" + resolved "https://registry.npmjs.org/find-process/-/find-process-1.4.7.tgz#8c76962259216c381ef1099371465b5b439ea121" + integrity sha512-/U4CYp1214Xrp3u3Fqr9yNynUrr5Le4y0SsJh2lMDDSbpwYSz3M2SMWQC+wqcx79cN8PQtHQIL8KnuY9M66fdg== dependencies: chalk "^4.0.0" commander "^5.1.0" @@ -15283,15 +15248,10 @@ fn.name@1.x.x: resolved "https://registry.npmjs.org/fn.name/-/fn.name-1.1.0.tgz#26cad8017967aea8731bc42961d04a3d5988accc" integrity sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw== -follow-redirects@^1.0.0: - version "1.13.0" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.0.tgz#b42e8d93a2a7eea5ed88633676d6597bc8e384db" - integrity sha512-aq6gF1BEKje4a9i9+5jimNFIpq4Q1WiwBToeRK5NvZBd/TRsmW8BsJfOEGkr76TbOyPVD3OVDN910EcUNtRYEA== - -follow-redirects@^1.14.0: - version "1.14.4" - resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.4.tgz#838fdf48a8bbdd79e52ee51fb1c94e3ed98b9379" - integrity sha512-zwGkiSXC1MUJG/qmeIFH2HBJx9u0V46QGUe3YR1fXG8bXQxq7fLj0RjLZQ5nubr9qNJUZrH+xUcwXEoXNpfS+g== +follow-redirects@^1.0.0, follow-redirects@^1.14.0, follow-redirects@^1.14.4: + version "1.14.6" + resolved "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.14.6.tgz#8cfb281bbc035b3c067d6cd975b0f6ade6e855cd" + integrity sha512-fhUl5EwSJbbl8AR+uYL2KQDxLkdSjZGR36xy46AO7cOMTrCMON6Sa28FmAnC2tRTDbd/Uuzz3aJBv7EBN7JH8A== for-in@^0.1.3: version "0.1.8" @@ -15919,19 +15879,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 +16939,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 +17464,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 +17590,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 +17611,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 +19615,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 +19753,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 +19788,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= @@ -20296,6 +20241,14 @@ marked@^2.1.1: resolved "https://registry.npmjs.org/marked/-/marked-2.1.3.tgz#bd017cef6431724fd4b27e0657f5ceb14bff3753" integrity sha512-/Q+7MGzaETqifOMWYEA7HVMaZb4XbcRfaOzcSsHZEith83KGlvaSG33u0SKu89Mj5h+T8V2hM+8O45Qc5XTgwA== +material-ui-search-bar@^1.0.0: + version "1.0.0" + resolved "https://registry.npmjs.org/material-ui-search-bar/-/material-ui-search-bar-1.0.0.tgz#2652dd5bdc4cb043cffb7144d9c296c120702e62" + integrity sha512-lCNuzMLPBVukVAkcnYKLXHneozsuKZREZNOcc8z9S9scXHqxJzhC9hOS3OC3/YJ+NJEB5lZB9zg1gryBaXEu8w== + dependencies: + classnames "^2.2.5" + prop-types "^15.5.8" + math-expression-evaluator@^1.2.14: version "1.2.22" resolved "https://registry.npmjs.org/math-expression-evaluator/-/math-expression-evaluator-1.2.22.tgz#c14dcb3d8b4d150e5dcea9c68c8dad80309b0d5e" @@ -21327,12 +21280,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 +22171,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 +23006,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 +24863,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 +24889,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 +25528,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 +25780,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 +27227,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 +28179,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 +28386,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 +29078,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 +30120,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"