diff --git a/.changeset/afraid-planets-crash.md b/.changeset/afraid-planets-crash.md new file mode 100644 index 0000000000..b31a492f3d --- /dev/null +++ b/.changeset/afraid-planets-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +**BREAKING**: Renamed `RootHttpRouterConfigureOptions` to `RootHttpRouterConfigureContext`, and removed the unused type `ServiceOrExtensionPoint`. diff --git a/.changeset/big-bags-glow.md b/.changeset/big-bags-glow.md new file mode 100644 index 0000000000..eb606838fd --- /dev/null +++ b/.changeset/big-bags-glow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Add additional validation as security precations for output entities. diff --git a/.changeset/brave-lemons-chew.md b/.changeset/brave-lemons-chew.md new file mode 100644 index 0000000000..61bf7cc6af --- /dev/null +++ b/.changeset/brave-lemons-chew.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-playlist-backend': patch +'@backstage/plugin-catalog-backend': patch +--- + +Minor improvements to the descriptions provided with permission rules schemas diff --git a/.changeset/breezy-ears-rule.md b/.changeset/breezy-ears-rule.md new file mode 100644 index 0000000000..753554187d --- /dev/null +++ b/.changeset/breezy-ears-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-gitlab': patch +--- + +The config now consistently uses the term 'instance' to refer to a single GitLab API host. diff --git a/.changeset/bright-plums-cough.md b/.changeset/bright-plums-cough.md new file mode 100644 index 0000000000..9af30f002e --- /dev/null +++ b/.changeset/bright-plums-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING**: Split out the hook for both lifecycle services so that the first parameter of `addShutdownHook` is the hook function, and the second is the options. diff --git a/.changeset/bright-teachers-compete.md b/.changeset/bright-teachers-compete.md new file mode 100644 index 0000000000..3cc900d613 --- /dev/null +++ b/.changeset/bright-teachers-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING**: The plugin ID option passed to `createBackendPlugin` is now `pluginId`, rather than just `id`. This is to make it match `createBackendModule` more closely. diff --git a/.changeset/brown-kings-wait.md b/.changeset/brown-kings-wait.md new file mode 100644 index 0000000000..db3fc0ee86 --- /dev/null +++ b/.changeset/brown-kings-wait.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +The list of entities takes into account the title when its different from the name to sort the entities. diff --git a/.changeset/chatty-lemons-reply.md b/.changeset/chatty-lemons-reply.md new file mode 100644 index 0000000000..118f832646 --- /dev/null +++ b/.changeset/chatty-lemons-reply.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend-module-elasticsearch': patch +--- + +Export `decodeElasticSearchPageCursor` utility function diff --git a/.changeset/chilled-brooms-jog.md b/.changeset/chilled-brooms-jog.md new file mode 100644 index 0000000000..d848246b2b --- /dev/null +++ b/.changeset/chilled-brooms-jog.md @@ -0,0 +1,7 @@ +--- +'@backstage/backend-common': patch +--- + +**BREAKING**: The `CacheClient` interface must now also implement the `withOptions` method. The `.get()` method has also received a type parameter that helps ensure that `undefined` in the event of a cache miss is handled. + +Added a `cacheToPluginCacheManager` helper that converts a `CacheService` into a legacy `PluginCacheManager` instance. diff --git a/.changeset/chilled-sheep-collect.md b/.changeset/chilled-sheep-collect.md new file mode 100644 index 0000000000..b944685e16 --- /dev/null +++ b/.changeset/chilled-sheep-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Removed unnecessary `@backstage/cli` dependency. diff --git a/.changeset/cool-radios-hope.md b/.changeset/cool-radios-hope.md new file mode 100644 index 0000000000..4a66925d05 --- /dev/null +++ b/.changeset/cool-radios-hope.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-techdocs-node': minor +'@techdocs/cli': patch +--- + +Deprecated getMkDocsYml in favor of getMkdocsYml (lowercase 'd') diff --git a/.changeset/dull-fishes-suffer.md b/.changeset/dull-fishes-suffer.md new file mode 100644 index 0000000000..54baae404e --- /dev/null +++ b/.changeset/dull-fishes-suffer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Removed the unused `TypesToServiceRef` type diff --git a/.changeset/dull-pianos-shave.md b/.changeset/dull-pianos-shave.md new file mode 100644 index 0000000000..8fae999bda --- /dev/null +++ b/.changeset/dull-pianos-shave.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added aria labels on the support button and sidebar diff --git a/.changeset/eighty-knives-repeat.md b/.changeset/eighty-knives-repeat.md new file mode 100644 index 0000000000..3e32a3589d --- /dev/null +++ b/.changeset/eighty-knives-repeat.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-app-api': patch +'@backstage/backend-common': patch +--- + +Updated to match the new `CacheService` interface. diff --git a/.changeset/eleven-shoes-crash.md b/.changeset/eleven-shoes-crash.md new file mode 100644 index 0000000000..8013f311d5 --- /dev/null +++ b/.changeset/eleven-shoes-crash.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': patch +'@backstage/plugin-scaffolder': patch +--- + +scaffolder/next: Implementing a simple `OngoingTask` page diff --git a/.changeset/empty-games-cheer.md b/.changeset/empty-games-cheer.md new file mode 100644 index 0000000000..d17e97f752 --- /dev/null +++ b/.changeset/empty-games-cheer.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-api-docs-module-protoc-gen-doc': patch +'@backstage/plugin-code-coverage-backend': patch +'@backstage/plugin-azure-devops-backend': patch +'@backstage/backend-test-utils': patch +'@backstage/plugin-analytics-module-ga': patch +'@backstage/plugin-azure-sites-backend': patch +'@backstage/plugin-microsoft-calendar': patch +'@backstage/plugin-catalog-graph': patch +'@backstage/plugin-code-coverage': patch +'@backstage/plugin-adr-backend': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-lighthouse': patch +'@backstage/plugin-shortcuts': patch +'@backstage/plugin-xcmetrics': patch +'@backstage/plugin-allure': patch +'@backstage/plugin-ilert': patch +'@backstage/plugin-home': patch +--- + +Update docs to always use `yarn add --cwd` for app & backend diff --git a/.changeset/famous-squids-wonder.md b/.changeset/famous-squids-wonder.md new file mode 100644 index 0000000000..5b027e9a63 --- /dev/null +++ b/.changeset/famous-squids-wonder.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Location rule target patterns now also match hidden files, i.e. path components with a leading dot. diff --git a/.changeset/few-meals-buy.md b/.changeset/few-meals-buy.md new file mode 100644 index 0000000000..c49eb8c7ac --- /dev/null +++ b/.changeset/few-meals-buy.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-aws': patch +--- + +Update to import `LocationSpec` from the correct package. diff --git a/.changeset/few-moons-accept.md b/.changeset/few-moons-accept.md new file mode 100644 index 0000000000..c790e837c8 --- /dev/null +++ b/.changeset/few-moons-accept.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Replaced several monorepo lint rules with new rules from `@backstage/eslint-plugin`. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for a full list of rules. diff --git a/.changeset/few-trees-cross.md b/.changeset/few-trees-cross.md new file mode 100644 index 0000000000..ed70c3bddb --- /dev/null +++ b/.changeset/few-trees-cross.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fixed Webpack loader patterns so that they properly match the file suffix only. diff --git a/.changeset/flat-lizards-poke.md b/.changeset/flat-lizards-poke.md new file mode 100644 index 0000000000..bc180836fa --- /dev/null +++ b/.changeset/flat-lizards-poke.md @@ -0,0 +1,10 @@ +--- +'@techdocs/cli': patch +--- + +Fix proxying to mkdocs + +The domain localhost may point to both 127.0.0.1 and ::1, ipv4 and ipv6 +and when node tries to lookup localhost it might prefer ipv6 while mkdocs +is only listening on ipv4. This tells node-proxy to target the ipv4 address +instead of relying on localhost hostname lookup. diff --git a/.changeset/four-tables-fix.md b/.changeset/four-tables-fix.md new file mode 100644 index 0000000000..f17f57e253 --- /dev/null +++ b/.changeset/four-tables-fix.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Added explicit return type signature for `mockServices.config()`. diff --git a/.changeset/friendly-chefs-rule.md b/.changeset/friendly-chefs-rule.md new file mode 100644 index 0000000000..872a36b5ae --- /dev/null +++ b/.changeset/friendly-chefs-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +--- + +Removing extra imports for `run` script as `TestBackend` auto loads the default factories diff --git a/.changeset/gorgeous-ligers-burn.md b/.changeset/gorgeous-ligers-burn.md new file mode 100644 index 0000000000..f1d8f2cf5f --- /dev/null +++ b/.changeset/gorgeous-ligers-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Added a global override for `window.open` that helps prevent security vulnerabilities. diff --git a/.changeset/gorgeous-llamas-mate.md b/.changeset/gorgeous-llamas-mate.md new file mode 100644 index 0000000000..a2553875e9 --- /dev/null +++ b/.changeset/gorgeous-llamas-mate.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-entity-feedback-backend': minor +'@backstage/plugin-entity-feedback-common': minor +'@backstage/plugin-entity-feedback': minor +--- + +Implement entity feedback plugin, check out the `README.md` for more details! diff --git a/.changeset/great-goats-talk.md b/.changeset/great-goats-talk.md new file mode 100644 index 0000000000..92a1cd59ee --- /dev/null +++ b/.changeset/great-goats-talk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': minor +--- + +Adds a new tRPC API definition widget which users can utilize to integrate their tRPC API definitions into Backstage. diff --git a/.changeset/green-adults-retire.md b/.changeset/green-adults-retire.md new file mode 100644 index 0000000000..c13e8d664f --- /dev/null +++ b/.changeset/green-adults-retire.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-dev-utils': minor +--- + +Introduced a new package for backend development utilities. Similar to how `@backstage/dev-utils` is used in the frontend. diff --git a/.changeset/green-guests-flow.md b/.changeset/green-guests-flow.md new file mode 100644 index 0000000000..688ff17dd9 --- /dev/null +++ b/.changeset/green-guests-flow.md @@ -0,0 +1,8 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/backend-app-api': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +--- + +Updated usage of the lifecycle service. diff --git a/.changeset/grumpy-fireants-drop.md b/.changeset/grumpy-fireants-drop.md new file mode 100644 index 0000000000..92b08663f4 --- /dev/null +++ b/.changeset/grumpy-fireants-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/codemods': patch +--- + +Moved `commander` to being a regular dependency. diff --git a/.changeset/heavy-comics-applaud.md b/.changeset/heavy-comics-applaud.md new file mode 100644 index 0000000000..faa0aabb4c --- /dev/null +++ b/.changeset/heavy-comics-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +Added `DescriptionField` field override to the `next/scaffolder` diff --git a/.changeset/hip-chairs-double.md b/.changeset/hip-chairs-double.md new file mode 100644 index 0000000000..e2b03f2e66 --- /dev/null +++ b/.changeset/hip-chairs-double.md @@ -0,0 +1,19 @@ +--- +'@backstage/plugin-tech-radar': patch +--- + +Deprecate `RadarEntry.url` - use `RadarEntry.links` instead + +```diff +- url: 'https://www.javascript.com/', + key: 'javascript', + id: 'javascript', + title: 'JavaScript', + quadrant: 'languages', + links: [ ++ { ++ url: 'https://www.javascript.com/', ++ title: 'Learn more', ++ }, + ], +``` diff --git a/.changeset/hip-onions-tan.md b/.changeset/hip-onions-tan.md new file mode 100644 index 0000000000..01be0d4867 --- /dev/null +++ b/.changeset/hip-onions-tan.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +Change the router's response to include the error message instead of its object type in case it fails during a search query because the messages have more info. diff --git a/.changeset/hip-tables-prove.md b/.changeset/hip-tables-prove.md new file mode 100644 index 0000000000..bc2c4e83d6 --- /dev/null +++ b/.changeset/hip-tables-prove.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Updated the `mockServices.rootLogger` options to accept a single level option instead. diff --git a/.changeset/honest-timers-pull.md b/.changeset/honest-timers-pull.md new file mode 100644 index 0000000000..d11d260a22 --- /dev/null +++ b/.changeset/honest-timers-pull.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder-react': minor +'@backstage/plugin-scaffolder': minor +--- + +Migrating the `TemplateEditorPage` to work with the new components from `@backstage/plugin-scaffolder-react` diff --git a/.changeset/hot-wasps-cover.md b/.changeset/hot-wasps-cover.md new file mode 100644 index 0000000000..e142980b56 --- /dev/null +++ b/.changeset/hot-wasps-cover.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Updated to work with the new `type: 'pem'` with `createHttpServer` from `@backstage/backend-app-api` diff --git a/.changeset/hungry-stingrays-type.md b/.changeset/hungry-stingrays-type.md new file mode 100644 index 0000000000..8cb11f67a5 --- /dev/null +++ b/.changeset/hungry-stingrays-type.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs-backend': patch +--- + +Keep the tech docs sync event stream alive even if it is taking a while to build. diff --git a/.changeset/itchy-chairs-punch.md b/.changeset/itchy-chairs-punch.md new file mode 100644 index 0000000000..034a9bbdae --- /dev/null +++ b/.changeset/itchy-chairs-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': patch +--- + +Aligned opaque type markers to all use a `$$type` property with namespacing. diff --git a/.changeset/itchy-rings-rule.md b/.changeset/itchy-rings-rule.md new file mode 100644 index 0000000000..3e8b9e8b2b --- /dev/null +++ b/.changeset/itchy-rings-rule.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Fixed an issue where entities sometimes were not properly deleted during a full mutation. diff --git a/.changeset/light-crabs-drum.md b/.changeset/light-crabs-drum.md new file mode 100644 index 0000000000..ebc0b3fa5a --- /dev/null +++ b/.changeset/light-crabs-drum.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-adr-backend': minor +'@backstage/plugin-adr': minor +--- + +Display title, status and date in ADR navigation, sourced from ADR content and reverse order. diff --git a/.changeset/little-jars-lay.md b/.changeset/little-jars-lay.md new file mode 100644 index 0000000000..f8851f7adc --- /dev/null +++ b/.changeset/little-jars-lay.md @@ -0,0 +1,14 @@ +--- +'@backstage/plugin-user-settings-backend': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-periskop-backend': patch +'@backstage/backend-common': patch +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-events-backend': patch +'@backstage/plugin-kafka-backend': patch +'@backstage/plugin-proxy-backend': patch +'@backstage/plugin-app-backend': patch +--- + +Updated usage of `createBackendPlugin`. diff --git a/.changeset/long-chefs-burn.md b/.changeset/long-chefs-burn.md new file mode 100644 index 0000000000..03a7b76db9 --- /dev/null +++ b/.changeset/long-chefs-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Updated database factory to pass service deps required for restoring database state during development. diff --git a/.changeset/lucky-papayas-allow.md b/.changeset/lucky-papayas-allow.md new file mode 100644 index 0000000000..5a896efd0b --- /dev/null +++ b/.changeset/lucky-papayas-allow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Added a missing validator check for items in an array diff --git a/.changeset/many-pans-unite.md b/.changeset/many-pans-unite.md new file mode 100644 index 0000000000..59c068cec1 --- /dev/null +++ b/.changeset/many-pans-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': minor +--- + +Adds the tRPC API type to the catalog. diff --git a/.changeset/mean-squids-kneel.md b/.changeset/mean-squids-kneel.md new file mode 100644 index 0000000000..41a1bd4086 --- /dev/null +++ b/.changeset/mean-squids-kneel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-vault': patch +--- + +Surface additional context and details to the Backstage UI when the Vault plugin encounters non-successful HTTP responses from the Vault API. diff --git a/.changeset/nasty-kids-deny.md b/.changeset/nasty-kids-deny.md new file mode 100644 index 0000000000..4a434eb1f6 --- /dev/null +++ b/.changeset/nasty-kids-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/theme': patch +--- + +Use same table header color as @backstage/core-components Table to meet accessibility color contrast requirements. This change affects material-ui tables. diff --git a/.changeset/neat-schools-raise.md b/.changeset/neat-schools-raise.md new file mode 100644 index 0000000000..b778ba8795 --- /dev/null +++ b/.changeset/neat-schools-raise.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +Kubernetes proxy endpoint now accepts content types that are not json diff --git a/.changeset/nervous-phones-shout.md b/.changeset/nervous-phones-shout.md new file mode 100644 index 0000000000..a4e46b3be9 --- /dev/null +++ b/.changeset/nervous-phones-shout.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/backend-app-api': patch +--- + +Updates to match new `BackendFeature` type. diff --git a/.changeset/new-pumpkins-sleep.md b/.changeset/new-pumpkins-sleep.md new file mode 100644 index 0000000000..cc5b89806d --- /dev/null +++ b/.changeset/new-pumpkins-sleep.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-react': patch +--- + +bump `@rjsf` `v5` dependencies to 5.1.0 diff --git a/.changeset/nine-ravens-kiss.md b/.changeset/nine-ravens-kiss.md new file mode 100644 index 0000000000..a507db6b03 --- /dev/null +++ b/.changeset/nine-ravens-kiss.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr': minor +--- + +The `AdrSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. diff --git a/.changeset/odd-kangaroos-itch.md b/.changeset/odd-kangaroos-itch.md new file mode 100644 index 0000000000..8529e5fe04 --- /dev/null +++ b/.changeset/odd-kangaroos-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Fix #16245: The CatalogGraphCard has parameter naming errors for CatalogGraphPage diff --git a/.changeset/old-ways-know.md b/.changeset/old-ways-know.md new file mode 100644 index 0000000000..60a1a923e8 --- /dev/null +++ b/.changeset/old-ways-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +The `DatabaseManager.forPlugin` method now accepts additional service dependencies. There is no need to update existing code to pass these dependencies. diff --git a/.changeset/olive-ads-peel.md b/.changeset/olive-ads-peel.md new file mode 100644 index 0000000000..7a7c73ddb7 --- /dev/null +++ b/.changeset/olive-ads-peel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Provide better error messaging when GitHub fails due to missing team definitions diff --git a/.changeset/perfect-pears-care.md b/.changeset/perfect-pears-care.md new file mode 100644 index 0000000000..2ea9f54a7f --- /dev/null +++ b/.changeset/perfect-pears-care.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-techdocs-backend': patch +'@backstage/plugin-techdocs-node': patch +'@techdocs/cli': patch +--- + +Added support for using a default `mkdocs.yml` configuration file when none is provided diff --git a/.changeset/pre.json b/.changeset/pre.json index 8d412f53b5..01c51bcc4b 100644 --- a/.changeset/pre.json +++ b/.changeset/pre.json @@ -1,5 +1,5 @@ { - "mode": "pre", + "mode": "exit", "tag": "next", "initialVersions": { "example-app": "0.2.79", @@ -195,82 +195,149 @@ "@backstage/plugin-vault": "0.1.8", "@backstage/plugin-vault-backend": "0.2.6", "@backstage/plugin-xcmetrics": "0.2.34", - "@backstage/plugin-scaffolder-node": "0.0.0" + "@backstage/plugin-scaffolder-node": "0.0.0", + "@backstage/backend-dev-utils": "0.0.0", + "@backstage/eslint-plugin": "0.0.0", + "@backstage/plugin-entity-validation": "0.0.0", + "@backstage/plugin-graphql-voyager": "0.0.0", + "@backstage/plugin-lighthouse-backend": "0.0.0", + "@backstage/plugin-lighthouse-common": "0.0.0" }, "changesets": [ "afraid-foxes-provide", + "afraid-planets-crash", "brave-cougars-burn", + "breezy-ears-rule", "bright-eagles-love", + "bright-plums-cough", + "bright-teachers-compete", "brown-islands-own", "calm-avocados-exercise", "chatty-owls-care", + "chilled-sheep-collect", "clean-queens-judge", "cold-cycles-switch", + "cool-spoons-bathe", "create-app-1674561612", "cuddly-boxes-agree", "cyan-deers-walk", "dry-donkeys-cheer", + "dull-fishes-suffer", "dull-gorillas-sing", "eight-falcons-explode", "eight-hotels-sparkle", + "eight-vans-drum", + "eleven-shoes-crash", + "few-moons-accept", "flat-cups-itch", "four-candles-add", "four-rivers-enjoy", + "four-tables-fix", + "friendly-chefs-rule", "friendly-scissors-shop", "gold-bulldogs-talk", "gold-lemons-eat", "gold-masks-sleep", + "great-goats-talk", + "green-adults-retire", + "green-guests-flow", "grumpy-bottles-stare", + "grumpy-fireants-drop", "happy-cows-moo", + "hip-chairs-double", + "hip-onions-tan", "hip-pugs-teach", + "hip-tables-prove", "hot-lions-cover", + "hot-wasps-cover", "hungry-news-fix", + "itchy-chairs-punch", "itchy-goats-melt", "khaki-toes-care", "late-rice-crash", "lazy-badgers-rule", "lazy-badgers-try", "lemon-tables-train", + "little-jars-lay", + "long-chefs-burn", "lovely-ladybugs-taste", "many-nails-joke", + "many-pans-unite", + "mean-squids-kneel", "modern-cats-worry", "nasty-beans-accept", + "neat-schools-raise", "neat-zebras-run", "nervous-apricots-whisper", "nervous-mangos-rhyme", + "nervous-phones-shout", + "new-pumpkins-sleep", "nine-glasses-invent", "nine-guests-compete", + "nine-ravens-kiss", "ninety-hats-serve", "old-knives-wonder", + "old-ways-know", "perfect-cheetahs-serve", "pink-falcons-serve", "poor-moons-confess", "popular-dancers-join", "pretty-ladybugs-taste", + "proud-glasses-live", "purple-feet-smile", "quick-ladybugs-reply", + "quick-mice-change", + "rare-berries-give", "rare-melons-battle", + "red-kiwis-compare", "renovate-0b349f9", "renovate-0c3644c", + "renovate-53e8b07", "rich-snakes-rest", "rotten-mayflies-love", + "selfish-frogs-punch", + "serious-pigs-watch", "sharp-lobsters-build", "shiny-years-tap", + "short-apes-relate", + "shy-buses-mix", "shy-steaks-invite", + "silent-peas-fly", + "silly-buckets-joke", + "silly-socks-lie", "silly-turkeys-hang", + "silver-wolves-breathe", + "slow-carrots-sniff", + "slow-moles-sin", "smart-dingos-raise", + "soft-actors-worry", "soft-boxes-buy", "soft-snails-notice", "spotty-coats-clean", "stale-dots-smile", + "stale-experts-punch", "stupid-ladybugs-brake", "swift-fishes-smash", + "tall-falcons-teach", + "tall-tables-hide", "tall-years-relate", + "tasty-jobs-help", "thick-clocks-pump", + "thick-tables-travel", + "thin-donkeys-attack", + "thin-scissors-press", + "tough-jeans-camp", "twenty-islands-wonder", "twenty-parents-relate", "two-melons-lie", + "violet-tips-rhyme", + "weak-radios-sing", + "wicked-beers-walk", + "wild-donkeys-rescue", "witty-moose-itch", + "yellow-roses-tell", + "young-beans-float", + "young-points-drop", "young-singers-learn" ] } diff --git a/.changeset/pretty-icons-glow.md b/.changeset/pretty-icons-glow.md new file mode 100644 index 0000000000..6023e47307 --- /dev/null +++ b/.changeset/pretty-icons-glow.md @@ -0,0 +1,8 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-dev-utils': patch +'@backstage/backend-defaults': patch +'@backstage/backend-app-api': patch +--- + +Tweaked messaging in the README. diff --git a/.changeset/pretty-jars-peel.md b/.changeset/pretty-jars-peel.md new file mode 100644 index 0000000000..639d4565ed --- /dev/null +++ b/.changeset/pretty-jars-peel.md @@ -0,0 +1,9 @@ +--- +'@backstage/core-plugin-api': minor +'@backstage/app-defaults': minor +'@backstage/core-app-api': minor +'@backstage/plugin-user-settings': minor +'@backstage/plugin-auth-backend': minor +--- + +Added a Bitbucket Server Auth Provider and added its API to the app defaults diff --git a/.changeset/proud-glasses-live.md b/.changeset/proud-glasses-live.md new file mode 100644 index 0000000000..ded7a53ce7 --- /dev/null +++ b/.changeset/proud-glasses-live.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-adr': patch +--- + +Support displaying an icon on `AdrSearchResultListItem` diff --git a/.changeset/purple-panthers-know.md b/.changeset/purple-panthers-know.md new file mode 100644 index 0000000000..c53c6d8890 --- /dev/null +++ b/.changeset/purple-panthers-know.md @@ -0,0 +1,5 @@ +--- +'@backstage/catalog-model': patch +--- + +Add additional validation for location references. diff --git a/.changeset/quick-mice-change.md b/.changeset/quick-mice-change.md new file mode 100644 index 0000000000..aeee24a021 --- /dev/null +++ b/.changeset/quick-mice-change.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/backend-defaults': patch +--- + +Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` diff --git a/.changeset/quiet-pets-count.md b/.changeset/quiet-pets-count.md new file mode 100644 index 0000000000..bc7da8f3e7 --- /dev/null +++ b/.changeset/quiet-pets-count.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/backend-app-api': patch +--- + +Updated usage of `ServiceFactory`. diff --git a/.changeset/rare-berries-give.md b/.changeset/rare-berries-give.md new file mode 100644 index 0000000000..39e360bf48 --- /dev/null +++ b/.changeset/rare-berries-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/repo-tools': patch +--- + +Packages without a declared `backstage.role` are now ignored. diff --git a/.changeset/red-kiwis-compare.md b/.changeset/red-kiwis-compare.md new file mode 100644 index 0000000000..6045367105 --- /dev/null +++ b/.changeset/red-kiwis-compare.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +The shutdown signal handlers are now installed as part of the backend instance rather than the lifecycle service, and explicitly cause the process to exit. diff --git a/.changeset/red-pugs-tap.md b/.changeset/red-pugs-tap.md new file mode 100644 index 0000000000..85da2b7990 --- /dev/null +++ b/.changeset/red-pugs-tap.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Updated Link URL validation to be more strict. diff --git a/.changeset/red-toes-suffer.md b/.changeset/red-toes-suffer.md new file mode 100644 index 0000000000..3d819fb298 --- /dev/null +++ b/.changeset/red-toes-suffer.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +'@backstage/plugin-kubernetes-backend': patch +'@backstage/plugin-playlist-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-vault-backend': patch +--- + +Moving the backend-test-utils to devDependencies. diff --git a/.changeset/renovate-53e8b07.md b/.changeset/renovate-53e8b07.md new file mode 100644 index 0000000000..cb19d8fe8b --- /dev/null +++ b/.changeset/renovate-53e8b07.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-api-docs': patch +--- + +Updated dependency `@asyncapi/react-component` to `1.0.0-next.47`. diff --git a/.changeset/rich-bags-sleep.md b/.changeset/rich-bags-sleep.md new file mode 100644 index 0000000000..14312ac7f4 --- /dev/null +++ b/.changeset/rich-bags-sleep.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-pagerduty': patch +--- + +The PagerDutyCard now supports an optional `readOnly` property (``) for suppressing the rendering of the "Create Incident" button from the Backstage UI. diff --git a/.changeset/rotten-birds-build.md b/.changeset/rotten-birds-build.md new file mode 100644 index 0000000000..e165129998 --- /dev/null +++ b/.changeset/rotten-birds-build.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-microsoft-calendar': minor +--- + +Created a new plugin `@backstage/plugin-microsoft-calendar` to display events from a Microsoft Calendar. + +please refer to the [README.md](https://github.com/backstage/backstage/blob/master/plugins/microsoft-calendar/README.md) for step-by-step instructions to setup the plugin in your Backstage instance. diff --git a/.changeset/selfish-frogs-punch.md b/.changeset/selfish-frogs-punch.md new file mode 100644 index 0000000000..6484576af8 --- /dev/null +++ b/.changeset/selfish-frogs-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING**: Switched out `LogMeta` type for `JsonObject`. diff --git a/.changeset/serious-grapes-brush.md b/.changeset/serious-grapes-brush.md new file mode 100644 index 0000000000..94eead7282 --- /dev/null +++ b/.changeset/serious-grapes-brush.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-github': patch +--- + +Use github team slug and login annotations rather than entity name to infer location annotations diff --git a/.changeset/serious-horses-juggle.md b/.changeset/serious-horses-juggle.md new file mode 100644 index 0000000000..13be1d9715 --- /dev/null +++ b/.changeset/serious-horses-juggle.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-linguist': minor +'@backstage/plugin-linguist-backend': minor +'@backstage/plugin-linguist-common': minor +--- + +Introduced the Linguist plugin, checkout the plugin's `README.md` for more details! diff --git a/.changeset/serious-pigs-watch.md b/.changeset/serious-pigs-watch.md new file mode 100644 index 0000000000..2efd8697f3 --- /dev/null +++ b/.changeset/serious-pigs-watch.md @@ -0,0 +1,10 @@ +--- +'@backstage/plugin-techdocs': patch +'@backstage/plugin-catalog': patch +'@backstage/plugin-explore': patch +'@backstage/plugin-search-react': patch +--- + +`ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + +Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. diff --git a/.changeset/short-apes-relate.md b/.changeset/short-apes-relate.md new file mode 100644 index 0000000000..d93c046a4d --- /dev/null +++ b/.changeset/short-apes-relate.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +**BREAKING** Renaming `*Factory` exports to `*ServiceFactory` instead. For example `configFactory` now is exported as `configServiceFactory`. diff --git a/.changeset/shy-buses-mix.md b/.changeset/shy-buses-mix.md new file mode 100644 index 0000000000..b7db90d03c --- /dev/null +++ b/.changeset/shy-buses-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-react': minor +--- + +refactor `createAsyncValidators` to be recursive to ensure validators are called in nested schemas. diff --git a/.changeset/silent-peas-fly.md b/.changeset/silent-peas-fly.md new file mode 100644 index 0000000000..8148cb5d12 --- /dev/null +++ b/.changeset/silent-peas-fly.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Moved the options of the `config` and `rootHttpRouter` services out to the factories themselves, where they belong diff --git a/.changeset/silent-roses-join.md b/.changeset/silent-roses-join.md new file mode 100644 index 0000000000..5794c8701c --- /dev/null +++ b/.changeset/silent-roses-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +The new backend system testing utilities have now been marked as stable API. diff --git a/.changeset/silly-buckets-joke.md b/.changeset/silly-buckets-joke.md new file mode 100644 index 0000000000..113272c965 --- /dev/null +++ b/.changeset/silly-buckets-joke.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Fixed a bug reading the `user.select` field expected from the `app-config.yaml` configuration diff --git a/.changeset/silly-socks-lie.md b/.changeset/silly-socks-lie.md new file mode 100644 index 0000000000..ae7e1c0b2a --- /dev/null +++ b/.changeset/silly-socks-lie.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-entity-validation': minor +--- + +First implementation for the entity validation UI. For more information refer to its `README.md`. diff --git a/.changeset/silver-badgers-heal.md b/.changeset/silver-badgers-heal.md new file mode 100644 index 0000000000..b1f239c3dc --- /dev/null +++ b/.changeset/silver-badgers-heal.md @@ -0,0 +1,35 @@ +--- +'@backstage/plugin-techdocs-module-addons-contrib': patch +'@backstage/plugin-techdocs': patch +--- + +Create a TechDocs `` addon that allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. + +Here's an example on how to use it in a Backstage app: + +```diff +import { + DefaultTechDocsHome, + TechDocsIndexPage, + TechDocsReaderPage, +} from '@backstage/plugin-techdocs'; +import { TechDocsAddons } from '@backstage/plugin-techdocs-react/alpha'; ++import { LightBox } from '@backstage/plugin-techdocs-module-addons-contrib'; + +const AppRoutes = () => { + + // other plugin routes + }> + + + } + > + ++ + + + ; +}; +``` diff --git a/.changeset/silver-wolves-breathe.md b/.changeset/silver-wolves-breathe.md new file mode 100644 index 0000000000..8c1529e142 --- /dev/null +++ b/.changeset/silver-wolves-breathe.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sentry': minor +--- + +Add option to filter issues based on the time it was triggered diff --git a/.changeset/slow-carrots-sniff.md b/.changeset/slow-carrots-sniff.md new file mode 100644 index 0000000000..d32c2a281e --- /dev/null +++ b/.changeset/slow-carrots-sniff.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': minor +--- + +Improve performance when loading users via group membership. +Users data is now loaded from a paged query, rather than having to make an extra call per user to load each user's profiles. + +Note, there are still additional per user calls made to load user avatars diff --git a/.changeset/slow-moles-sin.md b/.changeset/slow-moles-sin.md new file mode 100644 index 0000000000..02e1952312 --- /dev/null +++ b/.changeset/slow-moles-sin.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-circleci': patch +--- + +Making workflow a link diff --git a/.changeset/slow-sloths-do.md b/.changeset/slow-sloths-do.md new file mode 100644 index 0000000000..2d86d477bf --- /dev/null +++ b/.changeset/slow-sloths-do.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Expose additional props on the `CatalogGraphCard` to allow for custom node & label rendering or kind/relation filtering. diff --git a/.changeset/soft-actors-worry.md b/.changeset/soft-actors-worry.md new file mode 100644 index 0000000000..5186499292 --- /dev/null +++ b/.changeset/soft-actors-worry.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': minor +--- + +**BREAKING** `HttpServerCertificateOptions` when specified with a `key` and `cert` should also have the `type: 'pem'` instead of `type: 'plain'` diff --git a/.changeset/spotty-spiders-love.md b/.changeset/spotty-spiders-love.md new file mode 100644 index 0000000000..e0ac3b3cc7 --- /dev/null +++ b/.changeset/spotty-spiders-love.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Make catalog search result list a bit more customizable diff --git a/.changeset/stale-experts-punch.md b/.changeset/stale-experts-punch.md new file mode 100644 index 0000000000..85872bfffe --- /dev/null +++ b/.changeset/stale-experts-punch.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-user-settings-backend': patch +'@backstage/plugin-periskop-backend': patch +'@backstage/plugin-bazaar-backend': patch +'@backstage/plugin-kafka-backend': patch +'@backstage/plugin-proxy-backend': patch +--- + +Added a Backend System plugin feature diff --git a/.changeset/stale-houses-push.md b/.changeset/stale-houses-push.md new file mode 100644 index 0000000000..ef2213ed7b --- /dev/null +++ b/.changeset/stale-houses-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Updated `LinksGroup` to use `Link` over `ListItem` as this makes the links more obvious and follows the pattern already used in the `GroupProfileCard`. Also updated the `GroupProfileCard` `ExtraDetails` story in Storybook to enable the `showLinks` feature with this off there is no difference between it and the `default` story. diff --git a/.changeset/sweet-yaks-taste.md b/.changeset/sweet-yaks-taste.md new file mode 100644 index 0000000000..cbbbaea497 --- /dev/null +++ b/.changeset/sweet-yaks-taste.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Fix bug in EntityCostsCard if cost aggregation array is empty. Allow title override on the Cost Insights Page diff --git a/.changeset/tall-falcons-teach.md b/.changeset/tall-falcons-teach.md new file mode 100644 index 0000000000..d6608a6597 --- /dev/null +++ b/.changeset/tall-falcons-teach.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-components': patch +--- + +Navigation items in mobile sidebar now have aria label. diff --git a/.changeset/tall-tables-hide.md b/.changeset/tall-tables-hide.md new file mode 100644 index 0000000000..3e8011aff2 --- /dev/null +++ b/.changeset/tall-tables-hide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-incremental-ingestion': patch +--- + +Updated README to include newer API options for incremental entity providers diff --git a/.changeset/tall-years-relate.md b/.changeset/tall-years-relate.md index 10198992ea..73a0e875b0 100644 --- a/.changeset/tall-years-relate.md +++ b/.changeset/tall-years-relate.md @@ -2,4 +2,4 @@ '@backstage/plugin-airbrake': patch --- -Adds a boolean helper function to airbrake plugin for use on the EntityPage to show/hide airbrake tab depending on whether the entity's catalog-info.yml has an airbrake id set in the metadata +Adds a boolean helper function to airbrake plugin for use on the EntityPage to show/hide airbrake tab depending on whether the entity's `catalog-info.yaml` has an airbrake id set in the metadata diff --git a/.changeset/tasty-jobs-help.md b/.changeset/tasty-jobs-help.md new file mode 100644 index 0000000000..ad1df8fb8c --- /dev/null +++ b/.changeset/tasty-jobs-help.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-graphql-voyager': minor +--- + +Add the new GraphQL Voyager tool as a plugin for backstage, expose necessary API and components needed for it. + +For more information on how to use this plugin and configure it, please navigate to the [README](https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager/README.md) of the plugin. diff --git a/.changeset/thick-tables-travel.md b/.changeset/thick-tables-travel.md new file mode 100644 index 0000000000..d74ed18b8a --- /dev/null +++ b/.changeset/thick-tables-travel.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-events-node': patch +--- + +Make `EventParams` typed for implementing tidier event handling. diff --git a/.changeset/thin-donkeys-attack.md b/.changeset/thin-donkeys-attack.md new file mode 100644 index 0000000000..2fd0aebb6a --- /dev/null +++ b/.changeset/thin-donkeys-attack.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-test-utils': patch +--- + +Removed the `ref` from all `mockServices`. diff --git a/.changeset/thin-scissors-press.md b/.changeset/thin-scissors-press.md new file mode 100644 index 0000000000..2891d4ca66 --- /dev/null +++ b/.changeset/thin-scissors-press.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-azure-sites': patch +--- + +Fixed `AzureSites` plugin start and stop issue #15904 +Fixed `AzureSites` UI load issue #15907 diff --git a/.changeset/three-geckos-press.md b/.changeset/three-geckos-press.md new file mode 100644 index 0000000000..fc1af4fdc0 --- /dev/null +++ b/.changeset/three-geckos-press.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Implement a `showCounts` option to display result counts per type in `SearchType.Accordion` diff --git a/.changeset/tidy-kids-hide.md b/.changeset/tidy-kids-hide.md new file mode 100644 index 0000000000..194497d05c --- /dev/null +++ b/.changeset/tidy-kids-hide.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-apache-airflow': patch +'@backstage/plugin-azure-sites': patch +'@backstage/plugin-lighthouse': patch +--- + +Prefer Link over plain anchors diff --git a/.changeset/tidy-kings-cough.md b/.changeset/tidy-kings-cough.md new file mode 100644 index 0000000000..e908572c8b --- /dev/null +++ b/.changeset/tidy-kings-cough.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes-backend': patch +--- + +The name of the header used to specify a cluster to the proxy endpoint is now visible in the API reference. diff --git a/.changeset/tough-jeans-camp.md b/.changeset/tough-jeans-camp.md new file mode 100644 index 0000000000..8c7a53190f --- /dev/null +++ b/.changeset/tough-jeans-camp.md @@ -0,0 +1,5 @@ +--- +'@backstage/eslint-plugin': minor +--- + +Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details. diff --git a/.changeset/twenty-pugs-explain.md b/.changeset/twenty-pugs-explain.md new file mode 100644 index 0000000000..9ac77d2ffb --- /dev/null +++ b/.changeset/twenty-pugs-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +**BREAKING**: The `CacheService` has been changed to remove the indirection of `getClient`, instead making the `CacheClient` methods directly available on the `CacheService`. In order to allow for the creation of clients with default options, there is now a new `.withOptions` method that must be implemented as part of the service interface. diff --git a/.changeset/violet-tips-rhyme.md b/.changeset/violet-tips-rhyme.md new file mode 100644 index 0000000000..c2d95cfbf9 --- /dev/null +++ b/.changeset/violet-tips-rhyme.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Updated colors for each tab used on search modal, to improve color contrast. Aria label added to tabs wrapper component. `disableRipple` property on the `Tab` component removed to improve keyboard navigation indicator. diff --git a/.changeset/weak-radios-sing.md b/.changeset/weak-radios-sing.md new file mode 100644 index 0000000000..dff287b565 --- /dev/null +++ b/.changeset/weak-radios-sing.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Added an experimental mode for the `package start` command for backend packages. Enabled by setting `EXPERIMENTAL_BACKEND_START`. diff --git a/.changeset/wicked-beers-walk.md b/.changeset/wicked-beers-walk.md new file mode 100644 index 0000000000..160a171eb1 --- /dev/null +++ b/.changeset/wicked-beers-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/create-app': patch +--- + +Updated `packages/app/cypress/.eslintrc.json` to remove the unnecessary `import/no-extraneous-dependencies` rule. diff --git a/.changeset/wild-donkeys-rescue.md b/.changeset/wild-donkeys-rescue.md new file mode 100644 index 0000000000..43dc3743bb --- /dev/null +++ b/.changeset/wild-donkeys-rescue.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-test-utils': patch +'@backstage/backend-app-api': patch +--- + +Updated usages of `LogMeta`. diff --git a/.changeset/wild-melons-shout.md b/.changeset/wild-melons-shout.md new file mode 100644 index 0000000000..5a79612f22 --- /dev/null +++ b/.changeset/wild-melons-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +Switch `ServiceFactory` to be an opaque type, keeping only the `service` field as public API, but also adding a type parameter for the service scope. diff --git a/.changeset/yellow-roses-tell.md b/.changeset/yellow-roses-tell.md new file mode 100644 index 0000000000..ad645df294 --- /dev/null +++ b/.changeset/yellow-roses-tell.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-plugin-api': minor +--- + +Switched `BackendFeature` to be an opaque type. diff --git a/.changeset/young-points-drop.md b/.changeset/young-points-drop.md new file mode 100644 index 0000000000..4309d54c2c --- /dev/null +++ b/.changeset/young-points-drop.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Increased default page size to 999 (from 100) to reduce the number of calls made to the Microsoft Graph API. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 82d868a408..10f1ac188c 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -4,78 +4,80 @@ # The last matching pattern takes precedence. # https://help.github.com/articles/about-codeowners/ -* @backstage/reviewers -yarn.lock @backstage/reviewers @backstage-service -*/yarn.lock @backstage/reviewers @backstage-service -/.changeset/cost-insights-* @backstage/reviewers @backstage/silver-lining -/.changeset @backstage/reviewers @backstage/techdocs-core -/cypress/src/integration/plugins/techdocs.spec.ts @backstage/reviewers @backstage/techdocs-core -/docs/assets/search @backstage/reviewers @backstage/techdocs-core -/docs/features/search @backstage/reviewers @backstage/techdocs-core -/docs/features/techdocs @backstage/reviewers @backstage/techdocs-core -/docs/plugins/integrating-search-into-plugins.md @backstage/reviewers @backstage/techdocs-core -/packages/techdocs-cli @backstage/reviewers @backstage/techdocs-core -/packages/techdocs-cli-embedded-app @backstage/reviewers @backstage/techdocs-core -/plugins/adr @backstage/reviewers @kuangp -/plugins/adr-* @backstage/reviewers @kuangp -/plugins/allure @backstage/reviewers @deepak-bhardwaj-ps -/plugins/apache-airflow @backstage/reviewers @cmpadden -/plugins/api-docs @backstage/reviewers @backstage/sda-se-reviewers -/plugins/azure-devops @backstage/reviewers @marleypowell @awanlin -/plugins/azure-devops-backend @backstage/reviewers @marleypowell @awanlin -/plugins/azure-devops-common @backstage/reviewers @marleypowell @awanlin -/plugins/bitbucket-cloud-common @backstage/reviewers @pjungermann -/plugins/bitrise @backstage/reviewers @backstage/sda-se-reviewers -/plugins/catalog @backstage/reviewers @backstage/catalog-core -/plugins/catalog-* @backstage/reviewers @backstage/catalog-core -/plugins/catalog-backend-module-aws @backstage/reviewers @backstage/catalog-core @pjungermann -/plugins/catalog-backend-module-bitbucket-cloud @backstage/reviewers @backstage/catalog-core @pjungermann -/plugins/catalog-backend-module-msgraph @backstage/reviewers @backstage/catalog-core @pjungermann -/plugins/catalog-graph @backstage/reviewers @backstage/catalog-core @backstage/sda-se-reviewers -/plugins/circleci @backstage/reviewers @adamdmharvey -/plugins/cloudbuild @backstage/reviewers @trivago/ebarrios -/plugins/code-coverage @backstage/reviewers @alde @nissayeva -/plugins/code-coverage-backend @backstage/reviewers @alde @nissayeva -/plugins/cost-insights @backstage/reviewers @backstage/silver-lining -/plugins/cost-insights-* @backstage/reviewers @backstage/silver-lining -/plugins/events-backend @backstage/reviewers @pjungermann -/plugins/events-backend-module-aws-sqs @backstage/reviewers @pjungermann -/plugins/events-backend-module-azure @backstage/reviewers @pjungermann -/plugins/events-backend-module-bitbucket-cloud @backstage/reviewers @pjungermann -/plugins/events-backend-module-gerrit @backstage/reviewers @pjungermann -/plugins/events-backend-module-github @backstage/reviewers @pjungermann -/plugins/events-backend-module-gitlab @backstage/reviewers @pjungermann -/plugins/events-backend-test-utils @backstage/reviewers @pjungermann -/plugins/events-node @backstage/reviewers @pjungermann -/plugins/explore @backstage/reviewers @backstage/sda-se-reviewers -/plugins/explore-react @backstage/reviewers @backstage/sda-se-reviewers -/plugins/fossa @backstage/reviewers @backstage/sda-se-reviewers -/plugins/gcalendar @backstage/reviewers @szubster @ptychu @kielosz @alexrybch -/plugins/git-release-manager @backstage/reviewers @erikengervall -/plugins/home @backstage/reviewers @backstage/techdocs-core -/plugins/ilert @backstage/reviewers @yacut -/plugins/jenkins @backstage/reviewers @timja -/plugins/jenkins-backend @backstage/reviewers @timja -/plugins/kafka @backstage/reviewers @nirga @andrewthauer -/plugins/kafka-backend @backstage/reviewers @nirga @andrewthauer -/plugins/kubernetes @backstage/reviewers @backstage/warpspeed -/plugins/kubernetes-* @backstage/reviewers @backstage/warpspeed -/plugins/newrelic-dashboard @backstage/reviewers @mufaddal7 -/plugins/playlist @backstage/reviewers @kuangp -/plugins/playlist-* @backstage/reviewers @kuangp -/plugins/rollbar @backstage/reviewers @andrewthauer -/plugins/rollbar-backend @backstage/reviewers @andrewthauer -/plugins/scaffolder-backend-module-rails @backstage/reviewers @angeliski -/plugins/scaffolder-backend-module-yeoman @backstage/reviewers @pawelmitka -/plugins/search @backstage/reviewers @backstage/techdocs-core -/plugins/search-* @backstage/reviewers @backstage/techdocs-core -/plugins/sonarqube @backstage/reviewers @backstage/sda-se-reviewers -/plugins/stack-overflow @backstage/reviewers @backstage/techdocs-core -/plugins/stack-overflow-backend @backstage/reviewers @backstage/techdocs-core -/plugins/techdocs @backstage/reviewers @backstage/techdocs-core -/plugins/techdocs-* @backstage/reviewers @backstage/techdocs-core -/plugins/user-settings-backend @backstage/reviewers @backstage/sda-se-reviewers -/tech-insights-backend @backstage/reviewers @xantier @iain-b -/tech-insights-backend-module-jsonfc @backstage/reviewers @xantier @iain-b -/tech-insights-tech-insights-common @backstage/reviewers @xantier @iain-b -/tech-insights-tech-insights-node @backstage/reviewers @xantier @iain-b +* @backstage/maintainers +yarn.lock @backstage/maintainers @backstage-service +*/yarn.lock @backstage/maintainers @backstage-service +/.changeset/*.md +/cypress/src/integration/plugins/techdocs.spec.ts @backstage/techdocs-core +/docs/assets/search @backstage/techdocs-core +/docs/features/search @backstage/techdocs-core +/docs/features/techdocs @backstage/techdocs-core +/docs/plugins/integrating-search-into-plugins.md @backstage/techdocs-core +/packages/techdocs-cli @backstage/techdocs-core +/packages/techdocs-cli-embedded-app @backstage/techdocs-core +/plugins/adr @backstage/maintainers @kuangp +/plugins/adr-* @backstage/maintainers @kuangp +/plugins/allure @backstage/maintainers @deepak-bhardwaj-ps +/plugins/apache-airflow @backstage/maintainers @cmpadden +/plugins/api-docs @backstage/maintainers @backstage/sda-se-reviewers +/plugins/azure-devops @backstage/maintainers @marleypowell @awanlin +/plugins/azure-devops-backend @backstage/maintainers @marleypowell @awanlin +/plugins/azure-devops-common @backstage/maintainers @marleypowell @awanlin +/plugins/bitbucket-cloud-common @backstage/maintainers @pjungermann +/plugins/bitrise @backstage/maintainers @backstage/sda-se-reviewers +/plugins/catalog @backstage/maintainers @backstage/catalog-core +/plugins/catalog-* @backstage/maintainers @backstage/catalog-core +/plugins/catalog-backend-module-aws @backstage/maintainers @backstage/catalog-core @pjungermann +/plugins/catalog-backend-module-bitbucket-cloud @backstage/maintainers @backstage/catalog-core @pjungermann +/plugins/catalog-backend-module-msgraph @backstage/maintainers @backstage/catalog-core @pjungermann +/plugins/catalog-graph @backstage/maintainers @backstage/catalog-core @backstage/sda-se-reviewers +/plugins/circleci @backstage/maintainers @adamdmharvey +/plugins/cloudbuild @backstage/maintainers @trivago/ebarrios +/plugins/code-coverage @backstage/maintainers @alde @nissayeva +/plugins/code-coverage-backend @backstage/maintainers @alde @nissayeva +/plugins/cost-insights @backstage/maintainers @backstage/silver-lining +/plugins/cost-insights-* @backstage/maintainers @backstage/silver-lining +/plugins/entity-feedback @backstage/maintainers @kuangp +/plugins/entity-feedback-* @backstage/maintainers @kuangp +/plugins/events-backend @backstage/maintainers @pjungermann +/plugins/events-backend-module-aws-sqs @backstage/maintainers @pjungermann +/plugins/events-backend-module-azure @backstage/maintainers @pjungermann +/plugins/events-backend-module-bitbucket-cloud @backstage/maintainers @pjungermann +/plugins/events-backend-module-gerrit @backstage/maintainers @pjungermann +/plugins/events-backend-module-github @backstage/maintainers @pjungermann +/plugins/events-backend-module-gitlab @backstage/maintainers @pjungermann +/plugins/events-backend-test-utils @backstage/maintainers @pjungermann +/plugins/events-node @backstage/maintainers @pjungermann +/plugins/explore @backstage/maintainers @backstage/sda-se-reviewers +/plugins/explore-react @backstage/maintainers @backstage/sda-se-reviewers +/plugins/fossa @backstage/maintainers @backstage/sda-se-reviewers +/plugins/gcalendar @backstage/maintainers @szubster @ptychu @kielosz @alexrybch +/plugins/git-release-manager @backstage/maintainers @erikengervall +/plugins/home @backstage/techdocs-core +/plugins/ilert @backstage/maintainers @yacut +/plugins/jenkins @backstage/maintainers @timja +/plugins/jenkins-backend @backstage/maintainers @timja +/plugins/kafka @backstage/maintainers @nirga @andrewthauer +/plugins/kafka-backend @backstage/maintainers @nirga @andrewthauer +/plugins/kubernetes @backstage/maintainers @backstage/warpspeed +/plugins/kubernetes-* @backstage/maintainers @backstage/warpspeed +/plugins/microsoft-calendar @backstage/maintainers @abhay-soni-developer +/plugins/newrelic-dashboard @backstage/maintainers @mufaddal7 +/plugins/playlist @backstage/maintainers @kuangp +/plugins/playlist-* @backstage/maintainers @kuangp +/plugins/rollbar @backstage/maintainers @andrewthauer +/plugins/rollbar-backend @backstage/maintainers @andrewthauer +/plugins/scaffolder-backend-module-rails @backstage/maintainers @angeliski +/plugins/scaffolder-backend-module-yeoman @backstage/maintainers @pawelmitka +/plugins/search @backstage/techdocs-core +/plugins/search-* @backstage/techdocs-core +/plugins/sonarqube @backstage/maintainers @backstage/sda-se-reviewers +/plugins/stack-overflow @backstage/techdocs-core +/plugins/stack-overflow-backend @backstage/techdocs-core +/plugins/techdocs @backstage/techdocs-core +/plugins/techdocs-* @backstage/techdocs-core +/plugins/user-settings-backend @backstage/maintainers @backstage/sda-se-reviewers +/tech-insights-backend @backstage/maintainers @xantier @iain-b +/tech-insights-backend-module-jsonfc @backstage/maintainers @xantier @iain-b +/tech-insights-tech-insights-common @backstage/maintainers @xantier @iain-b +/tech-insights-tech-insights-node @backstage/maintainers @xantier @iain-b diff --git a/.github/vale/Vocab/Backstage/accept.txt b/.github/vale/Vocab/Backstage/accept.txt index f24b701d36..08e325c38b 100644 --- a/.github/vale/Vocab/Backstage/accept.txt +++ b/.github/vale/Vocab/Backstage/accept.txt @@ -9,6 +9,7 @@ Airbrake Airbrakes Alaria Alef +allowlisted Anddddd Apdex api @@ -23,6 +24,7 @@ Avro backend's backported backporting +BEPs Bigtable Billett bitbucket @@ -31,6 +33,7 @@ Blackbox bool boolean Brex +bugfixes builtins callout CDNs @@ -95,6 +98,7 @@ dockerfiles Dockerize dockerode Docusaurus +Dominik DOMPurify don'ts dynatrace @@ -119,6 +123,7 @@ Firekube Firestore Fiverr Francesco +Frontside gerrit Gerrit gitbeaker @@ -138,6 +143,7 @@ Hackathons haproxy hardcoded Helidon +Henneke Heroku hoc horizontalpodautoscalers @@ -167,12 +173,14 @@ jsonnet jsx JWTs Kaewkasi +Kaswell Keyv Knex KPIs +Kuang kubectl kubernetes -kubernetes +Lainfiesta ldap learnings Leasot @@ -336,6 +344,7 @@ subfolders subheader subheaders subkey +subpath subroutes subtree superfences @@ -412,10 +421,3 @@ zod Zolotusky zoomable zsh -Lainfiesta -allowlisted -Dominik -Henneke -Kuang -Frontside -Kaswell \ No newline at end of file diff --git a/.github/workflows/automate_changeset_feedback.yml b/.github/workflows/automate_changeset_feedback.yml index e18aa9d428..d1dcd97644 100644 --- a/.github/workflows/automate_changeset_feedback.yml +++ b/.github/workflows/automate_changeset_feedback.yml @@ -28,13 +28,13 @@ jobs: ref: 'refs/pull/${{ github.event.pull_request.number }}/merge' - name: fetch base run: git fetch --depth 1 origin ${{ github.base_ref }} - - uses: backstage/actions/changeset-feedback@v0.5.13 + - uses: backstage/actions/changeset-feedback@v0.6.2 name: Generate feedback with: - diffRef: 'origin/master' + diff-ref: 'origin/master' marker: issue-number: ${{ github.event.pull_request.number }} - botUsername: backstage-goalie[bot] + bot-username: backstage-goalie[bot] app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 10db591f4c..18af36c8c2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -63,7 +63,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -181,7 +181,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/cron.yml b/.github/workflows/cron.yml index dc221c1a1e..be27a732d6 100644 --- a/.github/workflows/cron.yml +++ b/.github/workflows/cron.yml @@ -8,7 +8,7 @@ jobs: cron: runs-on: ubuntu-latest steps: - - uses: backstage/actions/cron@v0.5.13 + - uses: backstage/actions/cron@v0.6.2 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/deploy_docker-image.yml b/.github/workflows/deploy_docker-image.yml index d8470d4124..d45cc557d5 100644 --- a/.github/workflows/deploy_docker-image.yml +++ b/.github/workflows/deploy_docker-image.yml @@ -26,7 +26,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_nightly.yml b/.github/workflows/deploy_nightly.yml index 5f337ace3e..49907042b5 100644 --- a/.github/workflows/deploy_nightly.yml +++ b/.github/workflows/deploy_nightly.yml @@ -26,7 +26,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/deploy_packages.yml b/.github/workflows/deploy_packages.yml index c05b850883..058f4f6089 100644 --- a/.github/workflows/deploy_packages.yml +++ b/.github/workflows/deploy_packages.yml @@ -68,7 +68,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} @@ -145,7 +145,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/issue.yaml b/.github/workflows/issue.yaml index d16d0b55d7..e64f919bdb 100644 --- a/.github/workflows/issue.yaml +++ b/.github/workflows/issue.yaml @@ -10,4 +10,4 @@ jobs: if: github.repository == 'backstage/backstage' steps: - name: Issue sync - uses: backstage/actions/issue-sync@v0.5.13 + uses: backstage/actions/issue-sync@v0.6.2 diff --git a/.github/workflows/pr-review-comment.yaml b/.github/workflows/pr-review-comment.yaml index 11b590809a..52db29c699 100644 --- a/.github/workflows/pr-review-comment.yaml +++ b/.github/workflows/pr-review-comment.yaml @@ -35,7 +35,7 @@ jobs: const prNumber = artifact.name.slice('pr_number-'.length) core.setOutput('pr-number', prNumber); - - uses: backstage/actions/re-review@v0.5.13 + - uses: backstage/actions/re-review@v0.6.2 with: app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index acbd11aece..db7d141abd 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -18,12 +18,12 @@ jobs: if: github.repository == 'backstage/backstage' && ( github.event.pull_request || github.event.issue.pull_request ) steps: - name: PR sync - uses: backstage/actions/pr-sync@v0.5.13 + uses: backstage/actions/pr-sync@v0.6.2 with: github-token: ${{ secrets.GH_SERVICE_ACCOUNT_TOKEN }} app-id: ${{ secrets.BACKSTAGE_GOALIE_APPLICATION_ID }} private-key: ${{ secrets.BACKSTAGE_GOALIE_PRIVATE_KEY }} installation-id: ${{ secrets.BACKSTAGE_GOALIE_INSTALLATION_ID }} project-id: PVT_kwDOBFKqdc02LQ - excluded-users: ${{ secrets.OOO_USERS }} + auto-assign: false owning-teams: '@backstage/techdocs-core' diff --git a/.github/workflows/sync_code-formatting.yml b/.github/workflows/sync_code-formatting.yml index 19cc5f8045..7b0cbbbb01 100644 --- a/.github/workflows/sync_code-formatting.yml +++ b/.github/workflows/sync_code-formatting.yml @@ -20,7 +20,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/sync_snyk-github-issues.yml b/.github/workflows/sync_snyk-github-issues.yml index 9dcc9f63ef..a89b7c8e65 100644 --- a/.github/workflows/sync_snyk-github-issues.yml +++ b/.github/workflows/sync_snyk-github-issues.yml @@ -23,7 +23,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/uffizzi-build.yml b/.github/workflows/uffizzi-build.yml index 5c8e179958..30a3489264 100644 --- a/.github/workflows/uffizzi-build.yml +++ b/.github/workflows/uffizzi-build.yml @@ -27,7 +27,7 @@ jobs: registry-url: https://registry.npmjs.org/ - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: linux-v16 diff --git a/.github/workflows/verify_e2e-linux.yml b/.github/workflows/verify_e2e-linux.yml index f687e4f5a4..4fda0749c0 100644 --- a/.github/workflows/verify_e2e-linux.yml +++ b/.github/workflows/verify_e2e-linux.yml @@ -52,7 +52,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_e2e-windows.yml b/.github/workflows/verify_e2e-windows.yml index 8a49b8fe31..145eefbd26 100644 --- a/.github/workflows/verify_e2e-windows.yml +++ b/.github/workflows/verify_e2e-windows.yml @@ -49,7 +49,7 @@ jobs: with: node-version: ${{ matrix.node-version }} - name: Add msbuild to PATH - uses: microsoft/setup-msbuild@v1.1.3 + uses: microsoft/setup-msbuild@v1.3.1 - name: setup chrome uses: browser-actions/setup-chrome@latest - name: yarn install diff --git a/.github/workflows/verify_kubernetes.yml b/.github/workflows/verify_kubernetes.yml index 7fbcf9fa3f..83280f8e85 100644 --- a/.github/workflows/verify_kubernetes.yml +++ b/.github/workflows/verify_kubernetes.yml @@ -28,7 +28,7 @@ jobs: registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} diff --git a/.github/workflows/verify_storybook.yml b/.github/workflows/verify_storybook.yml index 0e63393af8..8cde5b6e9b 100644 --- a/.github/workflows/verify_storybook.yml +++ b/.github/workflows/verify_storybook.yml @@ -35,7 +35,7 @@ jobs: node-version: ${{ matrix.node-version }} registry-url: https://registry.npmjs.org/ # Needed for auth - name: yarn install - uses: backstage/actions/yarn-install@v0.5.13 + uses: backstage/actions/yarn-install@v0.6.2 with: cache-prefix: ${{ runner.os }}-v${{ matrix.node-version }} - name: storybook yarn install diff --git a/ADOPTERS.md b/ADOPTERS.md index 965cf06727..4329a00664 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -186,7 +186,7 @@ _You can do this by using the [Adopter form](https://info.backstage.spotify.com/ | [Cho Tot](https://www.chotot.com) | [Chotot Team](mailto:sre@chotot.vn) | Internal developer portal, service catalog with CI/CD tools. | | [William Hill](https://www.williamhillgroup.com/) | [Pat Mills](mailto:pat.mills@williamhill.com), [Nathan Flynn](mailto:nflynn@williamhill.co.uk), and [Nishkarsh Raj](mailto:nishkarsh.raj@williamhill.co.uk) | William Hill are leveraging Backstage to build our Engineering Portal. Our mission is to centralize the software catalog inventory to enable service discoverability, reduce the onboarding time for new Engineers, provide a single pane of glass to accelerate Developer Productivity and Save Engineers time. Our aspiration is to create an InnerSource community focussed on organization-wide patterns that are re-usable and can be self-served with the Scaffolder. | | [Vodafone NewZealand Limited](https://vodafone.co.nz) | [Ankit Gupta](mailto:ankit.gupta@vodafone.nz), [DevOps COE](mailto:devopstooling@vodafone.nz) | Vodafone NZ are leveraging Backstage to build centralised and self service Engineering Portal. Our mission is to standardised Pipeline templates across the Engineering teams, One shop stop to create the pipelines and repository with a template approach which reduces creation part from days to minutes and no wait time for developers. A unified view for Azure DevOps pipeline, Azure Repo pull requests, Deployment status from Azure RedHat Openshift-ArgoCD and SonarQube Security and code quality scans report on a single pan to provide a streamlined view for all microservices across the app stack. | -| [Coamo](http://www.coamo.com.br) | [@holiiveira](https://github.com/holiiveira), [@gpxlnx](https://github.com/gpxlnx) | We're starting to use it as the main tool of a DevOps platform. Our goal is to provide software templates, centralize our software catalog enabling efficient service discovery, and make it easy to manage the entire software ecosystem in one place. | +| [Coamo](http://www.coamo.com.br) | [Henrique Oliveira](https://www.linkedin.com/in/oliveirah/), [Guilherme Xavier](https://br.linkedin.com/in/guilhermexavierlnx) | We're starting to use it as the main tool of a DevOps platform. Our goal is to provide software templates, centralize our software catalog enabling efficient service discovery, and make it easy to manage the entire software ecosystem in one place. | | [Exclaimer](https://www.exclaimer.com/) | [Marley Powell](mailto:marley.powell@exclaimer.com) | Developer portal, service catalog and documentation | | [Edge Consulting](https://egde.no/) | [Øyvind Brekkhus Sandåker](mailto:oyvindbs@egde.no) | Using it as a Software Catalog to get an overview of our systems, for ourselves and our customers. | | [CRV](https://crv4all.com) | [Pepijn Schildkamp](mailto:pepijn.schildkamp@crv4all.com) | diff --git a/cypress/.eslintrc.js b/cypress/.eslintrc.js index a77de4a629..07f264452d 100644 --- a/cypress/.eslintrc.js +++ b/cypress/.eslintrc.js @@ -2,15 +2,6 @@ module.exports = { extends: [require.resolve('@backstage/cli/config/eslint.backend')], rules: { 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], 'jest/valid-expect': 'off', 'jest/expect-expect': 'off', 'no-restricted-syntax': 'off', diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts new file mode 100644 index 0000000000..fd4db77d61 --- /dev/null +++ b/cypress/support/commands.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// *********************************************** +// This example commands.ts shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add('login', (email, password) => { ... }) +// +// +// -- This is a child command -- +// Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) +// +// +// -- This is a dual command -- +// Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) +// +// +// -- This will overwrite an existing command -- +// Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) +// +// declare global { +// namespace Cypress { +// interface Chainable { +// login(email: string, password: string): Chainable +// drag(subject: string, options?: Partial): Chainable +// dismiss(subject: string, options?: Partial): Chainable +// visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable +// } +// } +// } diff --git a/cypress/support/e2e.ts b/cypress/support/e2e.ts new file mode 100644 index 0000000000..c71790471e --- /dev/null +++ b/cypress/support/e2e.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// This example support/e2e.ts is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import './commands'; + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/docs/assets/auth/gcp-iap-jwt-audience-code-popup.png b/docs/assets/auth/gcp-iap-jwt-audience-code-popup.png new file mode 100644 index 0000000000..72b69b3b01 Binary files /dev/null and b/docs/assets/auth/gcp-iap-jwt-audience-code-popup.png differ diff --git a/docs/assets/permissions/permission-framework-overview.drawio.svg b/docs/assets/permissions/permission-framework-overview.drawio.svg index c7e628f8a6..3740384748 100644 --- a/docs/assets/permissions/permission-framework-overview.drawio.svg +++ b/docs/assets/permissions/permission-framework-overview.drawio.svg @@ -1,4 +1,4 @@ -
    User
    User
    Plugin
    Plugin
    Permission Response
    Permission Response
    Permission Framework
    Permission Framework
    Action
    (Read request)
    Action...
    Resources
    Resources
    Resource read permission
    Resource read...
    Permission Policy
    Permission Policy
    Permission Request
    Permission Request
    Provided by Backstage
    Provided by Backstage
    Configured by integrators
    Configured by integrators
    Configured by plugin authors
    Configured by plugin authors
    Permission Backend
    Permission hooks
    Integration helpers
    ...
    Permission Backend...
    1
    1
    2
    2
    3
    3
    4
    4
    Text is not SVG - cannot display
    \ No newline at end of file +
    User
    User
    Plugin
    Plugin
    Permission Response
    Permission Response
    Permission Framework
    Permission Framework
    Action
    (Read request)
    Action...
    Resources
    Resources
    Resource read permission
    Resource read...
    Permission Policy
    Permission Policy
    Permission Request
    Permission Request
    Provided by Backstage
    Provided by Backstage
    Configured by integrators
    Configured by integrators
    Configured by plugin authors
    Configured by plugin authors
    Permission Backend
    Permission hooks
    Integration helpers
    ...
    Permission Backend...
    1
    1
    2
    2
    3
    3
    4
    4
    5
    5
    User gets a response
    User gets a response
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/docs/assets/software-catalog/software-model-entities.drawio.svg b/docs/assets/software-catalog/software-model-entities.drawio.svg old mode 100755 new mode 100644 index e14cc93e62..d087583245 --- a/docs/assets/software-catalog/software-model-entities.drawio.svg +++ b/docs/assets/software-catalog/software-model-entities.drawio.svg @@ -1,4 +1,4 @@ -
    Domain
    Domain
    partOf
    partOf
    System
    System
    dependsOn
    dependsOn
    partOf
    partOf
    Resource
    (e.g. SQL Database, S3 bucket, ...)
    Resource...
    consumesAPI
    consumesAPI
    API
    (e.g. OpenAPI, gRPC API, Avro, Dataset, ...)
    API...
    providesAPI
    providesAPI
    partOf
    partOf
    Component
    (e.g. backend service, data pipeline ...)
    Component...
    partOf
    partOf
    dependsOn
    dependsOn
    Text is not SVG - cannot display
    \ No newline at end of file +
    Domain
    (e.g.  domain
    models, metrics, KPIs, business purpose)
    Domain...
    partOf
    partOf
    System
    Collection of entities that cooperate to perform some function
    System...
    dependsOn
    dependsOn
    partOf
    partOf
    Resource
    (e.g. SQL Database, S3 bucket, ...)
    Resource...
    consumesAPI
    consumesAPI
    API
    (e.g. OpenAPI, gRPC API, Avro, Dataset, ...)
    API...
    providesAPI
    providesAPI
    partOf
    partOf
    Component
    (e.g. backend service, data pipeline ...)
    Component...
    partOf
    partOf
    dependsOn
    dependsOn
    Kind: Group
    Kind: Group
    Kind: User
    Kind: User
    OwnerOf
    OwnerOf
    OwnedBy
    OwnedBy
    Service
    Service
    website
    website
    website
    website
    Types
    Types
    service
    service
    website
    website
    library
    library
    asyncapi
    asyncapi
    graphql
    graphql
    grpc
    grpc
    Types
    Types
    openapi
    openapi
    business-unit
    business-unit
    product-area
    product-area
    root
    root
    Types
    Types
    team
    team
    s3-bucket
    s3-bucket
    cluster
    cluster
    Types
    Types
    database
    database
    Kind: Template
    Parameters rendered in the
    frontend and the steps executed in  the scaffolding process
    Kind: Template...
    Kind: Location
    a marker that references other places to look for catalog data
    Kind: Location...
    memberOf
    memberOf
    hasMember
    hasMember
    parentOf
    parentOf
    childOf
    childOf
    Text is not SVG - cannot display
    \ No newline at end of file diff --git a/docs/auth/bitbucket/provider.md b/docs/auth/bitbucket/provider.md index 36859dab45..b03201893c 100644 --- a/docs/auth/bitbucket/provider.md +++ b/docs/auth/bitbucket/provider.md @@ -61,7 +61,7 @@ how this is done. Note that for the Bitbucket provider, you'll want to use factory. The `@backstage/plugin-auth-backend` plugin also comes with two built-in -resolves that can be used if desired. The first one is the +resolvers that can be used if desired. The first one is the `bitbucketUsernameSignInResolver`, which identifies users by matching their Bitbucket username to `bitbucket.org/username` annotations of `User` entities in the catalog. Note that you must populate your catalog with matching entities or diff --git a/docs/auth/bitbucketServer/provider.md b/docs/auth/bitbucketServer/provider.md new file mode 100644 index 0000000000..96c7db263e --- /dev/null +++ b/docs/auth/bitbucketServer/provider.md @@ -0,0 +1,52 @@ +--- +id: provider +title: Bitbucket Server Authentication Provider +sidebar_label: Bitbucket Server +description: Adding Bitbucket Server OAuth as an authentication provider in Backstage +--- + +The Backstage `core-plugin-api` package comes with a Bitbucket Server authentication provider that can authenticate +users using Bitbucket Server. This does **NOT** work with Bitbucket Cloud. + +## Create an Application Link in Bitbucket Server + +To add Bitbucket Server authentication, you must create an outgoing application link. Follow the steps described in +the [Bitbucket Server documentation](https://confluence.atlassian.com/bitbucketserver/configure-an-outgoing-link-1108483656.html) +to create one. + +## Configuration + +The provider configuration can then be added to your `app-config.yaml` under the root `auth` configuration: + +```yaml +auth: + environment: development + providers: + bitbucketServer: + development: + host: bitbucket.org + clientId: ${AUTH_BITBUCKET_SERVER_CLIENT_ID} + clientSecret: ${AUTH_BITBUCKET_SERVER_CLIENT_SECRET} +``` + +The Bitbucket Server provider is a structure with two configuration keys: + +- `clientId`: The client ID that was generated by Bitbucket, e.g. `b0f868455c15dcdff5c5fb5d173ae684`. +- `clientSecret`: The client secret tied to the generated client ID. + +## Adding the provider to the Backstage frontend + +To add the provider to the frontend, add the `bitbucketServerAuthApi` 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). + +## Using Bitbucket Server for sign-in + +In order to use the Bitbucket Server provider for sign-in, you must configure it with a `signIn.resolver`. See +the [Sign-In Resolver documentation](../identity-resolver.md) for more details on how this is done. Note that for the +Bitbucket Server provider, you'll want to use `bitbucketServer` as the provider ID, +and `providers.bitbucketServer.create` for the provider factory. + +The `@backstage/plugin-auth-backend` plugin also comes with a built-in resolver that can be used if desired. +The `emailMatchingUserEntityProfileEmail` identifies users by matching their Bitbucket Server email address to the email +address of `User` entities in the catalog. Note that you must populate your catalog with matching entities or users will +not be able to sign in with this resolver. diff --git a/docs/auth/google/gcp-iap-auth.md b/docs/auth/google/gcp-iap-auth.md index f9a78bbc71..36eece85c5 100644 --- a/docs/auth/google/gcp-iap-auth.md +++ b/docs/auth/google/gcp-iap-auth.md @@ -29,7 +29,9 @@ auth: jwtHeader: x-custom-header # Optional: Only if you are using a custom header for the IAP JWT ``` -You can find the project number and service ID in the Google Cloud Console. +The full `audience` value can be obtained by visiting your [Identity-Aware Proxy Google Cloud console](https://console.cloud.google.com/security/iap), selecting your project, finding your Backend Service to proxy, clicking the 3 vertical dots then "Get JWT Audience Code", and copying from the resulting popup, which will look similar to the following: + +![Identity-Aware Proxy JWT Audience Code popup](../../assets/auth/gcp-iap-jwt-audience-code-popup.png) This config section must be in place for the provider to load at all. Now let's add the provider itself. diff --git a/docs/auth/index.md b/docs/auth/index.md index fa185c0c23..8e15830aa8 100644 --- a/docs/auth/index.md +++ b/docs/auth/index.md @@ -18,6 +18,7 @@ Backstage comes with many common authentication providers in the core library: - [Auth0](auth0/provider.md) - [Azure](microsoft/provider.md) - [Bitbucket](bitbucket/provider.md) +- [Bitbucket Server](bitbucketServer/provider.md) - [Cloudflare Access](cloudflare/access.md) - [GitHub](github/provider.md) - [GitLab](gitlab/provider.md) diff --git a/docs/backend-system/architecture/01-index.md b/docs/backend-system/architecture/01-index.md index 88ba3262df..76953f7009 100644 --- a/docs/backend-system/architecture/01-index.md +++ b/docs/backend-system/architecture/01-index.md @@ -6,6 +6,8 @@ sidebar_label: System Architecture description: The structure and architecture of the new Backend System and its component parts --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + ## Building Blocks This section introduces the high-level building blocks upon which this new diff --git a/docs/backend-system/architecture/02-backends.md b/docs/backend-system/architecture/02-backends.md index 69b74e7dd3..b6079d71b4 100644 --- a/docs/backend-system/architecture/02-backends.md +++ b/docs/backend-system/architecture/02-backends.md @@ -6,6 +6,8 @@ sidebar_label: Backend description: Backend instances --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + ## The Backend Instance This is the main entry point for creating a backend. It does not have any functionality in and of itself, but is simply responsible for wiring things together. diff --git a/docs/backend-system/architecture/03-services.md b/docs/backend-system/architecture/03-services.md index 96b82d7e95..8a86d90efb 100644 --- a/docs/backend-system/architecture/03-services.md +++ b/docs/backend-system/architecture/03-services.md @@ -6,6 +6,8 @@ sidebar_label: Services description: Services for backend plugins --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + Backend services provide shared functionality available to all backend plugins and modules. They are made available through service references that embed a type that represents the service interface, similar to how [Utility APIs](../../api/utility-apis.md) work in the Backstage frontend system. To use a service in your plugin or module you request an implementation of that service using the service reference. The system surrounding services exists to provide a level of indirection between the service interfaces and their implementation. It is an implementation of dependency injection, where each backend instance is the dependency injection container. The implementation for each service is provided by a service factory, which encapsulates the logic for how each service instance is created. @@ -59,7 +61,7 @@ class DefaultFooService implements FooService { } } -export const fooFactory = createServiceFactory({ +export const fooServiceFactory = createServiceFactory({ service: fooServiceRef, deps: { bar: barServiceRef }, factory({ bar }) { @@ -73,7 +75,7 @@ To create a service factory we need to provide a reference to the `service` for If you need the creation of the service instance to be asynchronous, you can make the `factory` function async. For example: ```ts -export const fooFactory = createServiceFactory({ +export const fooServiceFactory = createServiceFactory({ service: fooServiceRef, deps: {}, async factory() { @@ -92,18 +94,18 @@ To install a service factory in a backend instance, we pass it in through the `s ```ts const backend = createBackend({ - services: [fooFactory()], + services: [fooServiceFactory()], }); ``` -Note that we call `fooFactory` to create the service factory instance. This is because `createServiceFactory` always returns a factory function that creates the actual service factory. This is done to always allow for options to be added to the service factory in the future, without breaking existing code. To add options to your service factory, you wrap the object passed to `createServiceFactory` in a callback that accepts the desired options. For example: +Note that we call `fooServiceFactory` to create the service factory instance. This is because `createServiceFactory` always returns a factory function that creates the actual service factory. This is done to always allow for options to be added to the service factory in the future, without breaking existing code. To add options to your service factory, you wrap the object passed to `createServiceFactory` in a callback that accepts the desired options. For example: ```ts export interface FooFactoryOptions { mode: 'eager' | 'lazy'; } -export const fooFactory = createServiceFactory( +export const fooServiceFactory = createServiceFactory( (options?: FooFactoryOptions) => ({ service: fooServiceRef, deps: { bar: barServiceRef }, @@ -118,7 +120,7 @@ This lets us use the options to customize the factory implementation in any way ```ts const backend = createBackend({ - services: [fooFactory({ mode: 'eager' })], + services: [fooServiceFactory({ mode: 'eager' })], }); ``` @@ -162,7 +164,7 @@ Plugin scoped services have access to a plugin metadata service, which is a spec The plugin metadata service is the base for all plugin specific customizations for services. For example, the default implementation of the plugin scoped logger service uses the plugin metadata service to attach the plugin ID as a field in all log messages: ```ts -export const loggerFactory = createServiceFactory({ +export const loggerServiceFactory = createServiceFactory({ service: coreServices.logger, deps: { rootLogger: coreServices.rootLogger, @@ -181,7 +183,7 @@ Some services may benefit from having a context that is shared across all instan The root context is defined as part of the service factory by passing the `createRootContext` option: ```ts -export const fooFactory = createServiceFactory({ +export const fooServiceFactory = createServiceFactory({ service: fooServiceRef, deps: { rootLogger: coreServices.rootLogger, bar: barServiceRef }, createRootContext({ rootLogger }) { diff --git a/docs/backend-system/architecture/04-plugins.md b/docs/backend-system/architecture/04-plugins.md index f535268b2a..04271b17da 100644 --- a/docs/backend-system/architecture/04-plugins.md +++ b/docs/backend-system/architecture/04-plugins.md @@ -6,6 +6,8 @@ sidebar_label: Plugins description: Backend plugins --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + Plugins provide the actual base features of a Backstage backend. Each plugin operates completely independently of all other plugins and they only communicate with each other through network calls. This means that there is a strong degree of isolation between plugins, and that each plugin can be considered a separate microservice. While a default Backstage project has all plugins installed within a single backend, it is also possible to split this setup into multiple backends, with each backend housing one or more plugins. ## Defining a Plugin @@ -20,7 +22,7 @@ import { } from '@backstage/backend-plugin-api'; export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -51,7 +53,7 @@ export interface ExamplePluginOptions { export const examplePlugin = createBackendPlugin( (options?: ExamplePluginOptions) => ({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { diff --git a/docs/backend-system/architecture/05-extension-points.md b/docs/backend-system/architecture/05-extension-points.md index f82fde3561..6d320037ba 100644 --- a/docs/backend-system/architecture/05-extension-points.md +++ b/docs/backend-system/architecture/05-extension-points.md @@ -6,6 +6,8 @@ sidebar_label: Extension Points description: Extension points of backend plugins --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + While plugins are able to accept options for lightweight forms of customization and extension, you quickly hit a limit where you need something more powerful to allow users to extend your plugin. For this purpose, the backend system provides a mechanism for plugins to provide extension points, which can be used to expose deeper customizations for your plugin. Extension points are used by modules, which are installed in the backend adjacent to plugins. Modules are covered more in-depth in the [next section](./06-modules.md). Extension points are quite similar to services, in that they both encapsulate an interface in a reference object. The key difference is that extension points are registered and provided by plugins themselves, and do not have any factory associated with them. Extension points for a given plugin are also only accessible to modules that extend that same plugin. @@ -42,7 +44,7 @@ class ActionsExtension implements ScaffolderActionsExtensionPoint { export const scaffolderPlugin = createBackendPlugin( { - id: 'scaffolder', + pluginId: 'scaffolder', register(env) { const actionsExtensions = new ActionsExtension(); env.registerExtensionPoint( diff --git a/docs/backend-system/architecture/06-modules.md b/docs/backend-system/architecture/06-modules.md index 83428c7a26..53aae9e38d 100644 --- a/docs/backend-system/architecture/06-modules.md +++ b/docs/backend-system/architecture/06-modules.md @@ -6,6 +6,8 @@ sidebar_label: Modules description: Modules for backend plugins --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + Backend modules are used to extend [plugins](./04-plugins.md) with additional features or change existing behavior. They must always be installed in the same backend instance as the plugin that they extend, and may only extend a single plugin. Modules interact with their target plugin using the [extension points](./05-extension-points.md) registered by the plugin, while also being able to depend on the [services](./03-services.md) of that plugin. Both modules and plugins register an `init` method that is called during startup. In order to ensure that modules have registered all their extensions before the plugin starts up, all modules for each plugin are completely initialized before the plugin itself is initialized. In practice this means that all promises returned by each `init` method of the modules need to resolve before the plugin `init` method is called. This also means that it is not possible to further interact with the extension points once the `init` method has resolved. diff --git a/docs/backend-system/architecture/07-naming-patterns.md b/docs/backend-system/architecture/07-naming-patterns.md index 6bd7c412c3..b541cc09ba 100644 --- a/docs/backend-system/architecture/07-naming-patterns.md +++ b/docs/backend-system/architecture/07-naming-patterns.md @@ -6,6 +6,8 @@ sidebar_label: Naming Patterns description: Naming patterns in the backend system --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + These are the naming patterns to adhere to within the backend system. They help us keep exports consistent across packages and make it easier to understand the usage and intent of exports. ### Plugins @@ -19,7 +21,7 @@ Example: ```ts export const catalogPlugin = createBackendPlugin({ - id: 'catalog', + pluginId: 'catalog', ... }) ``` @@ -34,7 +36,7 @@ export const catalogPlugin = createBackendPlugin({ Example: ```ts -export const catalogModuleGithubEntityProvider = createBackendPlugin({ +export const catalogModuleGithubEntityProvider = createBackendModule({ pluginId: 'catalog', moduleId: 'githubEntityProvider', ... @@ -64,12 +66,12 @@ export const catalogProcessingExtensionPoint = createExtensionPointService` | `LoggerService`, `DatabaseService` | -| Reference | `ServiceRef` | `loggerServiceRef`, `databaseServiceRef` | -| ID | `.` | `'core.rootHttpRouter'`, `'catalog.catalogClient'` | -| Factory | `Factory` | `loggerFactory`, `databaseFactory` | +| Description | Pattern | Examples | +| ----------- | ---------------------- | -------------------------------------------------- | +| Interface | `Service` | `LoggerService`, `DatabaseService` | +| Reference | `ServiceRef` | `loggerServiceRef`, `databaseServiceRef` | +| ID | `.` | `'core.rootHttpRouter'`, `'catalog.catalogClient'` | +| Factory | `ServiceFactory` | `loggerServiceFactory`, `databaseServiceFactory` | Example: @@ -83,7 +85,7 @@ export const catalogClientServiceRef = createServiceRef({ ... }) -export const catalogClientFactory = createServiceFactory({ +export const catalogClientServiceFactory = createServiceFactory({ service: catalogClientServiceRef, ... }) diff --git a/docs/backend-system/building-backends/01-index.md b/docs/backend-system/building-backends/01-index.md index f810417ed7..65c536afdd 100644 --- a/docs/backend-system/building-backends/01-index.md +++ b/docs/backend-system/building-backends/01-index.md @@ -6,6 +6,8 @@ sidebar_label: Overview description: Building backends using the new backend system --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + > NOTE: If you have an existing backend that is not yet using the new backend > system, see [migrating](./08-migrating.md). @@ -59,11 +61,11 @@ All of these services can be replaced with your own implementations if you need For example, let's say we want to customize the core configuration service to enable remote configuration loading. That would look something like this: ```ts -import { configFactory } from '@backstage/backend-app-api'; +import { configServiceFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ - configFactory({ + configServiceFactory({ remote: { reloadIntervalSeconds: 60 }, }), ], @@ -158,11 +160,11 @@ A shared environment is defined using `createSharedEnvironment`. In this example ```ts // packages/backend-env/src/index.ts import { createSharedEnvironment } from '@backstage/backend-plugin-api'; -import { customDiscoveryFactory } from './customDiscoveryFactory'; +import { customDiscoveryServiceFactory } from './customDiscoveryServiceFactory'; export const env = createSharedEnvironment({ services: [ - customDiscoveryFactory(), // custom DiscoveryService implementation + customDiscoveryServiceFactory(), // custom DiscoveryService implementation ], }); ``` diff --git a/docs/backend-system/building-backends/08-migrating.md b/docs/backend-system/building-backends/08-migrating.md index 6e0b84b10f..567476532f 100644 --- a/docs/backend-system/building-backends/08-migrating.md +++ b/docs/backend-system/building-backends/08-migrating.md @@ -6,6 +6,8 @@ sidebar_label: Migration Guide description: How to migrate existing backends to the new backend system --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. As such, it is not considered stable, and it is not recommended to migrate production backends to the new backend system until it has a stable release.** + ## Overview This section describes how to migrate an existing Backstage backend service diff --git a/docs/backend-system/building-plugins-and-modules/01-index.md b/docs/backend-system/building-plugins-and-modules/01-index.md index 01e35036aa..18cbabfe5d 100644 --- a/docs/backend-system/building-plugins-and-modules/01-index.md +++ b/docs/backend-system/building-plugins-and-modules/01-index.md @@ -6,6 +6,8 @@ sidebar_label: Overview description: Building backend plugins and modules using the new backend system --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + > NOTE: If you have an existing backend and/or backend plugins that are not yet > using the new backend system, see [migrating](./08-migrating.md). @@ -34,7 +36,7 @@ import { import { createExampleRouter } from './router'; export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -181,7 +183,7 @@ that needs to be different across environments. import { coreServices } from '@backstage/backend-plugin-api'; export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { config: coreServices.config }, @@ -214,7 +216,7 @@ export interface ExampleOptions { export const examplePlugin = createBackendPlugin( (options?: ExampleOptions) => ({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { diff --git a/docs/backend-system/building-plugins-and-modules/02-testing.md b/docs/backend-system/building-plugins-and-modules/02-testing.md index 20bef6fec4..7f93fab45b 100644 --- a/docs/backend-system/building-plugins-and-modules/02-testing.md +++ b/docs/backend-system/building-plugins-and-modules/02-testing.md @@ -6,6 +6,8 @@ sidebar_label: Testing description: Learn how to test your backend plugins and modules --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + Utilities for testing backend plugins and modules are available in `@backstage/backend-test-utils`. This section describes those facilities. diff --git a/docs/backend-system/building-plugins-and-modules/08-migrating.md b/docs/backend-system/building-plugins-and-modules/08-migrating.md index ec003a6fec..cf5aa8106f 100644 --- a/docs/backend-system/building-plugins-and-modules/08-migrating.md +++ b/docs/backend-system/building-plugins-and-modules/08-migrating.md @@ -6,6 +6,8 @@ sidebar_label: Migration Guide description: How to migrate existing backend plugins to the new backend system --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + Migrating an existing backend plugin to the new backend system is fairly straightforward. The process is similar across the majority of plugins which just return a `Router` that is then wired up in the `index.ts` file of your backend. The primary thing that we need to do is to make sure that the dependencies that are required by the plugin are available, and then registering the router with the HTTP router service. Let's look at an example of migrating the Kubernetes backend plugin. In the existing (old) system, the kubernetes backend is structured like this: @@ -44,7 +46,7 @@ import { Router } from 'express'; import { KubernetesBuilder } from './KubernetesBuilder'; export const kubernetesPlugin = createBackendPlugin({ - id: 'kubernetes', + pluginId: 'kubernetes', register(env) { env.registerInit({ deps: { @@ -93,7 +95,7 @@ export interface KubernetesOptions { } const kubernetesPlugin = createBackendPlugin((options: KubernetesOptions) => ({ - id: 'kubernetes', + pluginId: 'kubernetes', register(env) { env.registerInit({ deps: { @@ -180,7 +182,7 @@ class ClusterSupplier implements KubernetesClusterSupplierExtensionPoint { } export const kubernetesPlugin = createBackendPlugin({ - id: 'kubernetes', + pluginId: 'kubernetes', register(env) { const extensionPoint = new ClusterSupplier(); // We register the extension point with the backend, which allows modules to diff --git a/docs/backend-system/core-services/01-index.md b/docs/backend-system/core-services/01-index.md index 4ef9d75bde..6448943e3a 100644 --- a/docs/backend-system/core-services/01-index.md +++ b/docs/backend-system/core-services/01-index.md @@ -6,6 +6,8 @@ sidebar_label: Core Services description: Core backend service APIs --- +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** + The default backend provides several [core services](https://github.com/backstage/backstage/blob/master/packages/backend-plugin-api/src/services/definitions/coreServices.ts) out of the box which includes access to configuration, logging, URL Readers, databases and more. All core services are available through the `coreServices` namespace in the `@backstage/backend-plugin-api` package. @@ -31,7 +33,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { http: coreServices.httpRouter }, @@ -57,11 +59,11 @@ There's additional configuration that you can optionally pass to setup the `http You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { httpRouterFactory } from '@backstage/backend-app-api'; +import { httpRouterServiceFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ - httpRouterFactory({ + httpRouterServiceFactory({ getPath: (pluginId: string) => `/plugins/${pluginId}`, }), ], @@ -84,7 +86,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -114,11 +116,11 @@ There's additional options that you can pass to configure the root HTTP Router s You can configure the root HTTP Router service by passing the options to the `createBackend` function. ```ts -import { rootHttpRouterFactory } from '@backstage/backend-app-api'; +import { rootHttpRouterServiceFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ - rootHttpRouterFactory({ + rootHttpRouterServiceFactory({ configure: ({ app, middleware, routes, config, logger, lifecycle }) => { // the built in middleware is provided through an option in the configure function app.use(middleware.helmet()); @@ -155,7 +157,7 @@ import { } from '@backstage/backend-plugin-api'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -181,11 +183,11 @@ There's additional configuration that you can optionally pass to setup the `conf You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { configFactory } from '@backstage/backend-app-api'; +import { configServiceFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ - configFactory({ + configServiceFactory({ argv: [ '--config', '/backstage/app-config.development.yaml', @@ -213,7 +215,7 @@ import { } from '@backstage/backend-plugin-api'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -286,7 +288,7 @@ import { } from '@backstage/backend-plugin-api'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -325,7 +327,7 @@ import { import { resolvePackagePath } from '@backstage/backend-common'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -364,7 +366,7 @@ import { import { fetch } from 'node-fetch'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -395,7 +397,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -436,11 +438,11 @@ There's additional configuration that you can optionally pass to setup the `iden You can configure these additional options by adding an override for the core service when calling `createBackend` like follows: ```ts -import { identityFactory } from '@backstage/backend-app-api'; +import { identityServiceFactory } from '@backstage/backend-app-api'; const backend = createBackend({ services: [ - identityFactory({ + identityServiceFactory({ issuer: 'backstage', algorithms: ['ES256', 'RS256'], }), @@ -463,7 +465,7 @@ import { } from '@backstage/backend-plugin-api'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -477,10 +479,7 @@ createBackendPlugin({ // do some other stuff. }, 1000); - lifecycle.addShutdownHook({ - fn: () => clearInterval(interval), - logger, - }); + lifecycle.addShutdownHook(() => clearInterval(interval)); }, }); }, @@ -497,17 +496,19 @@ The following example shows how to override the default implementation of the li ```ts class MyCustomLifecycleService implements RootLifecycleService { - constructor(private readonly logger: LoggerService) { - ['SIGKILL', 'SIGTERM'].map(signal => - process.on(signal, () => this.shutdown()), - ); - } + constructor(private readonly logger: LoggerService) {} #isCalled = false; - #shutdownTasks: Array = []; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; - addShutdownHook(options: LifecycleServiceShutdownHook): void { - this.#shutdownTasks.push(options); + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + this.#shutdownTasks.push({ hook, options }); } async shutdown(): Promise { @@ -518,10 +519,10 @@ class MyCustomLifecycleService implements RootLifecycleService { this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( - this.#shutdownTasks.map(async hook => { - const { logger = this.logger } = hook; + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; try { - await hook.fn(); + await hook(); logger.info(`Shutdown hook succeeded`); } catch (error) { logger.error(`Shutdown hook failed, ${error}`); @@ -562,7 +563,7 @@ import { import { Router } from 'express'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -611,7 +612,7 @@ import { import { fetch } from 'node-fetch'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { @@ -650,7 +651,7 @@ import { import os from 'os'; createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env) { env.registerInit({ deps: { diff --git a/docs/backend-system/index.md b/docs/backend-system/index.md index de598167ae..d2d7c8e69e 100644 --- a/docs/backend-system/index.md +++ b/docs/backend-system/index.md @@ -6,10 +6,10 @@ sidebar_label: Overview description: The Backend System --- -> **DISCLAIMER: The new backend system is under active development and is not considered stable** +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** ## Status -The new backend system is under active development, and only a small number of plugins have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet. +The new backend system is in alpha, and only a small number of plugins have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet. You can find an example backend setup in [the `backend-next` package](https://github.com/backstage/backstage/tree/master/packages/backend-next). diff --git a/docs/features/kubernetes/proxy.md b/docs/features/kubernetes/proxy.md new file mode 100644 index 0000000000..5848d62e3e --- /dev/null +++ b/docs/features/kubernetes/proxy.md @@ -0,0 +1,87 @@ +--- +id: proxy +title: Kubernetes Backend Proxy Endpoint +sidebar_label: Proxy +description: Interacting with the Kubernetes API in Backstage plugins +--- + +[Contributors](https://backstage.io/docs/overview/glossary#backstage-user-profiles) wanting to +create developer portal experiences based on data from Kubernetes (e.g. for +interacting with [Custom +Resources](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) +beyond the default behaviors of the existing Kubernetes plugin) can leverage the +Kubernetes backend plugin's proxy endpoint to allow them to make arbitrary +requests to the [REST +API](https://kubernetes.io/docs/reference/using-api/api-concepts/). + +Here is a snippet fetching namespaces from a cluster configured with the +`google` [auth provider](https://backstage.io/docs/features/kubernetes/configuration#clustersauthprovider): + +```typescript +import { + discoveryApiRef, + googleAuthApiRef, + useApi, +} from '@backstage/core-plugin-api'; + +const CLUSTER_NAME = ''; // use a known cluster name + +// get a bearer token from Google +const googleAuthApi = useApi(googleAuthApiRef); +const token = await googleAuthApi.getAccessToken( + 'https://www.googleapis.com/auth/cloud-platform', +); + +const discoveryApi = useApi(discoveryApiRef); +const kubernetesBaseUrl = await discoveryApi.getBaseUrl('kubernetes'); +const kubernetesProxyEndpoint = `${kubernetesBaseUrl}/proxy`; + +// fetch namespaces +await fetch(`${kubernetesProxyEndpoint}/api/v1/namespaces`, { + method: 'GET', + headers: { + 'X-Kubernetes-Cluster': CLUSTER_NAME, + Authorization: `Bearer ${token}`, + }, +}); +``` + +## How it works + +The proxy will interpret the +[`X-Kubernetes-Cluster` +header](https://backstage.io/docs/reference/plugin-kubernetes-backend.header_kubernetes_cluster) +as the name of the cluster to target. This name will be compared to each cluster +returned by all the configured [cluster +locators](https://backstage.io/docs/features/kubernetes/configuration#clusterlocatormethods) +-- the first cluster whose [`name` field](https://backstage.io/docs/features/kubernetes/configuration#clustersname) matches +the value in the header will be targeted. + +Then the request will be forwarded verbatim (but with the endpoint's base URL +prefix stripped) to the cluster. + +## Authentication + +Until some security and permission decisions are made (see [this +conversation](https://github.com/backstage/backstage/pull/13026/files#r1029376939) +for context), contributors consuming the proxy endpoint in their plugin code are +responsible for negotiating their own bearer token out-of-band. This requires +knowing some auth details about the cluster being contacted -- in practice, only +clusters with [client side auth +providers](https://backstage.io/docs/features/kubernetes/authentication#client-side-providers) can reasonably be reached. + +The proxy has no provisions for mTLS, so it cannot be used to connect to +clusters using the [x509 Client +Certs](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#x509-client-certs) +authentication strategy. [Bearer +tokens](https://kubernetes.io/docs/reference/access-authn-authz/authentication/#putting-a-bearer-token-in-a-request) +will be forwarded as-is. + +## Other known limitations + +The proxy as it was released in [Backstage +1.9](https://github.com/backstage/backstage/blob/master/docs/releases/v1.9.0-changelog.md#patch-changes-15) +has a known bug: + +- [#15901](https://github.com/backstage/backstage/issues/15901) - it cannot + reliably target clusters who share the same name with another located cluster. diff --git a/docs/features/search/how-to-guides.md b/docs/features/search/how-to-guides.md index 6f113630f6..b464168724 100644 --- a/docs/features/search/how-to-guides.md +++ b/docs/features/search/how-to-guides.md @@ -235,6 +235,20 @@ export const YourSearchResultListItemExtension = plugin.provide( ); ``` +If your list item accept props, you can extend the `SearchResultListItemExtensionProps` with your component specific props: + +```tsx +export const YourSearchResultListItemExtension: ( + props: SearchResultListItemExtensionProps, +) => JSX.Element | null = plugin.provide( + createSearchResultListItemExtension({ + name: 'YourSearchResultListItem', + component: () => + import('./components').then(m => m.YourSearchResultListItem), + }), +); +``` + Additionally, you can define a predicate function that receives a result and returns whether your extension should be used to render it or not: ```tsx diff --git a/docs/features/software-catalog/external-integrations.md b/docs/features/software-catalog/external-integrations.md index da4eb3d229..5fc2537712 100644 --- a/docs/features/software-catalog/external-integrations.md +++ b/docs/features/software-catalog/external-integrations.md @@ -89,23 +89,23 @@ export class FrobsProvider implements EntityProvider { private readonly reader: UrlReader; private connection?: EntityProviderConnection; - /** [1] **/ + /** [1] */ constructor(env: string, reader: UrlReader) { this.env = env; this.reader = reader; } - /** [2] **/ + /** [2] */ getProviderName(): string { return `frobs-${this.env}`; } - /** [3] **/ + /** [3] */ async connect(connection: EntityProviderConnection): Promise { this.connection = connection; } - /** [4] **/ + /** [4] */ async run(): Promise { if (!this.connection) { throw new Error('Not initialized'); @@ -116,10 +116,10 @@ export class FrobsProvider implements EntityProvider { ); const data = JSON.parse(await response.buffer()).toString(); - /** [5] **/ + /** [5] */ const entities: Entity[] = frobsToEntities(data); - /** [6] **/ + /** [6] */ await this.connection.applyMutation({ type: 'full', entities: entities.map(entity => ({ diff --git a/docs/features/software-catalog/system-model.md b/docs/features/software-catalog/system-model.md index 409f3e8450..af359bb18a 100644 --- a/docs/features/software-catalog/system-model.md +++ b/docs/features/software-catalog/system-model.md @@ -63,6 +63,17 @@ BigTable databases, Pub/Sub topics, S3 buckets or CDNs. Modelling them together with components and systems will better allow us to visualize resource footprint, and create tooling around them. +## Organizational Entities + +### User + +A user describes a person, such as an employee, a contractor, or similar. + +### Group + +A group describes an organizational entity, such as for example a team, a +business unit, or a loose collection of people in an interest group. + ## Ecosystem Modeling A large catalogue of components, APIs and resources can be highly granular and @@ -108,3 +119,20 @@ domain would come with some documentation on how to accept payments for a new product or use-case, share the same entity types in their APIs, and integrate well with each other. Other domains could be “Content Ingestion”, “Ads” or “Search”. + +## Other + +### Location + +A location is a marker that references other places to look for catalog data. + +### Type + +The type field in the system has no set meaning. It is up to the user to assign their own types and use them as desired, such as for link validation or creating custom UI components. Some common pre-defined types are depicted in the +[ecosystem modeling diagram](#ecosystem-modeling). + +### Template + +A template definition describes both the parameters that are rendered in the +frontend part of the scaffolding wizard, and the steps that are executed when +scaffolding that component. diff --git a/docs/features/software-templates/testing-scaffolder-alpha.md b/docs/features/software-templates/testing-scaffolder-alpha.md new file mode 100644 index 0000000000..d10c7e7a0f --- /dev/null +++ b/docs/features/software-templates/testing-scaffolder-alpha.md @@ -0,0 +1,234 @@ +--- +id: testing-scaffolder-alpha +title: 'Experimental: Testing out the `alpha` Scaffolder plugin' +# prettier-ignore +description: Docs on the upcoming breaking release for the scaffolder plugin +--- + +## What's `scaffolder/next`? + +The `alpha` version, or as you might have seen referred to in other places the `scaffolder/next` release, is a new version of the `scaffolder` plugin that will be the first breaking change to the plugin, so you can also think of it as `@backstage/plugin-scaffolder@2.0.0`. +Its mostly a rewrite of a lot of the frontend components and pages that had very limited test coverage, which made adding new features to the `scaffolder` plugin quite hard, and we were lacking in confidence when making changes. + +There is of course some other things that have changed when re-writing this, which are essentially what has caused some breaking changes. +Now, this is not like previous scaffolder changes where you would have to change all of your templates as this is only the frontend plugin that is going to have breaking changes. You can read more about the [breaking changes](#breaking-changes) below. + +## What's new? + +First off, the main dependency that we have for the frontend which is responsible for rendering the `JSONSchema` into `material-ui` components is [react-jsonschema-form](https://github.com/rjsf-team/react-jsonschema-form). +This dependency in the current version of the plugin is 3.x.x, which is now 2 major versions out of date. Long story short, `v4` of this plugin contained some bug fixes, and new features but we we're unable to upgrade due to some issues with having support for `material-ui@v4`, so we had to wait for `v5` to be released, and because of the `FieldExtensions` and how they are very tightly coupled to the `react-jsonschema-form` library, we also wanted to make sure that this release was stable before getting people to migrate their `Field Extensions`. + +With that in mind, this release has `v5` of `react-jsonschema-form`, and with that comes all the new features and bugfixes in `v4` that we were waiting for - one of the main ones being the ability to use `if / then / else` syntax in the `template.yaml` definitions! 🎉 + +We've also rebuilt how validation works in the `scaffolder` components, which now means that we've opened the ability to have `async` validation functions in your `Field Extensions`. + +Some of the pages have gotten a little bit of an overhaul in terms of UI based on some research and feedback from the community and internally. + +- The `TemplateList` page has gotten some new `Card` components which show a little more information than the previous version with a little `material-ui` standards. +- The `WizardPage` has received some new updates with the stepper now running horizontally, and the `Review` step being a dedicated step in the stepper. +- The `OngoingTask` page now does not show the logs by default, and instead has a much cleaner interface for tracking the ongoing steps and the pipeline of actions that are currently showing. + - You can also now provide your own `OutputsComponent` which can be used to render the outputs from an ongoing / completed task in a way that suits your templates the best. For instance, if your template produces `Pull Requests`, it could be useful to render these in an interactive way where you can see the statuses of each of these `Pull Requests` in the `Ongoing Task` page. + +There's also a lot of bug fixes, and other things, but these are the main ones that we wanted to highlight. + +## How do I test out the `alpha` version? + +With the release of `v1.12.0` it's now possible to run the `scaffolder/next` plugin and it be a drop in replacement for the current version that you use today. This means that you can start using the new code, and start testing it out. Once we have collected enough feedback, and squashed any bugs that might block us from releasing, it will be promoted from the `/alpha` exports and replace the existing code leading to breaking changes if you haven't already made these changes as part of this testing pilot. Those that have chosen to opt into this testing pilot means that once we promote it from the `/alpha` exports, you will need to update your code to point to the original exports from the `scaffolder` plugin, just like the code is today but with the [breaking changes](#breaking-changes) that you already made to your `Custom Field Extensions`. + +It's also worth calling out that if you do test this out, and find some issues or something not working out as expected, feel free to raise an issue in the [repo](https://github.com/backstage/backstage) or reach out to us on Discord! + +### Make the required changes to `App.tsx` + +The `ScaffolderPage` router has a completely different export for the `scaffolder/next` work, so you will want to change any import from the old `ScaffolderPage` to the new `NextScaffolderPage` + +```diff +- import { ScaffolderPage } from '@backstage/plugin-scaffolder'; ++ import { NextScaffolderPage } from '@backstage/plugin-scaffolder/alpha'; + +``` + +And this API should be the exact same as the previous Router, so you should be able to make a change like the following further down in this file: + +```diff + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} + /> + } + > + + + ... other extensions + + + + ... other layouts + + +``` + +### Make the required changes to your `CustomFieldExtensions` + +There's differently named function for creating field extensions part of the `/alpha` exports as these are the ones that can contain breaking changes because of the breaking changes that have been applied in `react-jsonschema-form`. + +Let's take the following example: + +```ts +export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: EntityNamePicker, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + schema: EntityNamePickerSchema, + }), +); +``` + +References for `createScaffolderFieldExtension` have an `/alpha` version of `createNextScaffolderFieldExtension`, which should be used instead. + +```diff +-import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder'; ++import { createNextScaffolderFieldExtension } from '@backstage/plugin-scaffolder/alpha'; + +export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( +- createScaffolderFieldExtension({ ++ createNextScaffolderFieldExtension({ + component: EntityNamePicker, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + }), +); +``` + +Once you've done this you will find that you will have two squiggly lines under the properties that are passed in. One for the component and one for the validation (if provided.) + +Let's take the following code for the `EntityNamePicker` component: + +```ts +export const EntityNamePicker = ( + props: FieldExtensionComponentProps, +) => { + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, + formData, + uiSchema: { 'ui:autofocus': autoFocus }, + idSchema, + placeholder, + } = props; + ... +} +``` + +There's another `/alpha` export that you need to replace `FieldExtensionComponentProps` with which is the `NextFieldExtensionComponentProps`. + +```diff +- import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react'; ++ import { NextFieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react/alpha'; + +export const EntityNamePicker = ( +- props: FieldExtensionComponentProps, ++ props: NextFieldExtensionComponentProps, +) => { + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, + formData, +- uiSchema: { 'ui:autofocus': autoFocus }, ++ uiSchema: { 'ui:autofocus': autoFocus } = {}, + idSchema, + placeholder, + } = props; + ... +} +``` + +You'll notice that there's an additional change here, which is that we're now defaulting the `uiSchema` to an empty object. This is because the `uiSchema` is now optional, and if you don't provide it, it will be `undefined` instead of an empty object. There's more around this in the [breaking changes](#breaking-changes) section. + +To fix the previous validation error, you will need to change the import for the `FieldValidation` type that is used in the `validation` function. + +Let's take the following example of the validation function: + +```ts +import { FieldValidation } from '@rjsf/utils'; +import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; + +export const entityNamePickerValidation = ( + value: string, + validation: FieldValidation, +) => { + if (!KubernetesValidatorFunctions.isValidObjectName(value)) { + validation.addError( + 'Must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', + ); + } +}; +``` + +You will need to change the import for `FieldValidation` to point at the new `react-jsonschema-form` dependency. + +> Note: you will probably need to install this dependency too, by using `yarn add @rjsf/utils` in the package where you define these validation functions, this could also be in the `packages/app` folder, so you can install it there if needed. + +```diff +- import { FieldValidation } from '@rjsf/core'; ++ import { FieldValidation } from '@rjsf/utils; +import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; + +export const entityNamePickerValidation = ( + value: string, + validation: FieldValidation, +) => { +``` + +## Breaking Changes + +Once we fully release the code that is in the `/alpha` exports right now onto the current API and release v2.0.0 of `@backstage/plugin-scaffolder` the breaking changes will be as follows: + +### `uiSchema` is now optional + +Later releases of `react-jsonschema-form` have made the `uiSchema` optional, and if you don't provide it, it will be `undefined` instead of an empty object. This means that you will need to make sure that you're defaulting the `uiSchema` to an empty object if you're using it in your code. + +```diff + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, + formData, +- uiSchema: { 'ui:autofocus': autoFocus }, ++ uiSchema: { 'ui:autofocus': autoFocus } = {}, + idSchema, + placeholder, + } = props; +``` + +### `formData` can also be `undefined` + +If you were using the `formData` and assuming that it was set to an empty object when building `Field Extensions` that return objects, then this will be `undefined` now due to a change in the `react-jsonschema-form` library. + +```diff + const { + onChange, + required, + schema: { title = 'Name', description = 'Unique name of the component' }, + rawErrors, +- formData, ++ formData = {}, // or maybe some other default value that you would prefer + uiSchema: { 'ui:autofocus': autoFocus } = {}, + idSchema, + placeholder, + } = props; +``` diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index dfbd4b0a42..ffe3312501 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -168,5 +168,6 @@ scaffolder backend: | Scaffolder Git Actions | [plugin-scaffolder-git-actions](https://www.npmjs.com/package/@mdude2314/backstage-plugin-scaffolder-git-actions) | [Drew Hill](https://github.com/arhill05) | | Azure Pipeline Actions | [scaffolder-backend-module-azure-pipelines](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-pipelines) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) | | Azure Repository Actions | [scaffolder-backend-module-azure-repositories](https://www.npmjs.com/package/@parfuemerie-douglas/scaffolder-backend-module-azure-repositories) | [Parfümerie Douglas](https://github.com/Parfuemerie-Douglas) | +| Snyk Import Project | [plugin-scaffolder-backend-module-snyk](https://www.npmjs.com/package/@ma11hewthomas/plugin-scaffolder-backend-module-snyk) | [Matthew Thomas](https://github.com/Ma11hewThomas) | Have fun! 🚀 diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index 71aa0ab758..a272f05f3f 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -112,11 +112,11 @@ steps which would be rendered as different steps in the scaffolder plugin frontend. Each `Step` is `JSONSchema` with some extra goodies for styling what it might -look like in the frontend. For these steps we rely very heavily on this library: -https://github.com/rjsf-team/react-jsonschema-form. They have some great docs -too here: https://react-jsonschema-form.readthedocs.io/ and a playground where -you can play around with some examples here -https://rjsf-team.github.io/react-jsonschema-form. +look like in the frontend. For these steps we rely very heavily on this +[library](https://github.com/rjsf-team/react-jsonschema-form). They have some +[great docs](https://rjsf-team.github.io/react-jsonschema-form/docs/) and a +[playground](https://rjsf-team.github.io/react-jsonschema-form) where you can +play around with some examples. There's another option for that library called `uiSchema` which we've taken advantage of, and we've merged it with the existing `JSONSchema` that you diff --git a/docs/features/techdocs/addons.md b/docs/features/techdocs/addons.md index fe1058b2ca..60999f599c 100644 --- a/docs/features/techdocs/addons.md +++ b/docs/features/techdocs/addons.md @@ -125,6 +125,7 @@ discover available Addons, we've compiled a list of them here: | [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.expandablenavigation) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to expand or collapse the entire TechDocs main navigation, and keeps the user's preferred state between documentation sites. | | [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.reportissue) | `@backstage/plugin-techdocs-module-addons-contrib` | Allows TechDocs users to select a portion of text on a TechDocs page and open an issue against the repository that contains the documentation, populating the issue description with the selected text according to a configurable template. | | [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.textsize) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to customize text size on documentation pages, they can select how much they want to increase or decrease the font size via slider or buttons. The default value for font size is 100% and this setting is kept in the browser's local storage whenever it is changed. | +| [``](https://backstage.io/docs/reference/plugin-techdocs-module-addons-contrib.lightbox) | `@backstage/plugin-techdocs-module-addons-contrib` | This TechDocs addon allows users to open images in a light-box on documentation pages, they can navigate between images if there are several on one page. The image size of the light-box image is the same as the image size on the document page. | Got an Addon to contribute? Feel free to add a row above! diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index c2070e7840..578a631798 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -262,12 +262,12 @@ You will have to install the `mkdocs` and `mkdocs-techdocs-core` package from pip, as well as `graphviz` and `plantuml` from your OS package manager (e.g. apt). -You can do so by including the following lines in the last step of your +You can do so by including the following lines right above `USER node` of your `Dockerfile`: ```Dockerfile RUN apt-get update && apt-get install -y python3 python3-pip -RUN pip3 install mkdocs-techdocs-core==1.0.1 +RUN pip3 install mkdocs-techdocs-core==1.1.7 ``` Please be aware that the version requirement could change, you need to check our diff --git a/docs/getting-started/configure-app-with-plugins.md b/docs/getting-started/configure-app-with-plugins.md index ebe98f9ddc..835da0cd49 100644 --- a/docs/getting-started/configure-app-with-plugins.md +++ b/docs/getting-started/configure-app-with-plugins.md @@ -22,7 +22,8 @@ to an entity in the software catalog. 1. Add the plugin's npm package to the repo: ```bash - yarn workspace app add @backstage/plugin-circleci + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-circleci ``` Note the plugin is added to the `app` package, rather than the root diff --git a/docs/integrations/aws-s3/discovery.md b/docs/integrations/aws-s3/discovery.md index 748aeb2392..4a2b953bc9 100644 --- a/docs/integrations/aws-s3/discovery.md +++ b/docs/integrations/aws-s3/discovery.md @@ -63,7 +63,7 @@ As this provider is not one of the default providers, you will first need to ins the AWS catalog plugin: ```bash -# From the Backstage root directory +# From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-aws ``` diff --git a/docs/integrations/azure/locations.md b/docs/integrations/azure/locations.md index 3b3e3dd5ed..1fc476f418 100644 --- a/docs/integrations/azure/locations.md +++ b/docs/integrations/azure/locations.md @@ -28,3 +28,5 @@ The configuration is a structure with two elements: - `host`: The DevOps host; only `dev.azure.com` is supported. - `token` (optional): A personal access token as expected by Azure DevOps. + +> Note: The token should just be provided as the raw token generated by Azure DevOps using the format `raw_token` with no base64 encoding. Formatting and base64'ing is handled by dependent libraries handling the Azure DevOps API diff --git a/docs/integrations/gerrit/discovery.md b/docs/integrations/gerrit/discovery.md index 7ed05d2b3b..5d822effb5 100644 --- a/docs/integrations/gerrit/discovery.md +++ b/docs/integrations/gerrit/discovery.md @@ -17,7 +17,7 @@ As this provider is not one of the default providers, you will first need to ins the Gerrit provider plugin: ```bash -# From the Backstage root directory +# From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gerrit ``` diff --git a/docs/integrations/gitlab/discovery.md b/docs/integrations/gitlab/discovery.md index dc0b911b3d..9a065e776a 100644 --- a/docs/integrations/gitlab/discovery.md +++ b/docs/integrations/gitlab/discovery.md @@ -22,9 +22,9 @@ catalog: yourProviderId: host: gitlab-host # Identifies one of the hosts set up in the integrations branch: main # Optional. Uses `master` as default - group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole project will be scanned + group: example-group # Optional. Group and subgroup (if needed) to look for repositories. If not present the whole instance will be scanned entityFilename: catalog-info.yaml # Optional. Defaults to `catalog-info.yaml` - projectPattern: /[\s\S]*/ # Optional. Filters found projects based on provided patter. Defaults to `/[\s\S]*/`, what means to not filter anything + projectPattern: '[\s\S]*' # Optional. Filters found projects based on provided patter. Defaults to `[\s\S]*`, which means to not filter anything schedule: # optional; same options as in TaskScheduleDefinition # supports cron, ISO duration, "human duration" as used in code frequency: { minutes: 30 } @@ -36,7 +36,7 @@ As this provider is not one of the default providers, you will first need to ins the gitlab catalog plugin: ```bash -# From the Backstage root directory +# From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-gitlab ``` diff --git a/docs/integrations/gitlab/org.md b/docs/integrations/gitlab/org.md index 100b2c09fd..2ab72b4bd6 100644 --- a/docs/integrations/gitlab/org.md +++ b/docs/integrations/gitlab/org.md @@ -20,9 +20,9 @@ integrations: ``` This will query all users and groups from your gitlab installation. Depending on the size -of the Gitlab Instance, this can take some time our resources. +of the Gitlab Instance, this can take some time and resources. -The token that is used for the Organization Integration, has to be an Admin PAT. +The token that is used for the Organization Integration, has to be an Admin Personal Access Token (PAT). ```yaml catalog: diff --git a/docs/local-dev/cli-build-system.md b/docs/local-dev/cli-build-system.md index cc364d0ca5..cf8ac626b9 100644 --- a/docs/local-dev/cli-build-system.md +++ b/docs/local-dev/cli-build-system.md @@ -556,7 +556,22 @@ The overrides in a single `package.json` may for example look like this: }, ``` -If you want to configure editor integration for tests we recommend executing the bundled configuration directly with Jest rather than running through the Yarn test script. For example, with the Jest extension for VS Code the configuration would look something like this: +### Debugging Jest Tests + +For your productivity working with unit tests it's quite essential to have your debugging configured in IDE. It will help you to identify the root cause of the issue faster. + +#### IntelliJ IDEA + +1. Update Jest configuration template by: + +- Click on "Edit Configurations" on top panel +- In the modal dialog click on link "Edit configuration templates..." located in the bottom left corner. +- In "Jest package" you have to point to relative path of jest module (it will be suggested by IntelliJ), i.e. `~/proj/backstage/node_modules/jest` +- In "Jest config" point to your jest configuration file, use absolute path for that, i.e. `--config /Users/user/proj/backstage/packages/cli/config/jest.js --runInBand` + +2. Now you can run any tests by clicking on green arrow located on `describe` or `it`. + +#### VS Code ```jsonc { @@ -571,7 +586,7 @@ If you want to configure editor integration for tests we recommend executing the } ``` -If you also want to enable source maps when debugging tests, you can do so by setting the `ENABLE_SOURCE_MAPS` environment variable. For example, a complete launch configuration for VS Code debugging may look like this: +A complete launch configuration for VS Code debugging may look like this: ```json { @@ -583,9 +598,6 @@ If you also want to enable source maps when debugging tests, you can do so by se "disableOptimisticBPs": true, "program": "${workspaceFolder}/node_modules/.bin/jest", "cwd": "${workspaceFolder}", - "env": { - "ENABLE_SOURCE_MAPS": "true" - }, "args": [ "--config", "node_modules/@backstage/cli/config/jest.js", diff --git a/docs/permissions/getting-started.md b/docs/permissions/getting-started.md index d2c467ff08..e71601b703 100644 --- a/docs/permissions/getting-started.md +++ b/docs/permissions/getting-started.md @@ -47,7 +47,8 @@ The permissions framework uses a new `permission-backend` plugin to accept autho 1. Add `@backstage/plugin-permission-backend` as a dependency of your Backstage backend: ```bash -$ yarn workspace backend add @backstage/plugin-permission-backend +# From your Backstage root directory +$ yarn add --cwd packages/backend @backstage/plugin-permission-backend ``` 2. Add the following to a new file, `packages/backend/src/plugins/permission.ts`. This adds the permission-backend router, and configures it with a policy which allows everything. diff --git a/docs/permissions/overview.md b/docs/permissions/overview.md index b972814aeb..693f848c82 100644 --- a/docs/permissions/overview.md +++ b/docs/permissions/overview.md @@ -34,6 +34,8 @@ The permission framework was designed with a few key properties in mind: 4. An authorization decision is sent to the plugin from the permission backend. +5. The user is either granted access or an error is shown. The plugin is responsible for implementing a response to the user. + ## How do I get started? See the "[getting started](./getting-started.md)" permission documentation for Backstage integrators. diff --git a/docs/permissions/plugin-authors/01-setup.md b/docs/permissions/plugin-authors/01-setup.md index 18de410612..c368505849 100644 --- a/docs/permissions/plugin-authors/01-setup.md +++ b/docs/permissions/plugin-authors/01-setup.md @@ -26,6 +26,9 @@ The source code is available here: git checkout master -- plugins/example-todo-list/ git checkout master -- plugins/example-todo-list-backend/ git checkout master -- plugins/example-todo-list-common/ + sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list/package.json + sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-backend/package.json + sed -i '' 's/workspace:\^/\*/g' plugins/example-todo-list-common/package.json for file in plugins/*; do mv "$file" "$OLDPWD/${file/example-todo/todo}"; done cd - ``` @@ -36,16 +39,17 @@ The source code is available here: 2. Add these packages as dependencies for your Backstage app: - ``` - $ yarn workspace backend add @internal/plugin-todo-list-backend@^1.0.0 @internal/plugin-todo-list-common@^1.0.0 - $ yarn workspace app add @internal/plugin-todo-list@^1.0.0 + ```sh + # From your Backstage root directory + $ yarn add --cwd packages/backend @internal/plugin-todo-list-backend @internal/plugin-todo-list-common + $ yarn add --cwd packages/app @internal/plugin-todo-list ``` 3. Include the backend and frontend plugin in your application: Create a new `packages/backend/src/plugins/todolist.ts` with the following content: - ```javascript + ```typescript import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; import { createRouter } from '@internal/plugin-todo-list-backend'; import { Router } from 'express'; diff --git a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md index 241b175987..2ceb8057f4 100644 --- a/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md +++ b/docs/permissions/plugin-authors/02-adding-a-basic-permission-check.md @@ -146,7 +146,7 @@ In order to test the logic above, the integrators of your backstage instance nee - async handle(): Promise { + async handle( + request: PolicyQuery, -+ user?: BackstageIdentityResponse, ++ _user?: BackstageIdentityResponse, + ): Promise { + if (isPermission(request.permission, todoListCreatePermission)) { + return { @@ -204,7 +204,7 @@ First we'll clean up the `plugins/todo-list-backend/src/service/router.test.ts`: const router = await createRouter({ logger: getVoidLogger(), identity: {} as DefaultIdentityClient, -+ permissions: toPermissionEvaluator, ++ permissions: permissionEvaluator, }); app = express().use(router); }); @@ -235,7 +235,7 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ + ServerTokenManager, } from '@backstage/backend-common'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; - import { ServerPermissionClient } from '@backstage/plugin-permission-node'; ++ import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { Server } from 'http'; import { Logger } from 'winston'; import { createRouter } from './router'; @@ -285,4 +285,44 @@ Then we want to update the `plugins/todo-list-backend/src/service/standaloneServ module.hot?.accept(); ``` +Finally, we need to update `plugins/todo-list-backend/src/plugin.ts`: + +```diff + import { loggerToWinstonLogger } from '@backstage/backend-common'; + import { + coreServices, + createBackendPlugin, + } from '@backstage/backend-plugin-api'; + import { createRouter } from './service/router'; + + /** + * The example TODO list backend plugin. + * + * @alpha + */ + export const exampleTodoListPlugin = createBackendPlugin({ + pluginId: 'exampleTodoList', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, ++ permissions: coreServices.permissions, + }, +- async init({ identity, logger, httpRouter }) { ++ async init({ identity, logger, httpRouter, permissions }) { + httpRouter.use( + await createRouter({ + identity, + logger: loggerToWinstonLogger(logger), + permissions, + }), + ); + }, + }); + }, + }); +``` + Now when you run `yarn tsc` you should have no more errors. diff --git a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md index 8913b6d438..b063727c71 100644 --- a/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md +++ b/docs/permissions/plugin-authors/03-adding-a-resource-permission-check.md @@ -76,7 +76,7 @@ This enables decisions based on characteristics of the resource, but it's import Install the missing module: ``` -$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node +$ yarn workspace @internal/plugin-todo-list-backend add @backstage/plugin-permission-node zod ``` Create a new `plugins/todo-list-backend/src/service/rules.ts` file and append the following code: @@ -89,7 +89,8 @@ import { Todo, TodoFilter } from './todos'; export const createTodoListPermissionRule = makeCreatePermissionRule< Todo, - TodoFilter + TodoFilter, + typeof TODO_LIST_RESOURCE_TYPE >(); export const isOwner = createTodoListPermissionRule({ @@ -97,8 +98,8 @@ export const isOwner = createTodoListPermissionRule({ description: 'Should allow only if the todo belongs to the user', resourceType: TODO_LIST_RESOURCE_TYPE, paramsSchema: z.object({ - userId: z.string().describe('User ID to match on the resource') - }) + userId: z.string().describe('User ID to match on the resource'), + }), apply: (resource: Todo, { userId }) => { return resource.author === userId; }, @@ -187,6 +188,7 @@ Make sure `todoListConditions` and `createTodoListConditionalDecision` are expor ```diff export * from './service/router'; + export * from './conditionExports'; + export { exampleTodoListPlugin } from './plugin'; ``` ## Test the authorized update endpoint @@ -209,7 +211,6 @@ Let's go back to the permission policy's handle function and try to authorize ou + import { + todoListCreatePermission, + todoListUpdatePermission, -+ TODO_LIST_RESOURCE_TYPE, + } from '@internal/plugin-todo-list-common'; + import { + todoListConditions, @@ -217,7 +218,11 @@ Let's go back to the permission policy's handle function and try to authorize ou + } from '@internal/plugin-todo-list-backend'; ... - + async handle( + request: PolicyQuery, +- _user?: BackstageIdentityResponse, ++ user?: BackstageIdentityResponse, + ): Promise { if (isPermission(request.permission, todoListCreatePermission)) { return { result: AuthorizeResult.ALLOW, @@ -236,6 +241,7 @@ Let's go back to the permission policy's handle function and try to authorize ou return { result: AuthorizeResult.ALLOW, }; + } ``` For any incoming update requests, we now return a _Conditional Decision_. We are saying: diff --git a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md index 8ddcffdcd9..e84f53f176 100644 --- a/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md +++ b/docs/permissions/plugin-authors/04-authorizing-access-to-paginated-data.md @@ -15,7 +15,7 @@ One possible solution may leverage the batching functionality to authorize all o - res.json(getAll()) + const items = getAll(); + const decisions = await permissions.authorize( -+ items.map(({ id }) => ({ permission: todosListRead, resourceRef: id })), ++ items.map(({ id }) => ({ permission: todoListReadPermission, resourceRef: id })), + ); + const filteredItems = decisions.filter( @@ -53,7 +53,7 @@ Let's add another permission to the plugin. resourceType: TODO_LIST_RESOURCE_TYPE, }); + -+ export const todosListRead = createPermission({ ++ export const todoListReadPermission = createPermission({ + name: 'todos.list.read', + attributes: { action: 'read' }, + resourceType: TODO_LIST_RESOURCE_TYPE, @@ -81,10 +81,9 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev import { todosListCreate, todosListUpdate, -+ todosListRead, ++ todoListReadPermission, TODO_LIST_RESOURCE_TYPE, } from './permissions'; -+ import { rules } from './rules'; + const transformConditions: ConditionTransformer = createConditionTransformer(Object.values(rules)); @@ -95,7 +94,7 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev + ); + + const decision = ( -+ await permissions.authorizeConditional([{ permission: todosListRead }], { ++ await permissions.authorizeConditional([{ permission: todoListReadPermission }], { + token, + }) + )[0]; @@ -110,7 +109,6 @@ So far we've only used the `PermissionEvaluator.authorize` method, which will ev + } else { + res.json(getAll()); + } -+ } - res.json(getAll()); }); ``` @@ -121,7 +119,7 @@ Since `TodoFilter` used in our plugin matches the structure of the conditions ob ## Test the authorized read endpoint -Let's update our permission policy to return a conditional result whenever a `todosListRead` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission. +Let's update our permission policy to return a conditional result whenever a `todoListReadPermission` permission is received. In this case, we can reuse the decision returned for the `todosListCreate` permission. ```diff // packages/backend/src/plugins/permission.ts @@ -132,7 +130,6 @@ import { todoListCreatePermission, todoListUpdatePermission, + todoListReadPermission, - TODO_LIST_RESOURCE_TYPE, } from '@internal/plugin-todo-list-common'; ... diff --git a/docs/permissions/plugin-authors/05-frontend-authorization.md b/docs/permissions/plugin-authors/05-frontend-authorization.md index d494ea60ba..753ce56461 100644 --- a/docs/permissions/plugin-authors/05-frontend-authorization.md +++ b/docs/permissions/plugin-authors/05-frontend-authorization.md @@ -56,7 +56,11 @@ Let's make the following changes in `plugins/todo-list/src/components/TodoListPa - Add - + {!loadingPermission && ( -+ + )} @@ -118,7 +122,10 @@ Providing a disabled state can be a helpful signal to users, but there may be ca - - - -+ }> ++ } ++ > + + + @@ -149,11 +156,15 @@ Providing a disabled state can be a helpful signal to users, but there may be ca onChange={e => (title.current = e.target.value)} /> - {!loadingPermission && ( -- - )} -+ diff --git a/docs/plugins/backend-plugin.md b/docs/plugins/backend-plugin.md index 517779d161..c4414c8e55 100644 --- a/docs/plugins/backend-plugin.md +++ b/docs/plugins/backend-plugin.md @@ -66,7 +66,7 @@ To actually attach and run the plugin router, you will make some modifications to your backend. ```bash -# From the Backstage root directory +# From your Backstage root directory yarn add --cwd packages/backend @internal/plugin-carmen-backend@^0.1.1 # Change this to match the plugin's package.json ``` diff --git a/docs/plugins/new-backend-system.md b/docs/plugins/new-backend-system.md index bfc6f79661..2ff061a60b 100644 --- a/docs/plugins/new-backend-system.md +++ b/docs/plugins/new-backend-system.md @@ -4,11 +4,11 @@ title: New Backend System description: Details of the upcoming backend system --- -> **DISCLAIMER: The new backend system is under active development and is not considered stable** +> **DISCLAIMER: The new backend system is in alpha, and still under active development. While we have reviewed the interfaces carefully, they may still be iterated on before the stable release.** ## Status -The new backend system is under active development, and only a small number of plugins have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet. +The new backend system is in alpha, and only a small number of plugins have been migrated so far. It is possible to try it out, but it is not recommended to use this new system in production yet. You can find an example backend setup in [the backend-next package](https://github.com/backstage/backstage/tree/master/packages/backend-next). @@ -84,7 +84,7 @@ import { // export type ExamplePluginOptions = { exampleOption: boolean }; export const examplePlugin = createBackendPlugin({ // unique id for the plugin - id: 'example', + pluginId: 'example', // It's possible to provide options to the plugin // register(env, options: ExamplePluginOptions) { register(env) { @@ -111,7 +111,7 @@ If we wanted our plugin to accept options as well, we'd accept the options as th ```ts export const examplePlugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(env, options?: { silent?: boolean }) { env.registerInit({ deps: { logger: coreServices.logger }, diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index baee47c695..2fa086c444 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -371,14 +371,4 @@ Note: wrapping in the test application **requires** you to do a `find()` or # Debugging Jest Tests -Currently, debugging Jest tests using IntelliJ or `node-debugger` is possible -but can be -[problematic to set up.](https://intellij-support.jetbrains.com/hc/en-us/community/posts/115000634564-Debugging-Jest-unit-tests) - -It is possible, but you might spend a decent amount of time configuring your -IDE. - -In most cases, we have found that using `console.log` works well. - -Note: if your console.logs are not being displayed, focus your specific unit -test from the command line by running them like so `yarn test MyTest`. +You can find it [here](https://backstage.io/docs/local-dev/cli-build-system#debugging-jest-tests) diff --git a/docs/plugins/url-reader.md b/docs/plugins/url-reader.md index 595c87ab94..fa9c341543 100644 --- a/docs/plugins/url-reader.md +++ b/docs/plugins/url-reader.md @@ -40,7 +40,7 @@ i.e.`packages/backend/src/index.ts`. ```ts // File: packages/backend/src/index.ts -import { URLReaders } from '@backstage/backend-common'; +import { UrlReaders } from '@backstage/backend-common'; function makeCreateEnv(config: Config) { // .... diff --git a/docs/releases/v1.11.0-next.2-changelog.md b/docs/releases/v1.11.0-next.2-changelog.md new file mode 100644 index 0000000000..65f6622921 --- /dev/null +++ b/docs/releases/v1.11.0-next.2-changelog.md @@ -0,0 +1,2289 @@ +# Release v1.11.0-next.2 + +## @backstage/backend-app-api@0.4.0-next.2 + +### Minor Changes + +- 01a075ec1d: **BREAKING**: Renamed `RootHttpRouterConfigureOptions` to `RootHttpRouterConfigureContext`, and removed the unused type `ServiceOrExtensionPoint`. +- 4ae71b7f2e: **BREAKING** Renaming `*Factory` exports to `*ServiceFactory` instead. For example `configFactory` now is exported as `configServiceFactory`. +- d31d8e00b3: **BREAKING** `HttpServerCertificateOptions` when specified with a `key` and `cert` should also have the `type: 'pem'` instead of `type: 'plain'` + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- f60cca9da1: Updated database factory to pass service deps required for restoring database state during development. +- 610d65e143: Updates to match new `BackendFeature` type. +- ab22515647: The shutdown signal handlers are now installed as part of the backend instance rather than the lifecycle service, and explicitly cause the process to exit. +- b729f9f31f: Moved the options of the `config` and `rootHttpRouter` services out to the factories themselves, where they belong +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/backend-dev-utils@0.1.0-next.0 + +### Minor Changes + +- 9d9cdea117: Introduced a new package for backend development utilities. Similar to how `@backstage/dev-utils` is used in the frontend. + +## @backstage/backend-plugin-api@0.4.0-next.2 + +### Minor Changes + +- e716946103: **BREAKING**: Split out the hook for both lifecycle services so that the first parameter of `addShutdownHook` is the hook function, and the second is the options. +- 0ff03319be: **BREAKING**: The plugin ID option passed to `createBackendPlugin` is now `pluginId`, rather than just `id`. This is to make it match `createBackendModule` more closely. +- 71a5ec0f06: **BREAKING**: Switched out `LogMeta` type for `JsonObject`. +- 610d65e143: Switched `BackendFeature` to be an opaque type. + +### Patch Changes + +- 9c9456fd33: Removed the unused `TypesToServiceRef` type +- 181c03edb5: Aligned opaque type markers to all use a `$type` property with namespacing. +- Updated dependencies + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/catalog-model@1.2.0-next.1 + +### Minor Changes + +- d51668882d: Adds the tRPC API type to the catalog. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/eslint-plugin@0.1.0-next.0 + +### Minor Changes + +- 179d301518: Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details. + +## @backstage/plugin-adr@0.4.0-next.2 + +### Minor Changes + +- ec34b535c0: The `AdrSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- ec34b535c0: Support displaying an icon on `AdrSearchResultListItem` +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.6-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-api-docs@0.9.0-next.2 + +### Minor Changes + +- 9ed53218a5: Adds a new tRPC API definition widget which users can utilize to integrate their tRPC API definitions into Backstage. + +### Patch Changes + +- 6856ab98f4: Updated dependency `@asyncapi/react-component` to `1.0.0-next.47`. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-catalog-backend-module-msgraph@0.5.0-next.2 + +### Minor Changes + +- fb568e2683: Improve performance when loading users via group membership. + Users data is now loaded from a paged query, rather than having to make an extra call per user to load each user's profiles. + + Note, there are still additional per user calls made to load user avatars + +### Patch Changes + +- 28f9883440: Fixed a bug reading the `user.select` field expected from the `app-config.yaml` configuration +- c5b119ad9c: Increased default page size to 999 (from 100) to reduce the number of calls made to the Microsoft Graph API. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-entity-validation@0.1.0-next.0 + +### Minor Changes + +- d34b3267f9: First implementation for the entity validation UI. For more information refer to its `README.md`. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-graphql-voyager@0.1.0-next.0 + +### Minor Changes + +- d1fb4b7bf1: Add the new GraphQL Voyager tool as a plugin for backstage, expose necessary API and components needed for it. + + For more information on how to use this plugin and configure it, please navigate to the [README](https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager/README.md) of the plugin. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-lighthouse@0.4.0-next.2 + +### Minor Changes + +- eef62546ce: Require @backstage/plugin-lighthouse-common package where API implementation moved to. + + **BREAKING**: the following types have been moved to @backstage/plugin-lighthouse-common: + + - `Audit` + - `AuditBase` + - `AuditCompleted` + - `AuditFailed` + - `AuditRunning` + - `FetchError` + - `LASListRequest` + - `LASListResponse` + - `LighthouseApi` + - `LighthouseCategoryAbbr` + - `LighthouseCategoryId` + - `LighthouseConfigSettings` + - `LighthouseRestApi` + - `TriggerAuditPayload` + - `Website` + - `WebsiteListResponse` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-common@0.1.0-next.0 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-lighthouse-backend@0.1.0-next.0 + +### Minor Changes + +- eef62546ce: Introduce Lighthouse Backend Plugin to run scheduled Lighthouse Audits + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-common@0.1.0-next.0 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-lighthouse-common@0.1.0-next.0 + +### Minor Changes + +- eef62546ce: Introduce @backstage/plugin-lighthouse-common with the API implementation + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + +## @backstage/plugin-scaffolder-react@1.1.0-next.2 + +### Minor Changes + +- 5555e17313: refactor `createAsyncValidators` to be recursive to ensure validators are called in nested schemas. + +### Patch Changes + +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## @backstage/plugin-sentry@0.5.0-next.2 + +### Minor Changes + +- 6a8d6b9a0c: Add option to filter issues based on the time it was triggered + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/app-defaults@1.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.9 + +## @backstage/backend-common@0.18.2-next.2 + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- d31d8e00b3: Updated to work with the new `type: 'pem'` with `createHttpServer` from `@backstage/backend-app-api` +- 0ff03319be: Updated usage of `createBackendPlugin`. +- f60cca9da1: The `DatabaseManager.forPlugin` method now accepts additional service dependencies. There is no need to update existing code to pass these dependencies. +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-dev-utils@0.1.0-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/backend-defaults@0.1.7-next.2 + +### Patch Changes + +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + +## @backstage/backend-tasks@0.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/backend-test-utils@0.1.34-next.2 + +### Patch Changes + +- baf6e4c96a: Removed unnecessary `@backstage/cli` dependency. +- 1835311713: Added explicit return type signature for `mockServices.config()`. +- e716946103: Updated usage of the lifecycle service. +- 7e7557a2be: Updated the `mockServices.rootLogger` options to accept a single level option instead. +- 610d65e143: Updates to match new `BackendFeature` type. +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- f1adb2e36b: Removed the `ref` from all `mockServices`. +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/catalog-client@1.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/errors@1.1.4 + +## @backstage/cli@0.22.2-next.1 + +### Patch Changes + +- dd8a9afe66: Replaced several monorepo lint rules with new rules from `@backstage/eslint-plugin`. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for a full list of rules. +- 90616df9a8: Added an experimental mode for the `package start` command for backend packages. Enabled by setting `EXPERIMENTAL_BACKEND_START`. +- Updated dependencies + - @backstage/eslint-plugin@0.1.0-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + +## @backstage/codemods@0.1.43-next.0 + +### Patch Changes + +- 02f1316e57: Moved `commander` to being a regular dependency. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## @backstage/core-components@0.12.4-next.1 + +### Patch Changes + +- 66e2aab4c4: Navigation items in mobile sidebar now have aria label. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.3 + +## @backstage/create-app@0.4.37-next.2 + +### Patch Changes + +- 02f1316e57: Updated `packages/app/cypress/.eslintrc.json` to remove the unnecessary `import/no-extraneous-dependencies` rule. +- Updated dependencies + - @backstage/cli-common@0.1.11 + +## @backstage/dev-utils@1.0.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/integration-react@1.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @backstage/repo-tools@0.1.2-next.0 + +### Patch Changes + +- ff63acf30a: Packages without a declared `backstage.role` are now ignored. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + +## @techdocs/cli@1.3.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/plugin-techdocs-node@1.4.6-next.2 + +## @backstage/plugin-adr-backend@0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.6-next.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-adr-common@0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/integration@1.4.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-airbrake@0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/dev-utils@1.0.12-next.2 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-airbrake-backend@0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-allure@0.1.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-analytics-module-ga@0.1.26-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-apache-airflow@0.2.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + +## @backstage/plugin-apollo-explorer@0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-app-backend@0.3.42-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + +## @backstage/plugin-auth-backend@0.17.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-auth-node@0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-azure-devops@0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-azure-devops-backend@0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + +## @backstage/plugin-azure-sites@0.1.4-next.2 + +### Patch Changes + +- 98a00a09d5: Fixed `AzureSites` plugin start and stop issue #15904 + Fixed `AzureSites` UI load issue #15907 +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-azure-sites-backend@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + +## @backstage/plugin-badges@0.2.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-badges-backend@0.1.36-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-bazaar@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-bazaar-backend@0.2.5-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-bitrise@0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-catalog@1.8.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-backend@1.7.2-next.2 + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-backend-module-aws@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-azure@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-bitbucket@0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + +## @backstage/plugin-catalog-backend-module-bitbucket-cloud@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## @backstage/plugin-catalog-backend-module-bitbucket-server@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-catalog-backend-module-gerrit@0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-catalog-backend-module-github@0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## @backstage/plugin-catalog-backend-module-gitlab@0.1.13-next.2 + +### Patch Changes + +- 49948f644f: The config now consistently uses the term 'instance' to refer to a single GitLab API host. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-incremental-ingestion@0.2.0-next.2 + +### Patch Changes + +- 407dc01fc9: Removing extra imports for `run` script as `TestBackend` auto loads the default factories +- 77c41b6924: Updated README to include newer API options for incremental entity providers +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-catalog-backend-module-ldap@0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-backend-module-openapi@0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-common@1.0.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-catalog-graph@0.2.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-catalog-graphql@0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-catalog-import@0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-catalog-node@1.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + +## @backstage/plugin-catalog-react@1.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + +## @backstage/plugin-cicd-statistics@0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-cicd-statistics-module-gitlab@0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-cicd-statistics@0.1.17-next.2 + +## @backstage/plugin-circleci@0.3.15-next.2 + +### Patch Changes + +- 1616e06047: Making workflow a link +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-cloudbuild@0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-code-climate@0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-code-coverage@0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-code-coverage-backend@0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-codescene@0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-config-schema@0.1.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## @backstage/plugin-cost-insights@0.12.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-cost-insights-common@0.1.1 + +## @backstage/plugin-dynatrace@2.0.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-events-backend@0.2.3-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-module-aws-sqs@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-events-backend-module-azure@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## @backstage/plugin-events-backend-module-bitbucket-cloud@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## @backstage/plugin-events-backend-module-gerrit@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + +## @backstage/plugin-events-backend-module-github@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-module-gitlab@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-events-backend-test-utils@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3-next.2 + +## @backstage/plugin-events-node@0.2.3-next.2 + +### Patch Changes + +- 19d4abf72c: Make `EventParams` typed for implementing tidier event handling. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + +## @backstage/plugin-explore@0.4.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.25 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-explore-backend@0.0.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-firehydrant@0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-fossa@0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-gcalendar@0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gcp-projects@0.3.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-git-release-manager@0.3.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + +## @backstage/plugin-github-actions@0.5.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-github-deployments@0.1.46-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-github-issues@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-github-pull-requests-board@0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-gitops-profiles@0.3.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-gocd@0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-graphiql@0.2.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-graphql-backend@0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.18-next.1 + +## @backstage/plugin-home@0.4.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-ilert@0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-jenkins@0.7.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-jenkins-common@0.1.13-next.1 + +## @backstage/plugin-jenkins-backend@0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-jenkins-common@0.1.13-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-jenkins-common@0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-kafka@0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-kafka-backend@0.2.35-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-kubernetes@0.7.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-kubernetes-common@0.6.0-next.2 + +## @backstage/plugin-kubernetes-backend@0.9.3-next.2 + +### Patch Changes + +- 7ff81f7692: Kubernetes proxy endpoint now accepts content types that are not json +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-kubernetes-common@0.6.0-next.2 + +## @backstage/plugin-kubernetes-common@0.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + +## @backstage/plugin-newrelic@0.3.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-newrelic-dashboard@0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-org@0.6.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-org-react@0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-pagerduty@0.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-periskop@0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-periskop-backend@0.1.13-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-permission-backend@0.5.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-permission-node@0.7.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + +## @backstage/plugin-playlist@0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-playlist-backend@0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-playlist-common@0.1.4 + +## @backstage/plugin-proxy-backend@0.2.36-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-rollbar@0.4.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-rollbar-backend@0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + +## @backstage/plugin-scaffolder@1.11.0-next.2 + +### Patch Changes + +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## @backstage/plugin-scaffolder-backend@1.11.0-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## @backstage/plugin-scaffolder-backend-module-cookiecutter@0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-backend-module-sentry@0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-scaffolder-backend-module-yeoman@0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-common@1.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/types@1.0.2 + +## @backstage/plugin-scaffolder-node@0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + +## @backstage/plugin-search@1.1.0-next.2 + +### Patch Changes + +- 66e2aab4c4: Updated colors for each tab used on search modal, to improve color contrast. Aria label added to tabs wrapper component. `disableRipple` property on the `Tab` component removed to improve keyboard navigation indicator. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend@1.2.3-next.2 + +### Patch Changes + +- 1b30c9a0f7: Change the router's response to include the error message instead of its object type in case it fails during a search query because the messages have more info. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-module-pg@0.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-backend-node@1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-search-react@1.5.0-next.1 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-shortcuts@0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + +## @backstage/plugin-sonarqube@0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-sonarqube-react@0.1.2-next.1 + +## @backstage/plugin-sonarqube-backend@0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-sonarqube-react@0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + +## @backstage/plugin-splunk-on-call@0.4.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-stack-overflow@0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-home@0.4.31-next.2 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-stack-overflow-backend@0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-tech-insights@0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + +## @backstage/plugin-tech-insights-backend@0.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + +## @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + +## @backstage/plugin-tech-insights-node@0.4.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + +## @backstage/plugin-tech-radar@0.6.1-next.1 + +### Patch Changes + +- acf6c8c10b: Deprecate `RadarEntry.url` - use `RadarEntry.links` instead + + ```diff + - url: 'https://www.javascript.com/', + key: 'javascript', + id: 'javascript', + title: 'JavaScript', + quadrant: 'languages', + links: [ + + { + + url: 'https://www.javascript.com/', + + title: 'Learn more', + + }, + ], + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @backstage/plugin-techdocs@1.5.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## @backstage/plugin-techdocs-addons-test-utils@1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## @backstage/plugin-techdocs-backend@1.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-node@1.4.6-next.2 + +## @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## @backstage/plugin-techdocs-node@1.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + +## @backstage/plugin-techdocs-react@1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/version-bridge@1.0.3 + +## @backstage/plugin-todo@0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-todo-backend@0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + +## @backstage/plugin-user-settings@0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-user-settings-backend@0.1.6-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + +## @backstage/plugin-vault@0.1.9-next.2 + +### Patch Changes + +- fd7a77b9c4: Surface additional context and details to the Backstage UI when the Vault plugin encounters non-successful HTTP responses from the Vault API. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @backstage/plugin-vault-backend@0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + +## @backstage/plugin-xcmetrics@0.2.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + +## example-app@0.2.80-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.2 + - @backstage/plugin-scaffolder@1.11.0-next.2 + - @backstage/cli@0.22.2-next.1 + - @backstage/plugin-api-docs@0.9.0-next.2 + - @backstage/plugin-tech-radar@0.6.1-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-explore@0.4.0-next.2 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/plugin-sentry@0.5.0-next.2 + - @backstage/plugin-circleci@0.3.15-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/plugin-azure-sites@0.1.4-next.2 + - @backstage/plugin-search@1.1.0-next.2 + - @backstage/plugin-lighthouse@0.4.0-next.2 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-airbrake@0.3.15-next.2 + - @backstage/plugin-apache-airflow@0.2.8-next.1 + - @backstage/plugin-azure-devops@0.2.6-next.2 + - @backstage/plugin-badges@0.2.39-next.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-graph@0.2.27-next.2 + - @backstage/plugin-catalog-import@0.9.5-next.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-cloudbuild@0.3.15-next.2 + - @backstage/plugin-code-coverage@0.2.8-next.2 + - @backstage/plugin-cost-insights@0.12.4-next.2 + - @backstage/plugin-dynatrace@2.0.0-next.2 + - @backstage/plugin-gcalendar@0.3.11-next.1 + - @backstage/plugin-gcp-projects@0.3.34-next.1 + - @backstage/plugin-github-actions@0.5.15-next.2 + - @backstage/plugin-gocd@0.1.21-next.2 + - @backstage/plugin-graphiql@0.2.47-next.1 + - @backstage/plugin-home@0.4.31-next.2 + - @backstage/plugin-jenkins@0.7.14-next.2 + - @backstage/plugin-kafka@0.3.15-next.2 + - @backstage/plugin-kubernetes@0.7.8-next.2 + - @backstage/plugin-newrelic@0.3.33-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.8-next.2 + - @backstage/plugin-org@0.6.5-next.2 + - @backstage/plugin-pagerduty@0.5.8-next.2 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist@0.1.6-next.2 + - @backstage/plugin-rollbar@0.4.15-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-shortcuts@0.3.7-next.1 + - @backstage/plugin-stack-overflow@0.1.11-next.2 + - @backstage/plugin-tech-insights@0.3.7-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.2 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + - @backstage/plugin-todo@0.2.17-next.2 + - @backstage/plugin-user-settings@0.6.3-next.2 + - @internal/plugin-catalog-customized@0.0.7-next.2 + +## example-backend@0.2.80-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-backend@0.1.0-next.0 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/plugin-search-backend@1.2.3-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-events-backend@0.2.3-next.2 + - @backstage/plugin-kafka-backend@0.2.35-next.2 + - @backstage/plugin-proxy-backend@0.2.36-next.2 + - @backstage/plugin-app-backend@0.3.42-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-kubernetes-backend@0.9.3-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-backend@0.17.5-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/plugin-playlist-backend@0.2.5-next.2 + - @backstage/plugin-rollbar-backend@0.1.39-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.3-next.2 + - @backstage/plugin-tech-insights-backend@0.5.8-next.2 + - @backstage/plugin-techdocs-backend@1.5.3-next.2 + - example-app@0.2.80-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-backend@0.2.7-next.2 + - @backstage/plugin-azure-devops-backend@0.3.21-next.2 + - @backstage/plugin-azure-sites-backend@0.1.4-next.2 + - @backstage/plugin-badges-backend@0.1.36-next.2 + - @backstage/plugin-code-coverage-backend@0.2.8-next.2 + - @backstage/plugin-explore-backend@0.0.4-next.2 + - @backstage/plugin-graphql-backend@0.1.32-next.2 + - @backstage/plugin-jenkins-backend@0.1.32-next.2 + - @backstage/plugin-permission-backend@0.5.17-next.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.2 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.2 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + - @backstage/plugin-todo-backend@0.1.39-next.2 + +## example-backend-next@0.0.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-app-backend@0.3.42-next.2 + - @backstage/backend-defaults@0.1.7-next.2 + +## techdocs-cli-embedded-app@0.2.79-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + +## @internal/plugin-catalog-customized@0.0.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + +## @internal/plugin-todo-list@1.0.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + +## @internal/plugin-todo-list-backend@1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 diff --git a/docs/tutorials/configuring-plugin-databases.md b/docs/tutorials/configuring-plugin-databases.md index ed550f1801..34e62c285a 100644 --- a/docs/tutorials/configuring-plugin-databases.md +++ b/docs/tutorials/configuring-plugin-databases.md @@ -37,7 +37,7 @@ package. If you intend to use both PostgreSQL and SQLite, you can install both of them. ```bash -# From the Backstage root directory +# From your Backstage root directory # install pg if you need PostgreSQL yarn add --cwd packages/backend pg diff --git a/microsite-next/yarn.lock b/microsite-next/yarn.lock index 5f27b0f062..251ae303c0 100644 --- a/microsite-next/yarn.lock +++ b/microsite-next/yarn.lock @@ -2382,9 +2382,9 @@ __metadata: linkType: hard "@sideway/formula@npm:^3.0.0": - version: 3.0.0 - resolution: "@sideway/formula@npm:3.0.0" - checksum: 8ae26a0ed6bc84f7310be6aae6eb9d81e97f382619fc69025d346871a707eaab0fa38b8c857e3f0c35a19923de129f42d35c50b8010c928d64aab41578580ec4 + version: 3.0.1 + resolution: "@sideway/formula@npm:3.0.1" + checksum: e4beeebc9dbe2ff4ef0def15cec0165e00d1612e3d7cea0bc9ce5175c3263fc2c818b679bd558957f49400ee7be9d4e5ac90487e1625b4932e15c4aa7919c57a languageName: node linkType: hard @@ -6051,9 +6051,9 @@ __metadata: linkType: hard "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 languageName: node linkType: hard @@ -8361,11 +8361,11 @@ __metadata: linkType: hard "prettier@npm:^2.6.2": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.4 + resolution: "prettier@npm:2.8.4" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e languageName: node linkType: hard diff --git a/microsite/blog/2023-01-31-incremental-entity-provider.md b/microsite/blog/2023-01-31-incremental-entity-provider.md index 64ac69c7a0..fd273af208 100644 --- a/microsite/blog/2023-01-31-incremental-entity-provider.md +++ b/microsite/blog/2023-01-31-incremental-entity-provider.md @@ -4,11 +4,11 @@ author: Paul Cowan & Taras Mankovski authorURL: https://frontside.com/ --- -# Scaling Backstage Ingestion with Incremental Entity Providers +At the heart of [Backstage](https://backstage.io/) is the [Backstage Software Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview), which is a data store that allows an organization to centralize and visualize its many software services and components. Backstage inspects and transforms an organization's disparate software services and parts into a centralized data store. This blog post introduces the concept of incremental entity providers, which allow Backstage to scale ingestion to even larger datasets. -At the heart of [Backstage](https://backstage.io/) is the [Backstage Software Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview), which is a data store that allows an organization to centralize and visualize its many software services and components. Backstage inspects and transforms an organization's disparate software services and parts into a centralized data store. +![catalog pipeline](assets/2023-01-31/catalog-pipeline.png) -![catalog pipeline](./assets/2023-01-31/catalog-pipeline.png) + A common use case is for an organization to want to surface ownership and metadata about repositories. Backstage provides a mechanism for discovering and transforming repository information into a standard data structure and persisting it into the Backstage [Catalog](https://backstage.io/docs/features/software-catalog/software-catalog-overview). This process is known as ingestion, where all data is transformed into a standard Backstage data structure known as an entity. Entities in the Catalog’s data store are accessible to the Backstage App via the REST API. @@ -28,9 +28,9 @@ A large organization typically deals with massive datasets. Until recently, inge This is a problem that [DevEx team at HP](http://hp.com) faced when building their software catalog with Backstage. [Damon Kaswell](https://github.com/dekoding), Senior Application Developer on the DevEx team at HP, shared their experience at [BackstageCon 2022](https://www.youtube.com/watch?v=5qHyZntKXRU&list=PLj6h78yzYM2OKySsTuiip3BqmdYZQRnSf&index=13), detailing the problem and the solution that [Frontside](https://frontside.com/) created in collaboration with developers on HP’s DevEx team. -![Damon Kaswell](./assets/2023-01-31/damon.jpg) +![Damon Kaswell](assets/2023-01-31/damon.jpg) -The solution HP and [Frontside](https://frontside.com/) arrived at was to implement an incremental entity provider. An incremental entity provider effectively performs a full mutation using a series of delta mutations combined with a mark and sweep mechanism. It paginates through the dataset, tracking entities retrieved from each page and the cursor of the next page, pausing ingestion every few minutes to give the processing loop time to process existing entities. Once it reaches the end of the dataset, it determines which entities were not ingested during this ingestion cycle and emits a delta mutation to delete unmarked entities. +The solution HP and [Frontside](https://frontside.com/) arrived at was to implement an incremental entity provider. An incremental entity provider effectively performs a full mutation using a series of delta mutations combined with a mark and sweep mechanism. It paginates through the dataset, tracking entities retrieved from each page and the cursor of the next page, pausing ingestion every few seconds to give the processing loop time to process existing entities. Once it reaches the end of the dataset, it determines which entities were not ingested during this ingestion cycle and emits a delta mutation to delete unmarked entities. Simply by adding a few new tables to the database schema, the incremental ingestion entity provider converts any existing entity provider into an incremental entity provider. These tables allow the incremental entity provider to be long-lived and keep track of its current location in the dataset by persisting a cursor that it uses to page through any large dataset. The larger the dataset, the more pages of data or bursts of work the incremental entity provider will ingest—but there will be no out-of-memory errors, effectively removing scalability problems. diff --git a/microsite/blog/2023-02-06-whats-ahead-in-2023.md b/microsite/blog/2023-02-06-whats-ahead-in-2023.md new file mode 100644 index 0000000000..f989973ca0 --- /dev/null +++ b/microsite/blog/2023-02-06-whats-ahead-in-2023.md @@ -0,0 +1,60 @@ +--- +title: What’s Ahead in 2023? +author: Lee Mills +authorURL: https://github.com/leemills83 +--- + +# What’s Ahead in 2023? + +![Fewer “vibes” and more climbs (up the contribution ladder)](assets/2023-02-06/whats-ahead.png) + +Okay, everyone. Time to move on from all the 2022 reflections, Top 10 lists, and Wrapped(s), and turn our gaze in the other direction — towards 2023. As the Backstage team moves into the new year, we find ourselves asking those perennial questions: How can we continue to grow and enhance the Backstage community? How can we improve the experience for those adopting Backstage? How can we give the best support to Backstage contributors? + + + +Well, we don’t have all the answers, but we do have some. So we wanted to share more on three ongoing initiatives aimed at moving the needle forward for these three distinct, yet interrelated, questions. + +- Grow your influence by climbing the contribution ladder +- Come join our SIGs (and push for new ones) +- Work with us to drive a better experience for Backstage adopters + +# Climb the contribution ladder + +Truth be told, we’ve been wanting to grow the maintainer team for some time, and have had multiple requests from contributors and contributing organizations wanting to become maintainers. But, so far, we just haven’t managed to get anything off the ground. What we see is that becoming a maintainer is too hard and the current ownership model doesn’t work. In short, we need to update Backstage governance. + +To address this, we are proposing (RFC links below) the following three initiatives: + +- Introduce more steps in between first-time contributor and maintainer in the form of a contributor ladder (loosely based on the [CNCF Project Template](https://github.com/cncf/project-template/blob/main/CONTRIBUTOR_LADDER.md)). The ladder might look as follows: Contributor, Org Member, Reviewer, Maintainer, Project Maintainer. +- Change the ownership model, for example, divide the project up into several project areas, each covering particular parts of the project. +- Introduce a new process for large contributions in the form of Backstage Enhancement Proposals (BEPs). + +Read more and join the already rich discussion in [RFC: Updated Backstage Governance](https://github.com/backstage/backstage/issues/15317) and [RFC: Backstage Enhancement Proposals](https://github.com/backstage/backstage/issues/15318). The RFC will be open until 13/02/2023. + +# Come join our SIGs (and push for new ones) + +As many of you know, SIG stands for Special Interest Group. The purpose of a SIG is to bring together maintainers and interested contributors within a certain specialist area to discuss, debate, spread knowledge, brainstorm - and drive the overall project forward. They’re also a lot of fun! + +We currently have two SIGs up and running: + +- [Catalog](https://github.com/backstage/community/tree/main/sigs/sig-catalog?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9fx-nTmQga2j12NqzZaL-lYPq-xQjDoXcWKe_JUoa2RGlK65R9j0CIygfYILr0tGEQSvRVN9xzhiJ6LmVxBthKnStfSA&utm_content=241130993&utm_source=hs_email) started up last June and covers all aspects of the systems and components related to the Software Catalog. The group runs every second Tuesday between 16:00 and 17:00 CET. Just show up or hop into [#catalog](https://discord.com/channels/687207715902193673/923144214580191282) (Discord), if you want to talk to us first. Here are the [notes from our sessions so far](https://docs.google.com/document/d/1xg57CDfX2R5jTGbbWANzifm2szEb664kuTwfEEvKgZU/edit#heading=h.iw0hjv6xfb9e). +- [Adoption](https://github.com/backstage/community/tree/main/sigs/sig-adoption?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9CAxtT8JSeJZPh41o3ZnDBi0VBFpbL6fEqXInr9HHOFhU3JAo8HekKg1JzE5rhGP5zOaHcsYDd1YxJWNHw-eSk5mLWkA&utm_content=241130993&utm_source=hs_email) kicked off in December and covers all aspects of the Backstage adoption journey including documentation and tutorials. In the first two meetups, we discussed adoption challenges based on user persona, collecting data about those challenges, and bootstrapping the Catalog to improve metadata quality for adopters. This one runs every second Thursday between 16:00 and 17:00 CET. Again just show up or, if you want to talk to us first, you can do so in [#adoption](https://discord.com/channels/687207715902193673/1045031039061479536) (Discord). Here are the [notes from our sessions](https://docs.google.com/document/d/1zeYSzXYgh897bxPQmwy2btOwCFlEjlUDLf00riySLRc/edit?_hsmi=241130993&_hsenc=p2ANqtz-_nPuE5hW4fnbhJ5-uK8r5HisSTU7rkslKirvqu0g-vdynbitPVYF2AVLAatos2dLoxb2Bgx_g4LBgH02kXoq7_Acd43Q#heading=h.iw0hjv6xfb9e) so far. + +So that’s SIGs. Get involved and join the ones we have up and running. And/or feel free to propose new ones. + +# Help drive a better experience for all adopters + +Throughout all stages of adopting Backstage in an organization — initial discovery, evaluation and POC, and scaling to the wider org — we have identified opportunities where we want to make the journey for Backstage adopters faster and smoother. + +We’ll go step-by-step, but initial plans are to: + +- Make it easier to install, customize, and maintain Backstage +- Provide Golden Paths for adoption +- Improve the experience and the content on [backstage.io](https://backstage.io/docs/overview/what-is-backstage) + +As we move forward, we’d love you to be part of it. Here are some ways that you can contribute to this initiative: + +- Simply give us feedback. [Tell us about your adoption journey](https://spotify.co1.qualtrics.com/jfe/form/SV_cuxhYPB6oQDu7gW). +- Contribute to the pages and documentation on [backstage.io](https://backstage.io/docs/overview/what-is-backstage) +- Pay it forward by making code contributions even during your adoption journey (if you are on one) +- Join the [Adoption](https://github.com/backstage/community/tree/main/sigs/sig-adoption?utm_campaign=Newsletters%20Sent&utm_medium=email&_hsmi=241130993&_hsenc=p2ANqtz-9CAxtT8JSeJZPh41o3ZnDBi0VBFpbL6fEqXInr9HHOFhU3JAo8HekKg1JzE5rhGP5zOaHcsYDd1YxJWNHw-eSk5mLWkA&utm_content=241130993&utm_source=hs_email) SIG +- Chat with us on [#adoption](https://discord.com/channels/687207715902193673/1045031039061479536) (Discord) diff --git a/microsite/blog/assets/2023-02-06/whats-ahead.png b/microsite/blog/assets/2023-02-06/whats-ahead.png new file mode 100644 index 0000000000..e49f68149d Binary files /dev/null and b/microsite/blog/assets/2023-02-06/whats-ahead.png differ diff --git a/microsite/data/plugins/entity-feedback.yaml b/microsite/data/plugins/entity-feedback.yaml new file mode 100644 index 0000000000..c40d0450ca --- /dev/null +++ b/microsite/data/plugins/entity-feedback.yaml @@ -0,0 +1,10 @@ +--- +title: Entity Feedback +author: Phil Kuang +authorUrl: https://github.com/kuangp +category: Quality +description: Give and view feedback on entities available in the Backstage catalog. +documentation: https://github.com/backstage/backstage/tree/master/plugins/entity-feedback +iconUrl: img/entity-feedback-logo.png +npmPackageName: '@backstage/plugin-entity-feedback' +addedDate: '2023-01-20' diff --git a/microsite/data/plugins/graphql-voyager.yaml b/microsite/data/plugins/graphql-voyager.yaml new file mode 100644 index 0000000000..c6ebf5e412 --- /dev/null +++ b/microsite/data/plugins/graphql-voyager.yaml @@ -0,0 +1,13 @@ +--- +title: GraphQL Voyager +author: PostNL +authorUrl: https://github.com/postnl +category: Debugging +description: Integrates the GraphQL Voyager tool inside Backstage as a plugin. +documentation: https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager +iconUrl: https://res.cloudinary.com/apideck/image/upload/v1612724234/icons/graphql-voyager.png +npmPackageName: '@backstage/plugin-graphql-voyager' +tags: + - graphql + - graphql-voyager +addedDate: '2023-01-27' diff --git a/microsite/data/plugins/kubevela.yaml b/microsite/data/plugins/kubevela.yaml new file mode 100644 index 0000000000..524a046025 --- /dev/null +++ b/microsite/data/plugins/kubevela.yaml @@ -0,0 +1,15 @@ +--- +title: KubeVela +author: Charlie Chiang +authorUrl: https://github.com/charlie0129 +category: App Delivery +description: KubeVela is a modern software delivery platform that makes deploying and operating applications across today's hybrid, multi-cloud environments easier, faster and more reliable. +documentation: https://github.com/kubevela-contrib/backstage-plugin-kubevela +iconUrl: https://kubevela.io/img/logo.svg +npmPackageName: '@oamdev/plugin-kubevela-backend' +tags: + - cd + - delivery + - multi-cluster + - deployment as code +addedDate: '2023-02-12' diff --git a/microsite/data/plugins/vault.yaml b/microsite/data/plugins/vault.yaml index 822f5dcc90..bb8f799ba7 100644 --- a/microsite/data/plugins/vault.yaml +++ b/microsite/data/plugins/vault.yaml @@ -3,7 +3,7 @@ title: Vault author: Spread Group authorUrl: https://github.com/ivangonzalezacuna category: Vault -description: Visualize a list secrets stored in your vault instance. +description: Visualize a list of the secrets stored in your HashiCorp Vault instance. documentation: https://github.com/backstage/backstage/tree/master/plugins/vault iconUrl: img/vault.png npmPackageName: '@backstage/plugin-vault' diff --git a/microsite/sidebars.json b/microsite/sidebars.json index fcded5ebfb..b7e2068aca 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -87,7 +87,8 @@ "features/kubernetes/installation", "features/kubernetes/configuration", "features/kubernetes/authentication", - "features/kubernetes/troubleshooting" + "features/kubernetes/troubleshooting", + "features/kubernetes/proxy" ] }, { @@ -103,6 +104,7 @@ "features/software-templates/writing-custom-actions", "features/software-templates/writing-custom-field-extensions", "features/software-templates/writing-custom-step-layouts", + "features/software-templates/testing-scaffolder-alpha", "features/software-templates/migrating-from-v1beta2-to-v1beta3" ] }, @@ -365,44 +367,40 @@ ], "FAQ": ["FAQ"], "Experimental Backend System": [ + "backend-system/index", { - "_hidden_": [ - "backend-system/index", - { - "type": "subcategory", - "label": "Architecture", - "ids": [ - "backend-system/architecture/index", - "backend-system/architecture/services", - "backend-system/architecture/plugins", - "backend-system/architecture/extension-points", - "backend-system/architecture/modules", - "backend-system/architecture/naming-patterns" - ] - }, - { - "type": "subcategory", - "label": "Building Backends", - "ids": [ - "backend-system/building-backends/index", - "backend-system/building-backends/migrating" - ] - }, - { - "type": "subcategory", - "label": "Building Plugins & Modules", - "ids": [ - "backend-system/building-plugins-and-modules/index", - "backend-system/building-plugins-and-modules/testing", - "backend-system/building-plugins-and-modules/migrating" - ] - }, - { - "type": "subcategory", - "label": "Core Services", - "ids": ["backend-system/core-services/index"] - } + "type": "subcategory", + "label": "Architecture", + "ids": [ + "backend-system/architecture/index", + "backend-system/architecture/services", + "backend-system/architecture/plugins", + "backend-system/architecture/extension-points", + "backend-system/architecture/modules", + "backend-system/architecture/naming-patterns" ] + }, + { + "type": "subcategory", + "label": "Building Backends", + "ids": [ + "backend-system/building-backends/index", + "backend-system/building-backends/migrating" + ] + }, + { + "type": "subcategory", + "label": "Building Plugins & Modules", + "ids": [ + "backend-system/building-plugins-and-modules/index", + "backend-system/building-plugins-and-modules/testing", + "backend-system/building-plugins-and-modules/migrating" + ] + }, + { + "type": "subcategory", + "label": "Core Services", + "ids": ["backend-system/core-services/index"] } ] } diff --git a/microsite/static/img/entity-feedback-logo.png b/microsite/static/img/entity-feedback-logo.png new file mode 100644 index 0000000000..e172b260a0 Binary files /dev/null and b/microsite/static/img/entity-feedback-logo.png differ diff --git a/microsite/yarn.lock b/microsite/yarn.lock index 3bc81619e3..95b0c2885b 100644 --- a/microsite/yarn.lock +++ b/microsite/yarn.lock @@ -7369,11 +7369,11 @@ __metadata: linkType: hard "prettier@npm:^2.6.2": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.4 + resolution: "prettier@npm:2.8.4" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e languageName: node linkType: hard diff --git a/package.json b/package.json index 912dd53b8a..286e750f29 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "create-plugin": "echo \"use 'yarn new' instead\"", "release": "node scripts/prepare-release.js && changeset version && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' '.changeset/*.json' && yarn install --no-immutable", "prettier:check": "prettier --check .", + "prettier:fix": "prettier --write .", "storybook": "yarn ./storybook run start", "snyk:test": "npx snyk test --yarn-workspaces --strict-out-of-sync=false", "snyk:test:package": "yarn snyk:test --include", @@ -46,7 +47,7 @@ "@types/react": "^17", "@types/react-dom": "^17" }, - "version": "1.11.0-next.1", + "version": "1.11.0-next.2", "dependencies": { "@backstage/errors": "workspace:^", "@manypkg/get-packages": "^1.1.3" diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md index ecd6c448d5..e785ffe05a 100644 --- a/packages/app-defaults/CHANGELOG.md +++ b/packages/app-defaults/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/app-defaults +## 1.1.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-permission-react@0.4.9 + ## 1.1.1-next.0 ### Patch Changes diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json index 793baff253..8ffdfd60bc 100644 --- a/packages/app-defaults/package.json +++ b/packages/app-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/app-defaults", "description": "Provides the default wiring of a Backstage App", - "version": "1.1.1-next.0", + "version": "1.1.1-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts index 3f5cfc1c58..d3ebddfb84 100644 --- a/packages/app-defaults/src/defaults/apis.ts +++ b/packages/app-defaults/src/defaults/apis.ts @@ -25,6 +25,7 @@ import { GitlabAuth, MicrosoftAuth, BitbucketAuth, + BitbucketServerAuth, OAuthRequestManager, WebStorage, UrlPatternDiscovery, @@ -53,6 +54,7 @@ import { configApiRef, oneloginAuthApiRef, bitbucketAuthApiRef, + bitbucketServerAuthApiRef, atlassianAuthApiRef, } from '@backstage/core-plugin-api'; import { @@ -219,6 +221,19 @@ export const apis = [ environment: configApi.getOptionalString('auth.environment'), }), }), + createApiFactory({ + api: bitbucketServerAuthApiRef, + deps: { + discoveryApi: discoveryApiRef, + oauthRequestApi: oauthRequestApiRef, + }, + factory: ({ discoveryApi, oauthRequestApi }) => + BitbucketServerAuth.create({ + discoveryApi, + oauthRequestApi, + defaultScopes: ['REPO_READ'], + }), + }), createApiFactory({ api: atlassianAuthApiRef, deps: { diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index a541927bf4..818d21840d 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,69 @@ # example-app +## 0.2.80-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.2 + - @backstage/plugin-scaffolder@1.11.0-next.2 + - @backstage/cli@0.22.2-next.1 + - @backstage/plugin-api-docs@0.9.0-next.2 + - @backstage/plugin-tech-radar@0.6.1-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-explore@0.4.0-next.2 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/plugin-sentry@0.5.0-next.2 + - @backstage/plugin-circleci@0.3.15-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/plugin-azure-sites@0.1.4-next.2 + - @backstage/plugin-search@1.1.0-next.2 + - @backstage/plugin-lighthouse@0.4.0-next.2 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-airbrake@0.3.15-next.2 + - @backstage/plugin-apache-airflow@0.2.8-next.1 + - @backstage/plugin-azure-devops@0.2.6-next.2 + - @backstage/plugin-badges@0.2.39-next.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-graph@0.2.27-next.2 + - @backstage/plugin-catalog-import@0.9.5-next.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-cloudbuild@0.3.15-next.2 + - @backstage/plugin-code-coverage@0.2.8-next.2 + - @backstage/plugin-cost-insights@0.12.4-next.2 + - @backstage/plugin-dynatrace@2.0.0-next.2 + - @backstage/plugin-gcalendar@0.3.11-next.1 + - @backstage/plugin-gcp-projects@0.3.34-next.1 + - @backstage/plugin-github-actions@0.5.15-next.2 + - @backstage/plugin-gocd@0.1.21-next.2 + - @backstage/plugin-graphiql@0.2.47-next.1 + - @backstage/plugin-home@0.4.31-next.2 + - @backstage/plugin-jenkins@0.7.14-next.2 + - @backstage/plugin-kafka@0.3.15-next.2 + - @backstage/plugin-kubernetes@0.7.8-next.2 + - @backstage/plugin-newrelic@0.3.33-next.1 + - @backstage/plugin-newrelic-dashboard@0.2.8-next.2 + - @backstage/plugin-org@0.6.5-next.2 + - @backstage/plugin-pagerduty@0.5.8-next.2 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist@0.1.6-next.2 + - @backstage/plugin-rollbar@0.4.15-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-shortcuts@0.3.7-next.1 + - @backstage/plugin-stack-overflow@0.1.11-next.2 + - @backstage/plugin-tech-insights@0.3.7-next.2 + - @backstage/plugin-techdocs-module-addons-contrib@1.0.10-next.2 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + - @backstage/plugin-todo@0.2.17-next.2 + - @backstage/plugin-user-settings@0.6.3-next.2 + - @internal/plugin-catalog-customized@0.0.7-next.2 + ## 0.2.80-next.1 ### Patch Changes diff --git a/packages/app/cypress/.eslintrc.json b/packages/app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/app/cypress/.eslintrc.json +++ b/packages/app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/app/package.json b/packages/app/package.json index f6631ed05b..226e48f548 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,6 +1,6 @@ { "name": "example-app", - "version": "0.2.80-next.1", + "version": "0.2.80-next.2", "private": true, "backstage": { "role": "frontend" @@ -30,6 +30,7 @@ "@backstage/plugin-code-coverage": "workspace:^", "@backstage/plugin-cost-insights": "workspace:^", "@backstage/plugin-dynatrace": "workspace:^", + "@backstage/plugin-entity-feedback": "workspace:^", "@backstage/plugin-explore": "workspace:^", "@backstage/plugin-gcalendar": "workspace:^", "@backstage/plugin-gcp-projects": "workspace:^", @@ -41,6 +42,9 @@ "@backstage/plugin-kafka": "workspace:^", "@backstage/plugin-kubernetes": "workspace:^", "@backstage/plugin-lighthouse": "workspace:^", + "@backstage/plugin-linguist": "workspace:^", + "@backstage/plugin-linguist-common": "workspace:^", + "@backstage/plugin-microsoft-calendar": "workspace:^", "@backstage/plugin-newrelic": "workspace:^", "@backstage/plugin-newrelic-dashboard": "workspace:^", "@backstage/plugin-org": "workspace:^", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 8984a69259..f789328104 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -78,6 +78,7 @@ import { ExpandableNavigation, ReportIssue, TextSize, + LightBox, } from '@backstage/plugin-techdocs-module-addons-contrib'; import { SettingsLayout, @@ -203,6 +204,7 @@ const routes = ( + + ); @@ -374,6 +385,12 @@ const overviewContent = ( + + + + + + {cicdCard} @@ -404,6 +421,14 @@ const overviewContent = ( + + + + + + + + @@ -511,6 +536,10 @@ const serviceEntityPage = ( > + + + + ); @@ -590,6 +619,10 @@ const websiteEntityPage = ( + + + + ); @@ -606,6 +639,10 @@ const defaultEntityPage = ( + + + + ); @@ -642,6 +679,11 @@ const apiPage = ( + + + + + @@ -654,6 +696,10 @@ const apiPage = ( + + + + ); @@ -671,6 +717,9 @@ const userPage = ( entityFilterKind={customEntityFilterKind} /> + + + @@ -693,6 +742,9 @@ const groupPage = ( + + + @@ -718,6 +770,11 @@ const systemPage = ( + + + + + @@ -746,6 +803,9 @@ const systemPage = ( unidirectional={false} /> + + + ); @@ -763,8 +823,16 @@ const domainPage = ( + + + + + + + + ); diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx index e432e1a00e..29023738af 100644 --- a/packages/app/src/components/home/HomePage.tsx +++ b/packages/app/src/components/home/HomePage.tsx @@ -27,6 +27,7 @@ import { import { Content, Header, Page } from '@backstage/core-components'; import { HomePageSearchBar } from '@backstage/plugin-search'; import { HomePageCalendar } from '@backstage/plugin-gcalendar'; +import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar'; import Grid from '@material-ui/core/Grid'; import React from 'react'; @@ -114,6 +115,9 @@ export const homePage = ( + + + diff --git a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx index c521bcf86c..8a2e7fba53 100644 --- a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx +++ b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx @@ -68,7 +68,7 @@ export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide( const MockDelayComponent = ( props: NextFieldExtensionComponentProps<{ test?: string }>, ) => { - const { onChange, formData, rawErrors } = props; + const { onChange, formData, rawErrors = [] } = props; return ( void }) => { const classes = useStyles(); const navigate = useNavigate(); - const { transitions } = useTheme(); - const { focusContent } = useContent(); - const catalogApi = useApi(catalogApiRef); const { term, types } = useSearch(); @@ -91,19 +82,14 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { searchBarRef?.current?.focus(); }); - const handleSearchResultClick = useCallback(() => { - toggleModal(); - setTimeout(focusContent, transitions.duration.leavingScreen); - }, [toggleModal, focusContent, transitions]); - const handleSearchBarKeyDown = useCallback( (e: KeyboardEvent) => { if (e.key === 'Enter') { - handleSearchResultClick(); + toggleModal(); navigate(searchPagePath); } }, - [navigate, searchPagePath, handleSearchResultClick], + [navigate, searchPagePath, toggleModal], ); return ( @@ -187,7 +173,7 @@ export const SearchModal = ({ toggleModal }: { toggleModal: () => void }) => { alignItems="center" > - + void }) => { - + } /> } /> } /> diff --git a/packages/app/src/components/search/SearchPage.tsx b/packages/app/src/components/search/SearchPage.tsx index 8167889b02..6c4f06aed9 100644 --- a/packages/app/src/components/search/SearchPage.tsx +++ b/packages/app/src/components/search/SearchPage.tsx @@ -79,6 +79,7 @@ const SearchPage = () => { import { BackendFeature } from '@backstage/backend-plugin-api'; +import { CacheClient } from '@backstage/backend-common'; import { Config } from '@backstage/config'; import { ConfigService } from '@backstage/backend-plugin-api'; import { CorsOptions } from 'cors'; import { ErrorRequestHandler } from 'express'; import { Express as Express_2 } from 'express'; -import { ExtensionPoint } from '@backstage/backend-plugin-api'; import { Format } from 'logform'; import { Handler } from 'express'; import { HelmetOptions } from 'helmet'; import * as http from 'http'; import { HttpRouterService } from '@backstage/backend-plugin-api'; import { IdentityService } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; import { LoggerService } from '@backstage/backend-plugin-api'; -import { LogMeta } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; -import { PluginCacheManager } from '@backstage/backend-common'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; import { RequestHandler } from 'express'; @@ -34,7 +33,6 @@ import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceFactoryOrFunction } from '@backstage/backend-plugin-api'; -import { ServiceRef } from '@backstage/backend-plugin-api'; import { TokenManagerService } from '@backstage/backend-plugin-api'; import { transport } from 'winston'; import { UrlReader } from '@backstage/backend-common'; @@ -50,10 +48,7 @@ export interface Backend { } // @public (undocumented) -export const cacheFactory: () => ServiceFactory; - -// @public (undocumented) -export const configFactory: () => ServiceFactory; +export const cacheServiceFactory: () => ServiceFactory; // @public (undocumented) export interface ConfigFactoryOptions { @@ -61,6 +56,11 @@ export interface ConfigFactoryOptions { remote?: LoadConfigOptionsRemote; } +// @public (undocumented) +export const configServiceFactory: ( + options?: ConfigFactoryOptions | undefined, +) => ServiceFactory; + // @public (undocumented) export function createConfigSecretEnumerator(options: { logger: LoggerService; @@ -88,7 +88,10 @@ export interface CreateSpecializedBackendOptions { } // @public (undocumented) -export const databaseFactory: () => ServiceFactory; +export const databaseServiceFactory: () => ServiceFactory< + PluginDatabaseManager, + 'plugin' +>; // @public export class DefaultRootHttpRouter implements RootHttpRouterService { @@ -106,7 +109,10 @@ export interface DefaultRootHttpRouterOptions { } // @public (undocumented) -export const discoveryFactory: () => ServiceFactory; +export const discoveryServiceFactory: () => ServiceFactory< + PluginEndpointDiscovery, + 'plugin' +>; // @public export interface ExtendedHttpServer extends http.Server { @@ -118,20 +124,20 @@ export interface ExtendedHttpServer extends http.Server { stop(): Promise; } -// @public (undocumented) -export const httpRouterFactory: ( - options?: HttpRouterFactoryOptions | undefined, -) => ServiceFactory; - // @public (undocumented) export interface HttpRouterFactoryOptions { getPath?(pluginId: string): string; } +// @public (undocumented) +export const httpRouterServiceFactory: ( + options?: HttpRouterFactoryOptions | undefined, +) => ServiceFactory; + // @public export type HttpServerCertificateOptions = | { - type: 'plain'; + type: 'pem'; key: string; cert: string; } @@ -151,19 +157,22 @@ export type HttpServerOptions = { }; }; -// @public (undocumented) -export const identityFactory: ( - options?: IdentityFactoryOptions | undefined, -) => ServiceFactory; - // @public export type IdentityFactoryOptions = { issuer?: string; algorithms?: string[]; }; +// @public (undocumented) +export const identityServiceFactory: ( + options?: IdentityFactoryOptions | undefined, +) => ServiceFactory; + // @public -export const lifecycleFactory: () => ServiceFactory; +export const lifecycleServiceFactory: () => ServiceFactory< + LifecycleService, + 'plugin' +>; // @public export function loadBackendConfig(options: { @@ -174,7 +183,10 @@ export function loadBackendConfig(options: { }>; // @public (undocumented) -export const loggerFactory: () => ServiceFactory; +export const loggerServiceFactory: () => ServiceFactory< + LoggerService, + 'plugin' +>; // @public export class MiddlewareFactory { @@ -202,7 +214,10 @@ export interface MiddlewareFactoryOptions { } // @public (undocumented) -export const permissionsFactory: () => ServiceFactory; +export const permissionsServiceFactory: () => ServiceFactory< + PermissionsService, + 'plugin' +>; // @public export function readCorsOptions(config?: Config): CorsOptions; @@ -214,7 +229,7 @@ export function readHelmetOptions(config?: Config): HelmetOptions; export function readHttpServerOptions(config?: Config): HttpServerOptions; // @public (undocumented) -export interface RootHttpRouterConfigureOptions { +export interface RootHttpRouterConfigureContext { // (undocumented) app: Express_2; // (undocumented) @@ -229,55 +244,64 @@ export interface RootHttpRouterConfigureOptions { routes: RequestHandler; } -// @public (undocumented) -export const rootHttpRouterFactory: () => ServiceFactory; - // @public (undocumented) export type RootHttpRouterFactoryOptions = { indexPath?: string | false; - configure?(options: RootHttpRouterConfigureOptions): void; + configure?(context: RootHttpRouterConfigureContext): void; }; +// @public (undocumented) +export const rootHttpRouterServiceFactory: ( + options?: RootHttpRouterFactoryOptions | undefined, +) => ServiceFactory; + // @public -export const rootLifecycleFactory: () => ServiceFactory; +export const rootLifecycleServiceFactory: () => ServiceFactory< + RootLifecycleService, + 'root' +>; // @public (undocumented) -export const rootLoggerFactory: () => ServiceFactory; +export const rootLoggerServiceFactory: () => ServiceFactory< + RootLoggerService, + 'root' +>; // @public (undocumented) -export const schedulerFactory: () => ServiceFactory; +export const schedulerServiceFactory: () => ServiceFactory< + SchedulerService, + 'plugin' +>; // @public (undocumented) -export type ServiceOrExtensionPoint = - | ExtensionPoint - | ServiceRef; +export const tokenManagerServiceFactory: () => ServiceFactory< + TokenManagerService, + 'plugin' +>; // @public (undocumented) -export const tokenManagerFactory: () => ServiceFactory; - -// @public (undocumented) -export const urlReaderFactory: () => ServiceFactory; +export const urlReaderServiceFactory: () => ServiceFactory; // @public export class WinstonLogger implements RootLoggerService { // (undocumented) addRedactions(redactions: Iterable): void; // (undocumented) - child(meta: LogMeta): LoggerService; + child(meta: JsonObject): LoggerService; static colorFormat(): Format; static create(options: WinstonLoggerOptions): WinstonLogger; // (undocumented) - debug(message: string, meta?: LogMeta): void; + debug(message: string, meta?: JsonObject): void; // (undocumented) - error(message: string, meta?: LogMeta): void; + error(message: string, meta?: JsonObject): void; // (undocumented) - info(message: string, meta?: LogMeta): void; + info(message: string, meta?: JsonObject): void; static redacter(): { format: Format; add: (redactions: Iterable) => void; }; // (undocumented) - warn(message: string, meta?: LogMeta): void; + warn(message: string, meta?: JsonObject): void; } // @public (undocumented) @@ -287,7 +311,7 @@ export interface WinstonLoggerOptions { // (undocumented) level: string; // (undocumented) - meta?: LogMeta; + meta?: JsonObject; // (undocumented) transports: transport[]; } diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json index 0bd4eab227..3bbae1f7fe 100644 --- a/packages/backend-app-api/package.json +++ b/packages/backend-app-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-app-api", "description": "Core API used by Backstage backend apps", - "version": "0.3.2-next.1", + "version": "0.4.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-app-api/src/http/config.test.ts b/packages/backend-app-api/src/http/config.test.ts index f54ba927ca..577bae08f3 100644 --- a/packages/backend-app-api/src/http/config.test.ts +++ b/packages/backend-app-api/src/http/config.test.ts @@ -53,7 +53,7 @@ describe('readHttpServerOptions', () => { { listen: { host: '', port: 7007 }, https: { - certificate: { type: 'plain', cert: 'my-cert', key: 'my-key' }, + certificate: { type: 'pem', cert: 'my-cert', key: 'my-key' }, }, }, ], diff --git a/packages/backend-app-api/src/http/config.ts b/packages/backend-app-api/src/http/config.ts index e3b41c63de..ec29a8a044 100644 --- a/packages/backend-app-api/src/http/config.ts +++ b/packages/backend-app-api/src/http/config.ts @@ -93,7 +93,7 @@ function readHttpsOptions(config?: Config): HttpServerOptions['https'] { return { certificate: { - type: 'plain', + type: 'pem', cert: cc.getString('certificate.cert'), key: cc.getString('certificate.key'), }, diff --git a/packages/backend-app-api/src/http/types.ts b/packages/backend-app-api/src/http/types.ts index ff96f69bff..2298ef11ff 100644 --- a/packages/backend-app-api/src/http/types.ts +++ b/packages/backend-app-api/src/http/types.ts @@ -51,7 +51,7 @@ export type HttpServerOptions = { */ export type HttpServerCertificateOptions = | { - type: 'plain'; + type: 'pem'; key: string; cert: string; } diff --git a/packages/backend-app-api/src/logging/WinstonLogger.ts b/packages/backend-app-api/src/logging/WinstonLogger.ts index d32b816523..06a189508b 100644 --- a/packages/backend-app-api/src/logging/WinstonLogger.ts +++ b/packages/backend-app-api/src/logging/WinstonLogger.ts @@ -16,9 +16,9 @@ import { LoggerService, - LogMeta, RootLoggerService, } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; import { Format, TransformableInfo } from 'logform'; import { Logger, @@ -33,7 +33,7 @@ import { escapeRegExp } from '../lib/escapeRegExp'; * @public */ export interface WinstonLoggerOptions { - meta?: LogMeta; + meta?: JsonObject; level: string; format: Format; transports: Transport[]; @@ -150,23 +150,23 @@ export class WinstonLogger implements RootLoggerService { this.#addRedactions = addRedactions; } - error(message: string, meta?: LogMeta): void { + error(message: string, meta?: JsonObject): void { this.#winston.error(message, meta); } - warn(message: string, meta?: LogMeta): void { + warn(message: string, meta?: JsonObject): void { this.#winston.warn(message, meta); } - info(message: string, meta?: LogMeta): void { + info(message: string, meta?: JsonObject): void { this.#winston.info(message, meta); } - debug(message: string, meta?: LogMeta): void { + debug(message: string, meta?: JsonObject): void { this.#winston.debug(message, meta); } - child(meta: LogMeta): LoggerService { + child(meta: JsonObject): LoggerService { return new WinstonLogger(this.#winston.child(meta)); } diff --git a/packages/backend-app-api/src/services/implementations/cache/cacheFactory.ts b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts similarity index 89% rename from packages/backend-app-api/src/services/implementations/cache/cacheFactory.ts rename to packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts index 426a890c00..2208249280 100644 --- a/packages/backend-app-api/src/services/implementations/cache/cacheFactory.ts +++ b/packages/backend-app-api/src/services/implementations/cache/cacheServiceFactory.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-plugin-api'; /** @public */ -export const cacheFactory = createServiceFactory({ +export const cacheServiceFactory = createServiceFactory({ service: coreServices.cache, deps: { config: coreServices.config, @@ -31,6 +31,6 @@ export const cacheFactory = createServiceFactory({ return CacheManager.fromConfig(config); }, async factory({ plugin }, manager) { - return manager.forPlugin(plugin.getId()); + return manager.forPlugin(plugin.getId()).getClient(); }, }); diff --git a/packages/backend-app-api/src/services/implementations/cache/index.ts b/packages/backend-app-api/src/services/implementations/cache/index.ts index 3d1bcd75a7..f96ee77182 100644 --- a/packages/backend-app-api/src/services/implementations/cache/index.ts +++ b/packages/backend-app-api/src/services/implementations/cache/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { cacheFactory } from './cacheFactory'; +export { cacheServiceFactory } from './cacheServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/config/configFactory.ts b/packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts similarity index 76% rename from packages/backend-app-api/src/services/implementations/config/configFactory.ts rename to packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts index 0ec15c29fe..459c0d9f0c 100644 --- a/packages/backend-app-api/src/services/implementations/config/configFactory.ts +++ b/packages/backend-app-api/src/services/implementations/config/configServiceFactory.ts @@ -35,13 +35,14 @@ export interface ConfigFactoryOptions { } /** @public */ -export const configFactory = createServiceFactory({ - service: coreServices.config, - deps: {}, - async factory({}, options?: ConfigFactoryOptions) { - const { argv = process.argv, remote } = options ?? {}; - - const { config } = await loadBackendConfig({ argv, remote }); - return config; - }, -}); +export const configServiceFactory = createServiceFactory( + (options?: ConfigFactoryOptions) => ({ + service: coreServices.config, + deps: {}, + async factory({}) { + const { argv = process.argv, remote } = options ?? {}; + const { config } = await loadBackendConfig({ argv, remote }); + return config; + }, + }), +); diff --git a/packages/backend-app-api/src/services/implementations/config/index.ts b/packages/backend-app-api/src/services/implementations/config/index.ts index b23c47ce0a..690f1e2d36 100644 --- a/packages/backend-app-api/src/services/implementations/config/index.ts +++ b/packages/backend-app-api/src/services/implementations/config/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { configFactory } from './configFactory'; -export type { ConfigFactoryOptions } from './configFactory'; +export { configServiceFactory } from './configServiceFactory'; +export type { ConfigFactoryOptions } from './configServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/database/databaseFactory.ts b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts similarity index 79% rename from packages/backend-app-api/src/services/implementations/database/databaseFactory.ts rename to packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts index 8baf84cb69..05e2f1deb9 100644 --- a/packages/backend-app-api/src/services/implementations/database/databaseFactory.ts +++ b/packages/backend-app-api/src/services/implementations/database/databaseServiceFactory.ts @@ -22,11 +22,12 @@ import { import { ConfigReader } from '@backstage/config'; /** @public */ -export const databaseFactory = createServiceFactory({ +export const databaseServiceFactory = createServiceFactory({ service: coreServices.database, deps: { config: coreServices.config, - plugin: coreServices.pluginMetadata, + lifecycle: coreServices.lifecycle, + pluginMetadata: coreServices.pluginMetadata, }, async createRootContext({ config }) { return config.getOptional('backend.database') @@ -39,7 +40,10 @@ export const databaseFactory = createServiceFactory({ }), ); }, - async factory({ plugin }, databaseManager) { - return databaseManager.forPlugin(plugin.getId()); + async factory({ pluginMetadata, lifecycle }, databaseManager) { + return databaseManager.forPlugin(pluginMetadata.getId(), { + pluginMetadata, + lifecycle, + }); }, }); diff --git a/packages/backend-app-api/src/services/implementations/database/index.ts b/packages/backend-app-api/src/services/implementations/database/index.ts index 0045630e15..d676c8013e 100644 --- a/packages/backend-app-api/src/services/implementations/database/index.ts +++ b/packages/backend-app-api/src/services/implementations/database/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { databaseFactory } from './databaseFactory'; +export { databaseServiceFactory } from './databaseServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/discovery/discoveryFactory.ts b/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts similarity index 93% rename from packages/backend-app-api/src/services/implementations/discovery/discoveryFactory.ts rename to packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts index 51ea3c881d..6c0da8521d 100644 --- a/packages/backend-app-api/src/services/implementations/discovery/discoveryFactory.ts +++ b/packages/backend-app-api/src/services/implementations/discovery/discoveryServiceFactory.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-plugin-api'; /** @public */ -export const discoveryFactory = createServiceFactory({ +export const discoveryServiceFactory = createServiceFactory({ service: coreServices.discovery, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/services/implementations/discovery/index.ts b/packages/backend-app-api/src/services/implementations/discovery/index.ts index 182b9f6cd2..7b3b9816b1 100644 --- a/packages/backend-app-api/src/services/implementations/discovery/index.ts +++ b/packages/backend-app-api/src/services/implementations/discovery/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { discoveryFactory } from './discoveryFactory'; +export { discoveryServiceFactory } from './discoveryServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts similarity index 85% rename from packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts rename to packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts index 150bd628f5..e52b4c9a36 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.test.ts @@ -14,19 +14,12 @@ * limitations under the License. */ -import { - HttpRouterService, - ServiceFactory, -} from '@backstage/backend-plugin-api'; -import { httpRouterFactory } from './httpRouterFactory'; +import { httpRouterServiceFactory } from './httpRouterServiceFactory'; describe('httpRouterFactory', () => { it('should register plugin paths', async () => { const rootHttpRouter = { use: jest.fn() }; - const factory = httpRouterFactory() as Exclude< - ServiceFactory, - { scope: 'root' } - >; + const factory = httpRouterServiceFactory() as any; const handler1 = () => {}; const router1 = await factory.factory( @@ -55,9 +48,9 @@ describe('httpRouterFactory', () => { it('should use custom path generator', async () => { const rootHttpRouter = { use: jest.fn() }; - const factory = httpRouterFactory({ + const factory = httpRouterServiceFactory({ getPath: id => `/some/${id}/path`, - }) as Exclude, { scope: 'root' }>; + }) as any; const handler1 = () => {}; const router1 = await factory.factory( diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts similarity index 95% rename from packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts rename to packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts index 218910345d..bbd54b7ade 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/httpRouterServiceFactory.ts @@ -31,7 +31,7 @@ export interface HttpRouterFactoryOptions { } /** @public */ -export const httpRouterFactory = createServiceFactory( +export const httpRouterServiceFactory = createServiceFactory( (options?: HttpRouterFactoryOptions) => ({ service: coreServices.httpRouter, deps: { diff --git a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts index f2db660414..9dfb01c5be 100644 --- a/packages/backend-app-api/src/services/implementations/httpRouter/index.ts +++ b/packages/backend-app-api/src/services/implementations/httpRouter/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { httpRouterFactory } from './httpRouterFactory'; -export type { HttpRouterFactoryOptions } from './httpRouterFactory'; +export { httpRouterServiceFactory } from './httpRouterServiceFactory'; +export type { HttpRouterFactoryOptions } from './httpRouterServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/identity/identityFactory.ts b/packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts similarity index 95% rename from packages/backend-app-api/src/services/implementations/identity/identityFactory.ts rename to packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts index 0031979390..58da1c37d5 100644 --- a/packages/backend-app-api/src/services/implementations/identity/identityFactory.ts +++ b/packages/backend-app-api/src/services/implementations/identity/identityServiceFactory.ts @@ -34,7 +34,7 @@ export type IdentityFactoryOptions = { }; /** @public */ -export const identityFactory = createServiceFactory( +export const identityServiceFactory = createServiceFactory( (options?: IdentityFactoryOptions) => ({ service: coreServices.identity, deps: { diff --git a/packages/backend-app-api/src/services/implementations/identity/index.ts b/packages/backend-app-api/src/services/implementations/identity/index.ts index 9382232620..bf2f4436dd 100644 --- a/packages/backend-app-api/src/services/implementations/identity/index.ts +++ b/packages/backend-app-api/src/services/implementations/identity/index.ts @@ -14,5 +14,5 @@ * limitations under the License. */ -export { identityFactory } from './identityFactory'; -export type { IdentityFactoryOptions } from './identityFactory'; +export { identityServiceFactory } from './identityServiceFactory'; +export type { IdentityFactoryOptions } from './identityServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycle/index.ts b/packages/backend-app-api/src/services/implementations/lifecycle/index.ts index cfca37a162..8dac4c26b4 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycle/index.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycle/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { lifecycleFactory } from './lifecycleFactory'; +export { lifecycleServiceFactory } from './lifecycleServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts similarity index 76% rename from packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts rename to packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts index b2f3a9f70b..79658c0d42 100644 --- a/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleFactory.ts +++ b/packages/backend-app-api/src/services/implementations/lifecycle/lifecycleServiceFactory.ts @@ -17,12 +17,14 @@ import { createServiceFactory, coreServices, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, } from '@backstage/backend-plugin-api'; /** * Allows plugins to register shutdown hooks that are run when the process is about to exit. - * @public */ -export const lifecycleFactory = createServiceFactory({ + * @public + */ +export const lifecycleServiceFactory = createServiceFactory({ service: coreServices.lifecycle, deps: { logger: coreServices.logger, @@ -32,11 +34,12 @@ export const lifecycleFactory = createServiceFactory({ async factory({ rootLifecycle, logger, pluginMetadata }) { const plugin = pluginMetadata.getId(); return { - addShutdownHook(options: LifecycleServiceShutdownHook): void { - rootLifecycle.addShutdownHook({ - ...options, - - logger: options.logger?.child({ plugin }) ?? logger, + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + rootLifecycle.addShutdownHook(hook, { + logger: options?.logger?.child({ plugin }) ?? logger, }); }, }; diff --git a/packages/backend-app-api/src/services/implementations/logger/index.ts b/packages/backend-app-api/src/services/implementations/logger/index.ts index acef1b178f..ca52bf5193 100644 --- a/packages/backend-app-api/src/services/implementations/logger/index.ts +++ b/packages/backend-app-api/src/services/implementations/logger/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { loggerFactory } from './loggerFactory'; +export { loggerServiceFactory } from './loggerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/logger/loggerFactory.ts b/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts similarity index 94% rename from packages/backend-app-api/src/services/implementations/logger/loggerFactory.ts rename to packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts index 60c91d0ee6..d9158529ca 100644 --- a/packages/backend-app-api/src/services/implementations/logger/loggerFactory.ts +++ b/packages/backend-app-api/src/services/implementations/logger/loggerServiceFactory.ts @@ -20,7 +20,7 @@ import { } from '@backstage/backend-plugin-api'; /** @public */ -export const loggerFactory = createServiceFactory({ +export const loggerServiceFactory = createServiceFactory({ service: coreServices.logger, deps: { rootLogger: coreServices.rootLogger, diff --git a/packages/backend-app-api/src/services/implementations/permissions/index.ts b/packages/backend-app-api/src/services/implementations/permissions/index.ts index 29a6aa237a..781dda31a0 100644 --- a/packages/backend-app-api/src/services/implementations/permissions/index.ts +++ b/packages/backend-app-api/src/services/implementations/permissions/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { permissionsFactory } from './permissionsFactory'; +export { permissionsServiceFactory } from './permissionsServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/permissions/permissionsFactory.ts b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts similarity index 94% rename from packages/backend-app-api/src/services/implementations/permissions/permissionsFactory.ts rename to packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts index 4eb05e3e4c..38cba73eda 100644 --- a/packages/backend-app-api/src/services/implementations/permissions/permissionsFactory.ts +++ b/packages/backend-app-api/src/services/implementations/permissions/permissionsServiceFactory.ts @@ -21,7 +21,7 @@ import { import { ServerPermissionClient } from '@backstage/plugin-permission-node'; /** @public */ -export const permissionsFactory = createServiceFactory({ +export const permissionsServiceFactory = createServiceFactory({ service: coreServices.permissions, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts index e24662df05..8437399fec 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/index.ts @@ -15,10 +15,10 @@ */ export { - rootHttpRouterFactory, + rootHttpRouterServiceFactory, type RootHttpRouterFactoryOptions, - type RootHttpRouterConfigureOptions, -} from './rootHttpRouterFactory'; + type RootHttpRouterConfigureContext, +} from './rootHttpRouterServiceFactory'; export { DefaultRootHttpRouter, type DefaultRootHttpRouterOptions, diff --git a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts similarity index 55% rename from packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts rename to packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts index 58f8cc2db1..5eaf8aa152 100644 --- a/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootHttpRouter/rootHttpRouterServiceFactory.ts @@ -32,7 +32,7 @@ import { DefaultRootHttpRouter } from './DefaultRootHttpRouter'; /** * @public */ -export interface RootHttpRouterConfigureOptions { +export interface RootHttpRouterConfigureContext { app: Express; middleware: MiddlewareFactory; routes: RequestHandler; @@ -51,14 +51,11 @@ export type RootHttpRouterFactoryOptions = { */ indexPath?: string | false; - configure?(options: RootHttpRouterConfigureOptions): void; + configure?(context: RootHttpRouterConfigureContext): void; }; -function defaultConfigure({ - app, - routes, - middleware, -}: RootHttpRouterConfigureOptions) { +function defaultConfigure(context: RootHttpRouterConfigureContext) { + const { app, routes, middleware } = context; app.use(middleware.helmet()); app.use(middleware.cors()); app.use(middleware.compression()); @@ -69,50 +66,42 @@ function defaultConfigure({ } /** @public */ -export const rootHttpRouterFactory = createServiceFactory({ - service: coreServices.rootHttpRouter, - deps: { - config: coreServices.config, - rootLogger: coreServices.rootLogger, - lifecycle: coreServices.rootLifecycle, - }, - async factory( - { config, rootLogger, lifecycle }, - { - indexPath, - configure = defaultConfigure, - }: RootHttpRouterFactoryOptions = {}, - ) { - const logger = rootLogger.child({ service: 'rootHttpRouter' }); - const app = express(); +export const rootHttpRouterServiceFactory = createServiceFactory( + (options?: RootHttpRouterFactoryOptions) => ({ + service: coreServices.rootHttpRouter, + deps: { + config: coreServices.config, + rootLogger: coreServices.rootLogger, + lifecycle: coreServices.rootLifecycle, + }, + async factory({ config, rootLogger, lifecycle }) { + const { indexPath, configure = defaultConfigure } = options ?? {}; + const logger = rootLogger.child({ service: 'rootHttpRouter' }); + const app = express(); - const router = DefaultRootHttpRouter.create({ indexPath }); - const middleware = MiddlewareFactory.create({ config, logger }); + const router = DefaultRootHttpRouter.create({ indexPath }); + const middleware = MiddlewareFactory.create({ config, logger }); - configure({ - app, - routes: router.handler(), - middleware, - config, - logger, - lifecycle, - }); + configure({ + app, + routes: router.handler(), + middleware, + config, + logger, + lifecycle, + }); - const server = await createHttpServer( - app, - readHttpServerOptions(config.getOptionalConfig('backend')), - { logger }, - ); + const server = await createHttpServer( + app, + readHttpServerOptions(config.getOptionalConfig('backend')), + { logger }, + ); - lifecycle.addShutdownHook({ - async fn() { - await server.stop(); - }, - logger, - }); + lifecycle.addShutdownHook(() => server.stop()); - await server.start(); + await server.start(); - return router; - }, -}); + return router; + }, + }), +); diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts index 8adefbfe84..86589cd23e 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { rootLifecycleFactory } from './rootLifecycleFactory'; +export { rootLifecycleServiceFactory } from './rootLifecycleServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts similarity index 73% rename from packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts rename to packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts index fc1c266775..8d2b2af3d5 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.test.ts @@ -15,17 +15,13 @@ */ import { getVoidLogger } from '@backstage/backend-common'; -import { BackendLifecycleImpl } from './rootLifecycleFactory'; +import { BackendLifecycleImpl } from './rootLifecycleServiceFactory'; describe('lifecycleService', () => { it('should execute registered shutdown hook', async () => { const service = new BackendLifecycleImpl(getVoidLogger()); const hook = jest.fn(); - service.addShutdownHook({ - fn: async () => { - hook(); - }, - }); + service.addShutdownHook(() => hook()); // should not execute the hook more than once. await service.shutdown(); await service.shutdown(); @@ -35,10 +31,16 @@ describe('lifecycleService', () => { it('should not throw errors', async () => { const service = new BackendLifecycleImpl(getVoidLogger()); - service.addShutdownHook({ - fn: async () => { - throw new Error('oh no'); - }, + service.addShutdownHook(() => { + throw new Error('oh no'); + }); + await expect(service.shutdown()).resolves.toBeUndefined(); + }); + + it('should not throw async errors', async () => { + const service = new BackendLifecycleImpl(getVoidLogger()); + service.addShutdownHook(async () => { + throw new Error('oh no'); }); await expect(service.shutdown()).resolves.toBeUndefined(); }); diff --git a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts similarity index 71% rename from packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts rename to packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts index f451c38f20..f379c2ec52 100644 --- a/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootLifecycle/rootLifecycleServiceFactory.ts @@ -18,21 +18,25 @@ import { createServiceFactory, coreServices, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, RootLifecycleService, LoggerService, } from '@backstage/backend-plugin-api'; -const CALLBACKS = ['SIGTERM', 'SIGINT', 'beforeExit']; export class BackendLifecycleImpl implements RootLifecycleService { - constructor(private readonly logger: LoggerService) { - CALLBACKS.map(signal => process.on(signal, () => this.shutdown())); - } + constructor(private readonly logger: LoggerService) {} #isCalled = false; - #shutdownTasks: Array = []; + #shutdownTasks: Array<{ + hook: LifecycleServiceShutdownHook; + options?: LifecycleServiceShutdownOptions; + }> = []; - addShutdownHook(options: LifecycleServiceShutdownHook): void { - this.#shutdownTasks.push(options); + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void { + this.#shutdownTasks.push({ hook, options }); } async shutdown(): Promise { @@ -43,10 +47,10 @@ export class BackendLifecycleImpl implements RootLifecycleService { this.logger.info(`Running ${this.#shutdownTasks.length} shutdown tasks...`); await Promise.all( - this.#shutdownTasks.map(async hook => { - const { logger = this.logger } = hook; + this.#shutdownTasks.map(async ({ hook, options }) => { + const logger = options?.logger ?? this.logger; try { - await hook.fn(); + await hook(); logger.info(`Shutdown hook succeeded`); } catch (error) { logger.error(`Shutdown hook failed, ${error}`); @@ -58,8 +62,10 @@ export class BackendLifecycleImpl implements RootLifecycleService { /** * Allows plugins to register shutdown hooks that are run when the process is about to exit. - * @public */ -export const rootLifecycleFactory = createServiceFactory({ + * + * @public + */ +export const rootLifecycleServiceFactory = createServiceFactory({ service: coreServices.rootLifecycle, deps: { logger: coreServices.rootLogger, diff --git a/packages/backend-app-api/src/services/implementations/rootLogger/index.ts b/packages/backend-app-api/src/services/implementations/rootLogger/index.ts index ce5816e6fa..366cd53163 100644 --- a/packages/backend-app-api/src/services/implementations/rootLogger/index.ts +++ b/packages/backend-app-api/src/services/implementations/rootLogger/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { rootLoggerFactory } from './rootLoggerFactory'; +export { rootLoggerServiceFactory } from './rootLoggerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerFactory.ts b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts similarity index 96% rename from packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerFactory.ts rename to packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts index 1c2214b98c..11bbd58050 100644 --- a/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerFactory.ts +++ b/packages/backend-app-api/src/services/implementations/rootLogger/rootLoggerServiceFactory.ts @@ -23,7 +23,7 @@ import { transports, format } from 'winston'; import { createConfigSecretEnumerator } from '../../../config'; /** @public */ -export const rootLoggerFactory = createServiceFactory({ +export const rootLoggerServiceFactory = createServiceFactory({ service: coreServices.rootLogger, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/services/implementations/scheduler/index.ts b/packages/backend-app-api/src/services/implementations/scheduler/index.ts index 8b84906b5b..8b67006a4d 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/index.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { schedulerFactory } from './schedulerFactory'; +export { schedulerServiceFactory } from './schedulerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.test.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts similarity index 93% rename from packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.test.ts rename to packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts index 1aaf4ec71d..7af9de44da 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.test.ts @@ -19,16 +19,16 @@ import { createBackendPlugin, } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; -import { schedulerFactory } from './schedulerFactory'; +import { schedulerServiceFactory } from './schedulerServiceFactory'; describe('schedulerFactory', () => { it('creates sidecar database features', async () => { expect.assertions(3); - const subject = schedulerFactory(); + const subject = schedulerServiceFactory(); const plugin = createBackendPlugin({ - id: 'example', + pluginId: 'example', register(reg) { reg.registerInit({ deps: { diff --git a/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.ts b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts similarity index 95% rename from packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.ts rename to packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts index 458c5acfd6..4dfe567ccc 100644 --- a/packages/backend-app-api/src/services/implementations/scheduler/schedulerFactory.ts +++ b/packages/backend-app-api/src/services/implementations/scheduler/schedulerServiceFactory.ts @@ -22,7 +22,7 @@ import { import { TaskScheduler } from '@backstage/backend-tasks'; /** @public */ -export const schedulerFactory = createServiceFactory({ +export const schedulerServiceFactory = createServiceFactory({ service: coreServices.scheduler, deps: { plugin: coreServices.pluginMetadata, diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/index.ts b/packages/backend-app-api/src/services/implementations/tokenManager/index.ts index c0685079c4..3ae3d45d2f 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/index.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { tokenManagerFactory } from './tokenManagerFactory'; +export { tokenManagerServiceFactory } from './tokenManagerServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.test.ts b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts similarity index 81% rename from packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.test.ts rename to packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts index abc62fff69..aca5211e2e 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.test.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.test.ts @@ -14,22 +14,15 @@ * limitations under the License. */ -import { - LoggerService, - ServiceFactory, - TokenManagerService, -} from '@backstage/backend-plugin-api'; +import { LoggerService } from '@backstage/backend-plugin-api'; import { ConfigReader } from '@backstage/config'; -import { tokenManagerFactory } from './tokenManagerFactory'; +import { tokenManagerServiceFactory } from './tokenManagerServiceFactory'; describe('tokenManagerFactory', () => { it('should create managers that can share tokens in development', async () => { (process.env as { NODE_ENV?: string }).NODE_ENV = 'development'; - const factory = tokenManagerFactory() as Exclude< - ServiceFactory, - { scope: 'root' } - >; + const factory = tokenManagerServiceFactory() as any; const deps = { config: new ConfigReader({}), logger: { warn() {} } as unknown as LoggerService, diff --git a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.ts b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts similarity index 94% rename from packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.ts rename to packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts index ae0b402eec..b3726176b6 100644 --- a/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerFactory.ts +++ b/packages/backend-app-api/src/services/implementations/tokenManager/tokenManagerServiceFactory.ts @@ -21,7 +21,7 @@ import { import { ServerTokenManager } from '@backstage/backend-common'; /** @public */ -export const tokenManagerFactory = createServiceFactory({ +export const tokenManagerServiceFactory = createServiceFactory({ service: coreServices.tokenManager, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/services/implementations/urlReader/index.ts b/packages/backend-app-api/src/services/implementations/urlReader/index.ts index 5da0ef4049..6a4b9f65be 100644 --- a/packages/backend-app-api/src/services/implementations/urlReader/index.ts +++ b/packages/backend-app-api/src/services/implementations/urlReader/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { urlReaderFactory } from './urlReaderFactory'; +export { urlReaderServiceFactory } from './urlReaderServiceFactory'; diff --git a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderFactory.ts b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts similarity index 94% rename from packages/backend-app-api/src/services/implementations/urlReader/urlReaderFactory.ts rename to packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts index 1b52ce9604..83e594aa0b 100644 --- a/packages/backend-app-api/src/services/implementations/urlReader/urlReaderFactory.ts +++ b/packages/backend-app-api/src/services/implementations/urlReader/urlReaderServiceFactory.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-plugin-api'; /** @public */ -export const urlReaderFactory = createServiceFactory({ +export const urlReaderServiceFactory = createServiceFactory({ service: coreServices.urlReader, deps: { config: coreServices.config, diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index ce17398a79..2a8fa1e720 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -20,16 +20,19 @@ import { coreServices, ServiceRef, } from '@backstage/backend-plugin-api'; -import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleFactory'; +import { BackendLifecycleImpl } from '../services/implementations/rootLifecycle/rootLifecycleServiceFactory'; import { BackendRegisterInit, EnumerableServiceHolder, ServiceOrExtensionPoint, } from './types'; +// Direct internal import to avoid duplication +// eslint-disable-next-line @backstage/no-forbidden-package-imports +import { InternalBackendFeature } from '@backstage/backend-plugin-api/src/wiring/types'; export class BackendInitializer { - #started = false; - #features = new Map(); + #startPromise?: Promise; + #features = new Array(); #registerInits = new Array(); #extensionPoints = new Map, unknown>(); #serviceHolder: EnumerableServiceHolder; @@ -74,19 +77,52 @@ export class BackendInitializer { return Object.fromEntries(result); } - add(feature: BackendFeature, options?: TOptions) { - if (this.#started) { + add(feature: BackendFeature) { + if (this.#startPromise) { throw new Error('feature can not be added after the backend has started'); } - this.#features.set(feature, options); + if (feature.$$type !== '@backstage/BackendFeature') { + throw new Error( + `Failed to add feature, invalid type '${feature.$$type}'`, + ); + } + const internalFeature = feature as InternalBackendFeature; + if (internalFeature.version !== 'v1') { + throw new Error( + `Failed to add feature, invalid version '${internalFeature.version}'`, + ); + } + this.#features.push(internalFeature); } async start(): Promise { - if (this.#started) { + if (this.#startPromise) { throw new Error('Backend has already started'); } - this.#started = true; + const exitHandler = async () => { + process.removeListener('SIGTERM', exitHandler); + process.removeListener('SIGINT', exitHandler); + process.removeListener('beforeExit', exitHandler); + + try { + await this.stop(); + process.exit(0); + } catch (error) { + console.error(error); + process.exit(1); + } + }; + + process.addListener('SIGTERM', exitHandler); + process.addListener('SIGINT', exitHandler); + process.addListener('beforeExit', exitHandler); + + this.#startPromise = this.#doStart(); + await this.#startPromise; + } + + async #doStart(): Promise { // Initialize all root scoped services for (const ref of this.#serviceHolder.getServiceRefs()) { if (ref.scope === 'root') { @@ -95,50 +131,39 @@ export class BackendInitializer { } // Initialize all features - for (const [feature] of this.#features) { - const provides = new Set>(); + for (const feature of this.#features) { + for (const r of feature.getRegistrations()) { + const provides = new Set>(); - let registerInit: BackendRegisterInit | undefined = undefined; + if (r.type === 'plugin') { + for (const [extRef, extImpl] of r.extensionPoints) { + if (this.#extensionPoints.has(extRef)) { + throw new Error( + `ExtensionPoint with ID '${extRef.id}' is already registered`, + ); + } + this.#extensionPoints.set(extRef, extImpl); + provides.add(extRef); + } + } - feature.register({ - registerExtensionPoint: (extensionPointRef, impl) => { - if (registerInit) { - throw new Error('registerExtensionPoint called after registerInit'); - } - if (this.#extensionPoints.has(extensionPointRef)) { - throw new Error(`API ${extensionPointRef.id} already registered`); - } - this.#extensionPoints.set(extensionPointRef, impl); - provides.add(extensionPointRef); - }, - registerInit: registerOptions => { - if (registerInit) { - throw new Error('registerInit must only be called once'); - } - registerInit = { - id: feature.id, - provides, - consumes: new Set(Object.values(registerOptions.deps)), - deps: registerOptions.deps, - init: registerOptions.init as BackendRegisterInit['init'], - }; - }, - }); - - if (!registerInit) { - throw new Error( - `registerInit was not called by register in ${feature.id}`, - ); + this.#registerInits.push({ + id: r.type === 'plugin' ? r.pluginId : `${r.pluginId}.${r.moduleId}`, + provides, + consumes: new Set(Object.values(r.init.deps)), + init: r.init, + }); } - - this.#registerInits.push(registerInit); } const orderedRegisterResults = this.#resolveInitOrder(this.#registerInits); for (const registerInit of orderedRegisterResults) { - const deps = await this.#getInitDeps(registerInit.deps, registerInit.id); - await registerInit.init(deps); + const deps = await this.#getInitDeps( + registerInit.init.deps, + registerInit.id, + ); + await registerInit.init.func(deps); } } @@ -177,9 +202,10 @@ export class BackendInitializer { } async stop(): Promise { - if (!this.#started) { + if (!this.#startPromise) { return; } + await this.#startPromise; const lifecycleService = await this.#serviceHolder.get( coreServices.rootLifecycle, diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index 68dff3f7c3..b5b55f67cf 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -18,36 +18,66 @@ import { ServiceFactory, ServiceRef, coreServices, + createServiceFactory, } from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; import { EnumerableServiceHolder } from './types'; +// Direct internal import to avoid duplication +// eslint-disable-next-line @backstage/no-forbidden-package-imports +import { InternalServiceFactory } from '@backstage/backend-plugin-api/src/services/system/types'; /** * Keep in sync with `@backstage/backend-plugin-api/src/services/system/types.ts` * @internal */ -export type InternalServiceRef = ServiceRef & { +export type InternalServiceRef = ServiceRef & { __defaultFactory?: ( - service: ServiceRef, - ) => Promise | (() => ServiceFactory)>; + service: ServiceRef, + ) => Promise ServiceFactory)>; }; +function toInternalServiceFactory( + factory: ServiceFactory, +): InternalServiceFactory { + const f = factory as InternalServiceFactory; + if (f.$$type !== '@backstage/ServiceFactory') { + throw new Error(`Invalid service factory, bad type '${f.$$type}'`); + } + if (f.version !== 'v1') { + throw new Error(`Invalid service factory, bad version '${f.version}'`); + } + return f; +} + +const pluginMetadataServiceFactory = createServiceFactory( + (options: { pluginId: string }) => ({ + service: coreServices.pluginMetadata, + deps: {}, + factory: async () => ({ getId: () => options.pluginId }), + }), +); + export class ServiceRegistry implements EnumerableServiceHolder { - readonly #providedFactories: Map; - readonly #loadedDefaultFactories: Map>; + readonly #providedFactories: Map; + readonly #loadedDefaultFactories: Map< + Function, + Promise + >; readonly #implementations: Map< - ServiceFactory, + InternalServiceFactory, { context: Promise; byPlugin: Map>; } >; readonly #rootServiceImplementations = new Map< - ServiceFactory, + InternalServiceFactory, Promise >(); - constructor(factories: Array>) { - this.#providedFactories = new Map(factories.map(f => [f.service.id, f])); + constructor(factories: Array) { + this.#providedFactories = new Map( + factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]), + ); this.#loadedDefaultFactories = new Map(); this.#implementations = new Map(); } @@ -55,23 +85,19 @@ export class ServiceRegistry implements EnumerableServiceHolder { #resolveFactory( ref: ServiceRef, pluginId: string, - ): Promise | undefined { + ): Promise | undefined { // Special case handling of the plugin metadata service, generating a custom factory for it each time if (ref.id === coreServices.pluginMetadata.id) { - return Promise.resolve< - ServiceFactory - >({ - scope: 'plugin', - service: coreServices.pluginMetadata, - deps: {}, - factory: async () => ({ getId: () => pluginId }), - }); + return Promise.resolve( + toInternalServiceFactory(pluginMetadataServiceFactory({ pluginId })), + ); } - let resolvedFactory: Promise | ServiceFactory | undefined = - this.#providedFactories.get(ref.id); - const { __defaultFactory: defaultFactory } = - ref as InternalServiceRef; + let resolvedFactory: + | Promise + | InternalServiceFactory + | undefined = this.#providedFactories.get(ref.id); + const { __defaultFactory: defaultFactory } = ref as InternalServiceRef; if (!resolvedFactory && !defaultFactory) { return undefined; } @@ -82,8 +108,8 @@ export class ServiceRegistry implements EnumerableServiceHolder { loadedFactory = Promise.resolve() .then(() => defaultFactory!(ref)) .then(f => - typeof f === 'function' ? f() : f, - ) as Promise; + toInternalServiceFactory(typeof f === 'function' ? f() : f), + ); this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); } resolvedFactory = loadedFactory.catch(error => { @@ -100,7 +126,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { return Promise.resolve(resolvedFactory); } - #checkForMissingDeps(factory: ServiceFactory, pluginId: string) { + #checkForMissingDeps(factory: InternalServiceFactory, pluginId: string) { const missingDeps = Object.values(factory.deps).filter(ref => { if (ref.id === coreServices.pluginMetadata.id) { return false; @@ -109,7 +135,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { return false; } - return !(ref as InternalServiceRef).__defaultFactory; + return !(ref as InternalServiceRef).__defaultFactory; }); if (missingDeps.length) { @@ -126,7 +152,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { get(ref: ServiceRef, pluginId: string): Promise | undefined { return this.#resolveFactory(ref, pluginId)?.then(factory => { - if (factory.scope === 'root') { + if (factory.service.scope === 'root') { let existing = this.#rootServiceImplementations.get(factory); if (!existing) { this.#checkForMissingDeps(factory, pluginId); @@ -143,7 +169,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { } existing = Promise.all(rootDeps).then(entries => - factory.factory(Object.fromEntries(entries)), + factory.factory(Object.fromEntries(entries), undefined), ); this.#rootServiceImplementations.set(factory, existing); } diff --git a/packages/backend-app-api/src/wiring/index.ts b/packages/backend-app-api/src/wiring/index.ts index 6e0fb272ae..00e4a9d4b4 100644 --- a/packages/backend-app-api/src/wiring/index.ts +++ b/packages/backend-app-api/src/wiring/index.ts @@ -14,9 +14,5 @@ * limitations under the License. */ -export type { - Backend, - CreateSpecializedBackendOptions, - ServiceOrExtensionPoint, -} from './types'; +export type { Backend, CreateSpecializedBackendOptions } from './types'; export { createSpecializedBackend } from './createSpecializedBackend'; diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index d3c4fc48c7..2a91bf46b9 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -34,8 +34,10 @@ export interface BackendRegisterInit { id: string; consumes: Set; provides: Set; - deps: { [name: string]: ServiceOrExtensionPoint }; - init: (deps: { [name: string]: unknown }) => Promise; + init: { + deps: { [name: string]: ServiceOrExtensionPoint }; + func: (deps: { [name: string]: unknown }) => Promise; + }; } /** diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index f8090ae971..1a495ff771 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/backend-common +## 0.18.2-next.2 + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- d31d8e00b3: Updated to work with the new `type: 'pem'` with `createHttpServer` from `@backstage/backend-app-api` +- 0ff03319be: Updated usage of `createBackendPlugin`. +- f60cca9da1: The `DatabaseManager.forPlugin` method now accepts additional service dependencies. There is no need to update existing code to pass these dependencies. +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-dev-utils@0.1.0-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + ## 0.18.2-next.1 ### Patch Changes diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index b03d6ec866..d21ef30a0b 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -13,9 +13,9 @@ import { BackendFeature } from '@backstage/backend-plugin-api'; import { BitbucketCloudIntegration } from '@backstage/integration'; import { BitbucketIntegration } from '@backstage/integration'; import { BitbucketServerIntegration } from '@backstage/integration'; -import { CacheClient } from '@backstage/backend-plugin-api'; -import { CacheClientOptions } from '@backstage/backend-plugin-api'; -import { CacheClientSetOptions } from '@backstage/backend-plugin-api'; +import { CacheService as CacheClient } from '@backstage/backend-plugin-api'; +import { CacheServiceOptions as CacheClientOptions } from '@backstage/backend-plugin-api'; +import { CacheServiceSetOptions as CacheClientSetOptions } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { ConfigService } from '@backstage/backend-plugin-api'; import cors from 'cors'; @@ -32,14 +32,15 @@ import { IdentityService } from '@backstage/backend-plugin-api'; import { isChildPath } from '@backstage/cli-common'; import { Knex } from 'knex'; import { KubeConfig } from '@kubernetes/client-node'; +import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoadConfigOptionsRemote } from '@backstage/config-loader'; import { Logger } from 'winston'; import { LoggerService } from '@backstage/backend-plugin-api'; import { MergeResult } from 'isomorphic-git'; import { PermissionsService } from '@backstage/backend-plugin-api'; -import { CacheService as PluginCacheManager } from '@backstage/backend-plugin-api'; import { DatabaseService as PluginDatabaseManager } from '@backstage/backend-plugin-api'; import { DiscoveryService as PluginEndpointDiscovery } from '@backstage/backend-plugin-api'; +import { PluginMetadataService } from '@backstage/backend-plugin-api'; import { PushResult } from 'isomorphic-git'; import { Readable } from 'stream'; import { ReadCommitResult } from 'isomorphic-git'; @@ -197,6 +198,11 @@ export type CacheManagerOptions = { onError?: (err: Error) => void; }; +// @public (undocumented) +export function cacheToPluginCacheManager( + cache: CacheClient, +): PluginCacheManager; + // @public export const coloredFormat: winston.Logform.Format; @@ -232,6 +238,10 @@ export class Contexts { export function createDatabaseClient( dbConfig: Config, overrides?: Partial, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ): Knex; // @public @@ -252,7 +262,13 @@ export function createStatusCheckRouter(options: { // @public export class DatabaseManager { - forPlugin(pluginId: string): PluginDatabaseManager; + forPlugin( + pluginId: string, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): PluginDatabaseManager; static fromConfig( config: Config, options?: DatabaseManagerOptions, @@ -516,7 +532,7 @@ export const legacyPlugin: ( default: LegacyCreateRouter< TransformedEnv< { - cache: PluginCacheManager; + cache: CacheClient; config: ConfigService; database: PluginDatabaseManager; discovery: PluginEndpointDiscovery; @@ -529,6 +545,7 @@ export const legacyPlugin: ( }, { logger: (log: LoggerService) => Logger; + cache: (cache: CacheClient) => PluginCacheManager; } > >; @@ -569,7 +586,11 @@ export function makeLegacyPlugin< // @public export function notFoundHandler(): RequestHandler; -export { PluginCacheManager }; +// @public (undocumented) +export interface PluginCacheManager { + // (undocumented) + getClient(options?: CacheClientOptions): CacheClient; +} export { PluginDatabaseManager }; diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index c950950680..9619ef25f8 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.18.2-next.1", + "version": "0.18.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -35,6 +35,7 @@ }, "dependencies": { "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-dev-utils": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/cli-common": "workspace:^", "@backstage/config": "workspace:^", diff --git a/packages/backend-common/src/cache/CacheClient.test.ts b/packages/backend-common/src/cache/CacheClient.test.ts index 8a5779550a..245d8687c9 100644 --- a/packages/backend-common/src/cache/CacheClient.test.ts +++ b/packages/backend-common/src/cache/CacheClient.test.ts @@ -32,7 +32,7 @@ describe('CacheClient', () => { describe('CacheClient.get', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const key = 'somekey'; await sut.get(key); @@ -41,7 +41,7 @@ describe('CacheClient', () => { }); it('calls client with normalized key (very long key)', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const key = 'x'.repeat(251); await sut.get(key); @@ -53,7 +53,7 @@ describe('CacheClient', () => { }); it('rejects on underlying error', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedError = new Error('Some runtime error'); client.get = jest.fn().mockRejectedValue(expectedError); @@ -61,7 +61,7 @@ describe('CacheClient', () => { }); it('resolves what underlying client resolves', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedValue = 'some value'; client.get = jest.fn().mockResolvedValue(expectedValue); @@ -73,7 +73,7 @@ describe('CacheClient', () => { describe('CacheClient.set', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const key = 'somekey'; await sut.set(key, {}); @@ -84,7 +84,7 @@ describe('CacheClient', () => { }); it('passes ttl to client when given', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedTtl = 3600; await sut.set('someKey', {}, { ttl: expectedTtl }); @@ -95,7 +95,7 @@ describe('CacheClient', () => { }); it('rejects on underlying error if configured', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedError = new Error('Some runtime error'); client.set = jest.fn().mockRejectedValue(expectedError); @@ -105,7 +105,7 @@ describe('CacheClient', () => { describe('CacheClient.delete', () => { it('calls client with normalized key', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const key = 'somekey'; await sut.delete(key); @@ -116,11 +116,35 @@ describe('CacheClient', () => { }); it('rejects on underlying error if configured', async () => { - const sut = new DefaultCacheClient({ client }); + const sut = new DefaultCacheClient(client, () => client, {}); const expectedError = new Error('Some runtime error'); client.delete = jest.fn().mockRejectedValue(expectedError); return expect(sut.delete('someKey')).rejects.toEqual(expectedError); }); }); + + describe('CacheClient.withOptions', () => { + it('merges together options to create a new instance', async () => { + const factory = jest.fn(); + const sut = new DefaultCacheClient(client, factory, { foo: 1 } as any); + expect(factory).not.toHaveBeenCalled(); + + const sutA = await sut.withOptions({}); + expect(factory).toHaveBeenLastCalledWith({ foo: 1 }); + + const sutA2 = await sutA.withOptions({ bar: 2 } as any); + expect(factory).toHaveBeenCalledWith({ foo: 1, bar: 2 }); + + await sutA2.withOptions({ foo: 3 } as any); + expect(factory).toHaveBeenCalledWith({ foo: 3, bar: 2 }); + + // calling .withOptions should not mutate state + const sutB = await sut.withOptions({ foo: 2 } as any); + expect(factory).toHaveBeenLastCalledWith({ foo: 2 }); + + await sutB.withOptions({ bar: 3 } as any); + expect(factory).toHaveBeenLastCalledWith({ foo: 2, bar: 3 }); + }); + }); }); diff --git a/packages/backend-common/src/cache/CacheClient.ts b/packages/backend-common/src/cache/CacheClient.ts index 47cb786f14..24ee7ac455 100644 --- a/packages/backend-common/src/cache/CacheClient.ts +++ b/packages/backend-common/src/cache/CacheClient.ts @@ -15,50 +15,64 @@ */ import { - CacheClient, - CacheClientSetOptions, + CacheService, + CacheServiceOptions, + CacheServiceSetOptions, } from '@backstage/backend-plugin-api'; import { JsonValue } from '@backstage/types'; import { createHash } from 'crypto'; import Keyv from 'keyv'; -export type { - CacheClient, - CacheClientSetOptions, -} from '@backstage/backend-plugin-api'; - -type CacheClientArgs = { - client: Keyv; -}; +export type CacheClientFactory = (options: CacheServiceOptions) => Keyv; /** - * A basic, concrete implementation of the CacheClient, suitable for almost + * A basic, concrete implementation of the CacheService, suitable for almost * all uses in Backstage. */ -export class DefaultCacheClient implements CacheClient { - private readonly client: Keyv; +export class DefaultCacheClient implements CacheService { + #client: Keyv; + #clientFactory: CacheClientFactory; + #options: CacheServiceOptions; - constructor({ client }: CacheClientArgs) { - this.client = client; + constructor( + client: Keyv, + clientFactory: CacheClientFactory, + options: CacheServiceOptions, + ) { + this.#client = client; + this.#clientFactory = clientFactory; + this.#options = options; } - async get(key: string): Promise { + async get( + key: string, + ): Promise { const k = this.getNormalizedKey(key); - return await this.client.get(k); + const value = await this.#client.get(k); + return value as TValue | undefined; } async set( key: string, value: JsonValue, - opts: CacheClientSetOptions = {}, + opts: CacheServiceSetOptions = {}, ): Promise { const k = this.getNormalizedKey(key); - await this.client.set(k, value, opts.ttl); + await this.#client.set(k, value, opts.ttl); } async delete(key: string): Promise { const k = this.getNormalizedKey(key); - await this.client.delete(k); + await this.#client.delete(k); + } + + withOptions(options: CacheServiceOptions): CacheService { + const newOptions = { ...this.#options, ...options }; + return new DefaultCacheClient( + this.#clientFactory(newOptions), + this.#clientFactory, + newOptions, + ); } /** diff --git a/packages/backend-common/src/cache/CacheManager.test.ts b/packages/backend-common/src/cache/CacheManager.test.ts index 50b775af25..3239be4d0e 100644 --- a/packages/backend-common/src/cache/CacheManager.test.ts +++ b/packages/backend-common/src/cache/CacheManager.test.ts @@ -94,7 +94,7 @@ describe('CacheManager', () => { const client = DefaultCacheClient as jest.Mock; const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0].client as Keyv; + const realClient = mockCalls[0][0] as Keyv; expect(realClient.on).toHaveBeenCalledWith('error', expect.any(Function)); }); @@ -233,7 +233,7 @@ describe('CacheManager', () => { // Retrieve the error handler attached to the cache client. const client = DefaultCacheClient as jest.Mock; const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0].client as Keyv; + const realClient = mockCalls[0][0] as Keyv; const realOnError = realClient.on as jest.Mock; const realHandler = realOnError.mock.calls.splice(-1)[0][1]; @@ -259,7 +259,7 @@ describe('CacheManager', () => { // Retrieve the error handler attached to the cache client. const client = DefaultCacheClient as jest.Mock; const mockCalls = client.mock.calls.splice(-1); - const realClient = mockCalls[0][0].client as Keyv; + const realClient = mockCalls[0][0] as Keyv; const realOnError = realClient.on as jest.Mock; const realHandler = realOnError.mock.calls.splice(-1)[0][1]; diff --git a/packages/backend-common/src/cache/CacheManager.ts b/packages/backend-common/src/cache/CacheManager.ts index 4f7f5403a8..12ece42854 100644 --- a/packages/backend-common/src/cache/CacheManager.ts +++ b/packages/backend-common/src/cache/CacheManager.ts @@ -18,9 +18,13 @@ import { Config } from '@backstage/config'; import Keyv from 'keyv'; import KeyvMemcache from '@keyv/memcache'; import KeyvRedis from '@keyv/redis'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { + CacheService, + CacheServiceOptions, + LoggerService, +} from '@backstage/backend-plugin-api'; import { getRootLogger } from '../logging'; -import { DefaultCacheClient, CacheClient } from './CacheClient'; +import { DefaultCacheClient } from './CacheClient'; import { NoStore } from './NoStore'; import { CacheManagerOptions, PluginCacheManager } from './types'; @@ -99,23 +103,32 @@ export class CacheManager { */ forPlugin(pluginId: string): PluginCacheManager { return { - getClient: (opts = {}): CacheClient => { - const concreteClient = this.getClientWithTtl(pluginId, opts.defaultTtl); + getClient: (defaultOptions = {}) => { + const clientFactory = (options: CacheServiceOptions) => { + const concreteClient = this.getClientWithTtl( + pluginId, + options.defaultTtl, + ); - // Always provide an error handler to avoid stopping the process. - concreteClient.on('error', (err: Error) => { - // In all cases, just log the error. - this.logger.error('Failed to create cache client', err); + // Always provide an error handler to avoid stopping the process. + concreteClient.on('error', (err: Error) => { + // In all cases, just log the error. + this.logger.error('Failed to create cache client', err); - // Invoke any custom error handler if provided. - if (typeof this.errorHandler === 'function') { - this.errorHandler(err); - } - }); + // Invoke any custom error handler if provided. + if (typeof this.errorHandler === 'function') { + this.errorHandler(err); + } + }); - return new DefaultCacheClient({ - client: concreteClient, - }); + return concreteClient; + }; + + return new DefaultCacheClient( + clientFactory(defaultOptions), + clientFactory, + defaultOptions, + ); }, }; } @@ -164,3 +177,12 @@ export class CacheManager { }); } } + +/** @public */ +export function cacheToPluginCacheManager( + cache: CacheService, +): PluginCacheManager { + return { + getClient: (opts: CacheServiceOptions) => cache.withOptions(opts), + }; +} diff --git a/packages/backend-common/src/cache/index.ts b/packages/backend-common/src/cache/index.ts index 26e6c102d8..0187aa45d5 100644 --- a/packages/backend-common/src/cache/index.ts +++ b/packages/backend-common/src/cache/index.ts @@ -14,9 +14,10 @@ * limitations under the License. */ -export type { CacheClient, CacheClientSetOptions } from './CacheClient'; -export { CacheManager } from './CacheManager'; +export { CacheManager, cacheToPluginCacheManager } from './CacheManager'; export type { + CacheClient, + CacheClientSetOptions, PluginCacheManager, CacheManagerOptions, CacheClientOptions, diff --git a/packages/backend-common/src/cache/types.ts b/packages/backend-common/src/cache/types.ts index eeeafde5b6..93fa0ac77c 100644 --- a/packages/backend-common/src/cache/types.ts +++ b/packages/backend-common/src/cache/types.ts @@ -15,10 +15,15 @@ */ import { LoggerService } from '@backstage/backend-plugin-api'; +import { + CacheService, + CacheServiceOptions, +} from '@backstage/backend-plugin-api'; export type { - CacheService as PluginCacheManager, - CacheClientOptions, + CacheService as CacheClient, + CacheServiceSetOptions as CacheClientSetOptions, + CacheServiceOptions as CacheClientOptions, } from '@backstage/backend-plugin-api'; /** @@ -38,3 +43,10 @@ export type CacheManagerOptions = { */ onError?: (err: Error) => void; }; + +/** + * @public + */ +export interface PluginCacheManager { + getClient(options?: CacheServiceOptions): CacheService; +} diff --git a/packages/backend-common/src/database/DatabaseManager.ts b/packages/backend-common/src/database/DatabaseManager.ts index 5a497fff29..08979a92f8 100644 --- a/packages/backend-common/src/database/DatabaseManager.ts +++ b/packages/backend-common/src/database/DatabaseManager.ts @@ -29,7 +29,11 @@ import { } from './connection'; import { PluginDatabaseManager } from './types'; import path from 'path'; -import { LoggerService } from '@backstage/backend-plugin-api'; +import { + LifecycleService, + LoggerService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; /** @@ -94,12 +98,18 @@ export class DatabaseManager { * should be unique as they are used to look up database config overrides under * `backend.database.plugin`. */ - forPlugin(pluginId: string): PluginDatabaseManager { + forPlugin( + pluginId: string, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): PluginDatabaseManager { const _this = this; return { getClient(): Promise { - return _this.getDatabase(pluginId); + return _this.getDatabase(pluginId, deps); }, migrations: { skip: false, @@ -307,7 +317,13 @@ export class DatabaseManager { * @returns Promise which resolves to a scoped Knex database client for a * plugin */ - private async getDatabase(pluginId: string): Promise { + private async getDatabase( + pluginId: string, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): Promise { if (this.databaseCache.has(pluginId)) { return this.databaseCache.get(pluginId)!; } @@ -351,6 +367,7 @@ export class DatabaseManager { const client = createDatabaseClient( pluginConfig, databaseClientOverrides, + deps, ); this.startKeepaliveLoop(pluginId, client); return client; diff --git a/packages/backend-common/src/database/connection.ts b/packages/backend-common/src/database/connection.ts index 5f487886b4..ff10d349dd 100644 --- a/packages/backend-common/src/database/connection.ts +++ b/packages/backend-common/src/database/connection.ts @@ -22,6 +22,10 @@ import { mergeDatabaseConfig } from './config'; import { DatabaseConnector } from './types'; import { mysqlConnector, pgConnector, sqlite3Connector } from './connectors'; +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; type DatabaseClient = | 'pg' @@ -55,11 +59,15 @@ const ConnectorMapping: Record = { export function createDatabaseClient( dbConfig: Config, overrides?: Partial, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ) { const client: DatabaseClient = dbConfig.getString('client'); return ( - ConnectorMapping[client]?.createClient(dbConfig, overrides) ?? + ConnectorMapping[client]?.createClient(dbConfig, overrides, deps) ?? knexFactory(mergeDatabaseConfig(dbConfig.get(), overrides)) ); } diff --git a/packages/backend-common/src/database/connectors/sqlite3.ts b/packages/backend-common/src/database/connectors/sqlite3.ts index 6426005f0e..3cb0f57de6 100644 --- a/packages/backend-common/src/database/connectors/sqlite3.ts +++ b/packages/backend-common/src/database/connectors/sqlite3.ts @@ -18,8 +18,13 @@ import { Config } from '@backstage/config'; import { ensureDirSync } from 'fs-extra'; import knexFactory, { Knex } from 'knex'; import path from 'path'; +import { DevDataStore } from '@backstage/backend-dev-utils'; import { mergeDatabaseConfig } from '../config'; import { DatabaseConnector } from '../types'; +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; /** * Creates a knex SQLite3 database connection @@ -30,22 +35,63 @@ import { DatabaseConnector } from '../types'; export function createSqliteDatabaseClient( dbConfig: Config, overrides?: Knex.Config, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, ) { const knexConfig = buildSqliteDatabaseConfig(dbConfig, overrides); + const connConfig = knexConfig.connection as Knex.Sqlite3ConnectionConfig; + + const filename = connConfig.filename ?? ':memory:'; // If storage on disk is used, ensure that the directory exists - if ( - (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename && - (knexConfig.connection as Knex.Sqlite3ConnectionConfig).filename !== - ':memory:' - ) { - const { filename } = knexConfig.connection as Knex.Sqlite3ConnectionConfig; + if (filename !== ':memory:') { const directory = path.dirname(filename); - ensureDirSync(directory); } - const database = knexFactory(knexConfig); + let database: Knex; + + if (deps && filename === ':memory:') { + // The dev store is used during watch mode to store and restore the database + // across reloads. It is only available when running the backend through + // `backstage-cli package start`. + const devStore = DevDataStore.get(); + + if (devStore) { + const dataKey = `sqlite3-db-${deps.pluginMetadata.getId()}`; + + const connectionLoader = async () => { + // If seed data is available, use it tconnectionLoader restore the database + const { data: seedData } = await devStore.load(dataKey); + + return { + ...(knexConfig.connection as Knex.Sqlite3ConnectionConfig), + filename: seedData ?? ':memory:', + }; + }; + + database = knexFactory({ + ...knexConfig, + connection: Object.assign(connectionLoader, { + // This is a workaround for the knex SQLite driver always warning when using a config loader + filename: ':memory:', + }), + }); + + // If the dev store is available we save the database state on shutdown + deps.lifecycle.addShutdownHook(async () => { + const connection = await database.client.acquireConnection(); + const data = connection.serialize(); + await devStore.save(dataKey, data); + }); + } else { + database = knexFactory(knexConfig); + } + } else { + database = knexFactory(knexConfig); + } database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { resource.run('PRAGMA foreign_keys = ON', () => {}); diff --git a/packages/backend-common/src/database/types.ts b/packages/backend-common/src/database/types.ts index 2d59cf636e..621bcc676b 100644 --- a/packages/backend-common/src/database/types.ts +++ b/packages/backend-common/src/database/types.ts @@ -14,6 +14,10 @@ * limitations under the License. */ +import { + LifecycleService, + PluginMetadataService, +} from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import { Knex } from 'knex'; @@ -26,7 +30,14 @@ export interface DatabaseConnector { /** * createClient provides an instance of a knex database connector. */ - createClient(dbConfig: Config, overrides?: Partial): Knex; + createClient( + dbConfig: Config, + overrides?: Partial, + deps?: { + lifecycle: LifecycleService; + pluginMetadata: PluginMetadataService; + }, + ): Knex; /** * createNameOverride provides a partial knex config sufficient to override a * database name. diff --git a/packages/backend-common/src/legacy.ts b/packages/backend-common/src/legacy.ts index 30e7dd9dc5..408966f3bc 100644 --- a/packages/backend-common/src/legacy.ts +++ b/packages/backend-common/src/legacy.ts @@ -20,6 +20,7 @@ import { ServiceRef, } from '@backstage/backend-plugin-api'; import { RequestHandler } from 'express'; +import { cacheToPluginCacheManager } from './cache'; import { loggerToWinstonLogger } from './logging'; /** @@ -60,7 +61,7 @@ export function makeLegacyPlugin< }>, ) => { const compatPlugin = createBackendPlugin({ - id: name, + pluginId: name, register(env) { env.registerInit({ deps: { ...envMapping, _router: coreServices.httpRouter }, @@ -119,5 +120,6 @@ export const legacyPlugin = makeLegacyPlugin( }, { logger: log => loggerToWinstonLogger(log), + cache: cache => cacheToPluginCacheManager(cache), }, ); diff --git a/packages/backend-common/src/logging/loggerToWinstonLogger.ts b/packages/backend-common/src/logging/loggerToWinstonLogger.ts index f15bd5be81..4ba88358d2 100644 --- a/packages/backend-common/src/logging/loggerToWinstonLogger.ts +++ b/packages/backend-common/src/logging/loggerToWinstonLogger.ts @@ -15,6 +15,7 @@ */ import { LoggerService } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; import { Logger as WinstonLogger, createLogger } from 'winston'; import Transport, { TransportStreamOptions } from 'winston-transport'; @@ -31,7 +32,7 @@ class BackstageLoggerTransport extends Transport { callback(); return; } - const { level, message, ...meta } = info as { [name: string]: unknown }; + const { level, message, ...meta } = info as JsonObject; switch (level) { case 'error': this.backstageLogger.error(String(message), meta); diff --git a/packages/backend-common/src/scm/git.test.ts b/packages/backend-common/src/scm/git.test.ts index fdd3c7dbc8..629173f96e 100644 --- a/packages/backend-common/src/scm/git.test.ts +++ b/packages/backend-common/src/scm/git.test.ts @@ -202,7 +202,7 @@ describe('Git', () => { await git.clone({ url, dir }); const { onAuth } = ( - isomorphic.clone as unknown as jest.Mock + isomorphic.clone as unknown as jest.Mock<(typeof isomorphic)['clone']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); @@ -315,7 +315,7 @@ describe('Git', () => { await git.fetch({ remote, dir }); const { onAuth } = ( - isomorphic.fetch as unknown as jest.Mock + isomorphic.fetch as unknown as jest.Mock<(typeof isomorphic)['fetch']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); @@ -486,7 +486,7 @@ describe('Git', () => { await git.push({ remote, dir, remoteRef, force }); const { onAuth } = ( - isomorphic.push as unknown as jest.Mock + isomorphic.push as unknown as jest.Mock<(typeof isomorphic)['push']> ).mock.calls[0][0]!; expect(onAuth()).toEqual(auth); diff --git a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts index f74dcd9b64..f577017b77 100644 --- a/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts +++ b/packages/backend-common/src/service/lib/ServiceBuilderImpl.ts @@ -102,7 +102,7 @@ export class ServiceBuilderImpl implements ServiceBuilder { this.serverOptions.https = { certificate: { ...settings.certificate, - type: 'plain', + type: 'pem', }, }; } diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md index 0824ee5655..89f3ee25c6 100644 --- a/packages/backend-defaults/CHANGELOG.md +++ b/packages/backend-defaults/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/backend-defaults +## 0.1.7-next.2 + +### Patch Changes + +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + ## 0.1.7-next.1 ### Patch Changes diff --git a/packages/backend-defaults/README.md b/packages/backend-defaults/README.md index 3128159f7c..826994da23 100644 --- a/packages/backend-defaults/README.md +++ b/packages/backend-defaults/README.md @@ -1,8 +1,8 @@ # @backstage/backend-defaults -**This package is HIGHLY EXPERIMENTAL, do not use this for production** +**This package is EXPERIMENTAL, we recommend against using it for production deployments** -This package provides the core API used by Backstage backend apps. +This package provides the default implementations and setup for a standard Backstage backend app. ## Installation @@ -17,3 +17,4 @@ yarn add --cwd packages/backend @backstage/backend-defaults - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) - [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Backend System](https://backstage.io/docs/backend-system/) diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json index 6b1b173205..2d92d15ab8 100644 --- a/packages/backend-defaults/package.json +++ b/packages/backend-defaults/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-defaults", "description": "Backend defaults used by Backstage backend apps", - "version": "0.1.7-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-defaults/src/CreateBackend.test.ts b/packages/backend-defaults/src/CreateBackend.test.ts index 2cfb92c3a6..8d56cd0986 100644 --- a/packages/backend-defaults/src/CreateBackend.test.ts +++ b/packages/backend-defaults/src/CreateBackend.test.ts @@ -143,7 +143,7 @@ describe('createBackend', () => { expect.assertions(3); backend.add( createBackendPlugin({ - id: 'test', + pluginId: 'test', register(reg) { reg.registerInit({ deps: { diff --git a/packages/backend-defaults/src/CreateBackend.ts b/packages/backend-defaults/src/CreateBackend.ts index e6b5a15336..480f365216 100644 --- a/packages/backend-defaults/src/CreateBackend.ts +++ b/packages/backend-defaults/src/CreateBackend.ts @@ -16,22 +16,22 @@ import { Backend, - cacheFactory, - configFactory, + cacheServiceFactory, + configServiceFactory, createSpecializedBackend, - databaseFactory, - discoveryFactory, - httpRouterFactory, - rootHttpRouterFactory, - lifecycleFactory, - rootLifecycleFactory, - loggerFactory, - permissionsFactory, - rootLoggerFactory, - schedulerFactory, - tokenManagerFactory, - urlReaderFactory, - identityFactory, + databaseServiceFactory, + discoveryServiceFactory, + httpRouterServiceFactory, + rootHttpRouterServiceFactory, + lifecycleServiceFactory, + rootLifecycleServiceFactory, + loggerServiceFactory, + permissionsServiceFactory, + rootLoggerServiceFactory, + schedulerServiceFactory, + tokenManagerServiceFactory, + urlReaderServiceFactory, + identityServiceFactory, } from '@backstage/backend-app-api'; import { ServiceFactory, @@ -40,25 +40,25 @@ import { } from '@backstage/backend-plugin-api'; // Internal import of the type to avoid needing to export this. -// eslint-disable-next-line monorepo/no-internal-import +// eslint-disable-next-line @backstage/no-forbidden-package-imports import type { InternalSharedBackendEnvironment } from '@backstage/backend-plugin-api/src/wiring/createSharedEnvironment'; export const defaultServiceFactories = [ - cacheFactory(), - configFactory(), - databaseFactory(), - discoveryFactory(), - httpRouterFactory(), - identityFactory(), - lifecycleFactory(), - loggerFactory(), - permissionsFactory(), - rootHttpRouterFactory(), - rootLifecycleFactory(), - rootLoggerFactory(), - schedulerFactory(), - tokenManagerFactory(), - urlReaderFactory(), + cacheServiceFactory(), + configServiceFactory(), + databaseServiceFactory(), + discoveryServiceFactory(), + httpRouterServiceFactory(), + identityServiceFactory(), + lifecycleServiceFactory(), + loggerServiceFactory(), + permissionsServiceFactory(), + rootHttpRouterServiceFactory(), + rootLifecycleServiceFactory(), + rootLoggerServiceFactory(), + schedulerServiceFactory(), + tokenManagerServiceFactory(), + urlReaderServiceFactory(), ]; /** diff --git a/packages/backend-dev-utils/.eslintrc.js b/packages/backend-dev-utils/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/packages/backend-dev-utils/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/backend-dev-utils/CHANGELOG.md b/packages/backend-dev-utils/CHANGELOG.md new file mode 100644 index 0000000000..9acf8cf15f --- /dev/null +++ b/packages/backend-dev-utils/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/backend-dev-utils + +## 0.1.0-next.0 + +### Minor Changes + +- 9d9cdea117: Introduced a new package for backend development utilities. Similar to how `@backstage/dev-utils` is used in the frontend. diff --git a/packages/backend-dev-utils/README.md b/packages/backend-dev-utils/README.md new file mode 100644 index 0000000000..c60f5a4ad7 --- /dev/null +++ b/packages/backend-dev-utils/README.md @@ -0,0 +1,20 @@ +# @backstage/backend-dev-utils + +**This package is EXPERIMENTAL, but we encourage use of it to add support for the new backend system in your own plugins** + +This package helps set up local development environments for Backstage backend packages. + +## Installation + +Add the library to your backend plugin or module package: + +```bash +# From your Backstage root directory +yarn add --cwd plugins/-backend @backstage/backend-dev-utils +``` + +## Documentation + +- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Backend System](https://backstage.io/docs/backend-system/) diff --git a/packages/backend-dev-utils/api-report.md b/packages/backend-dev-utils/api-report.md new file mode 100644 index 0000000000..216311ccd8 --- /dev/null +++ b/packages/backend-dev-utils/api-report.md @@ -0,0 +1,20 @@ +## API Report File for "@backstage/backend-dev-utils" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public +export class DevDataStore { + static get(): DevDataStore | undefined; + load(key: string): Promise<{ + loaded: boolean; + data: T; + }>; + save( + key: string, + data: T, + ): Promise<{ + saved: boolean; + }>; +} +``` diff --git a/packages/backend-dev-utils/package.json b/packages/backend-dev-utils/package.json new file mode 100644 index 0000000000..6e1cf7d3b0 --- /dev/null +++ b/packages/backend-dev-utils/package.json @@ -0,0 +1,36 @@ +{ + "name": "@backstage/backend-dev-utils", + "version": "0.1.0-next.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "node-library" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/backend-dev-utils" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/packages/backend-dev-utils/src/DevDataStore.test.ts b/packages/backend-dev-utils/src/DevDataStore.test.ts new file mode 100644 index 0000000000..2bb3f221ae --- /dev/null +++ b/packages/backend-dev-utils/src/DevDataStore.test.ts @@ -0,0 +1,121 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Serializable, spawn } from 'child_process'; +import { DevDataStore } from './DevDataStore'; +import { BackstageIpcClient } from './ipcClient'; + +function applyIpcTransform(value: Serializable): Promise { + const child = spawn( + 'node', + [ + '--eval', + ` + const listener = msg => { + process.send(msg); + process.removeListener('message', listener); + } + process.addListener('message', listener); + `, + ], + { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + serialization: 'advanced', + }, + ); + + return new Promise(resolve => { + child.addListener('message', resolve); + child.send(value); + }); +} + +class MockIpcClient implements Pick { + #data = new Map(); + + async request(method: string, body: any): Promise { + if (method === 'DevDataStore.save') { + this.#data.set(body.key, body.data); + return { saved: true }; + } else if (method === 'DevDataStore.load') { + if (!this.#data.has(body.key)) { + return { loaded: false, data: undefined }; + } + const data = this.#data.get(body.key)!; + return { loaded: true, data: await applyIpcTransform(data) }; + } + throw new Error('Unknown message'); + } +} + +describe('DevDataStore', () => { + it('should save and load data', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect(store.save('test', { foo: 'bar' })).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: { foo: 'bar' }, + }); + }); + + it('should save and load buffers', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', Buffer.from('abc', 'ascii')), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: Buffer.from('abc', 'ascii'), + }); + }); + + it('should save and load array buffers', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', new Uint8Array(Buffer.from('abc', 'ascii'))), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: new Uint8Array(Buffer.from('abc', 'ascii')), + }); + }); + + it('should save and load buffers nested in objects', async () => { + const store = DevDataStore.forTest(new MockIpcClient()); + + await expect( + store.save('test', { + x: Buffer.from('x', 'ascii'), + y: [Buffer.from('y', 'ascii')], + }), + ).resolves.toEqual({ + saved: true, + }); + await expect(store.load('test')).resolves.toEqual({ + loaded: true, + data: { x: Buffer.from('x', 'ascii'), y: [Buffer.from('y', 'ascii')] }, + }); + }); +}); diff --git a/packages/backend-dev-utils/src/DevDataStore.ts b/packages/backend-dev-utils/src/DevDataStore.ts new file mode 100644 index 0000000000..4069ab1bdb --- /dev/null +++ b/packages/backend-dev-utils/src/DevDataStore.ts @@ -0,0 +1,99 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { BackstageIpcClient, ipcClient } from './ipcClient'; + +interface SaveRequest { + key: string; + data: unknown; +} + +interface SaveResponse { + saved: boolean; +} + +interface LoadRequest { + key: string; +} + +interface LoadResponse { + loaded: boolean; + data: unknown; +} + +/** + * A data store that can be used to store temporary data during development. + * + * @public + */ +export class DevDataStore { + static #instance?: DevDataStore; + + /** + * Tries to acquire a DevDataStore instance. This will only succeed when the backend + * process is being run through the `@backstage/cli` in development mode. + * + * @returns A DevDataStore instance, or undefined if not available. + */ + static get(): DevDataStore | undefined { + if (ipcClient) { + if (!this.#instance) { + this.#instance = new DevDataStore(ipcClient); + } + return this.#instance; + } + return undefined; + } + + /** @internal */ + static forTest(client: Pick): DevDataStore { + return new DevDataStore(client as BackstageIpcClient); + } + + #client: BackstageIpcClient; + + private constructor(client: BackstageIpcClient) { + this.#client = client; + } + + /** + * Save data to the data store. + * + * @param key - The key used to identify the data. + * @param data - The data to save. The data will be serialized using advanced IPC serialization. + * @returns A promise that resolves to a result object that indicates whether the data was saved. + */ + async save(key: string, data: T): Promise<{ saved: boolean }> { + return this.#client.request( + 'DevDataStore.save', + { key, data }, + ); + } + + /** + * Loads data from the data store. + * + * @param key - The key used to identify the data. + * @returns A promise that resolves to a result object that indicates whether the data was loaded, as well as the data. + */ + async load(key: string): Promise<{ loaded: boolean; data: T }> { + const result = await this.#client.request( + 'DevDataStore.load', + { key }, + ); + return result as { loaded: boolean; data: T }; + } +} diff --git a/packages/backend-dev-utils/src/index.ts b/packages/backend-dev-utils/src/index.ts new file mode 100644 index 0000000000..9b707db029 --- /dev/null +++ b/packages/backend-dev-utils/src/index.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Local development helpers for backend. + * + * @packageDocumentation + */ + +export { DevDataStore } from './DevDataStore'; diff --git a/packages/backend-dev-utils/src/ipcClient.ts b/packages/backend-dev-utils/src/ipcClient.ts new file mode 100644 index 0000000000..87c1596a39 --- /dev/null +++ b/packages/backend-dev-utils/src/ipcClient.ts @@ -0,0 +1,123 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +type SendMessage = Exclude; + +interface Request { + id: number; + method: string; + body: unknown; + type: string; +} + +type Response = + | { + type: string; + id: number; + body: unknown; + } + | { + type: string; + id: number; + error: Error; + }; + +const requestType = '@backstage/cli/channel/request'; +const responseType = '@backstage/cli/channel/response'; + +/** + * The client side of an IPC communication channel. + * + * @internal + */ +export class BackstageIpcClient { + #messageId = 0; + #sendMessage: SendMessage; + + /** + * Creates a new client if we're in a child process with IPC and BACKSTAGE_CLI_CHANNEL is set. + */ + static create(): BackstageIpcClient | undefined { + const sendMessage = process.send?.bind(process); + return sendMessage && process.env.BACKSTAGE_CLI_CHANNEL + ? new BackstageIpcClient(sendMessage) + : undefined; + } + + constructor(sendMessage: SendMessage) { + this.#sendMessage = sendMessage; + } + + /** + * Send a request to the parent process and wait for a response. + */ + async request( + method: string, + body: TRequestBody, + ): Promise { + return new Promise((resolve, reject) => { + const id = this.#messageId++; + + const request: Request = { + type: requestType, + id, + method, + body, + }; + + this.#sendMessage(request, (e: Error) => { + if (e) { + reject(e); + return; + } + + let timeout: NodeJS.Timeout | undefined = undefined; + + const messageHandler = (response: Response) => { + if (response?.type !== responseType) { + return; + } + if (response.id !== id) { + return; + } + + if ('error' in response) { + const error = new Error(response.error.message); + if (response.error.name) { + error.name = response.error.name; + } + reject(error); + } else { + resolve(response.body as TResponseBody); + } + + clearTimeout(timeout); + process.removeListener('message', messageHandler); + }; + + timeout = setTimeout(() => { + reject(new Error(`IPC request '${method}' with ID ${id} timed out`)); + process.removeListener('message', messageHandler); + }, 5000); + timeout.unref(); + + process.addListener('message', messageHandler as () => void); + }); + }); + } +} + +export const ipcClient = BackstageIpcClient.create(); diff --git a/packages/backend-dev-utils/src/setupTests.ts b/packages/backend-dev-utils/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/packages/backend-dev-utils/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md index 914835b1c4..9e8f7a2d83 100644 --- a/packages/backend-next/CHANGELOG.md +++ b/packages/backend-next/CHANGELOG.md @@ -1,5 +1,15 @@ # example-backend-next +## 0.0.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-app-backend@0.3.42-next.2 + - @backstage/backend-defaults@0.1.7-next.2 + ## 0.0.8-next.1 ### Patch Changes diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json index cc537ef514..8f007dc7ea 100644 --- a/packages/backend-next/package.json +++ b/packages/backend-next/package.json @@ -1,6 +1,6 @@ { "name": "example-backend-next", - "version": "0.0.8-next.1", + "version": "0.0.8-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,7 +18,7 @@ "backstage" ], "scripts": { - "start": "backstage-cli package start", + "start": "EXPERIMENTAL_BACKEND_START=1 backstage-cli package start", "build": "backstage-cli package build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md index 3ad0715538..f5aa2dd9fc 100644 --- a/packages/backend-plugin-api/CHANGELOG.md +++ b/packages/backend-plugin-api/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/backend-plugin-api +## 0.4.0-next.2 + +### Minor Changes + +- e716946103: **BREAKING**: Split out the hook for both lifecycle services so that the first parameter of `addShutdownHook` is the hook function, and the second is the options. +- 0ff03319be: **BREAKING**: The plugin ID option passed to `createBackendPlugin` is now `pluginId`, rather than just `id`. This is to make it match `createBackendModule` more closely. +- 71a5ec0f06: **BREAKING**: Switched out `LogMeta` type for `JsonObject`. +- 610d65e143: Switched `BackendFeature` to be an opaque type. + +### Patch Changes + +- 9c9456fd33: Removed the unused `TypesToServiceRef` type +- 181c03edb5: Aligned opaque type markers to all use a `$type` property with namespacing. +- Updated dependencies + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + ## 0.3.2-next.1 ### Patch Changes diff --git a/packages/backend-plugin-api/README.md b/packages/backend-plugin-api/README.md index 364655dd60..02b19a0c40 100644 --- a/packages/backend-plugin-api/README.md +++ b/packages/backend-plugin-api/README.md @@ -1,19 +1,20 @@ # @backstage/backend-plugin-api -**This package is HIGHLY EXPERIMENTAL, do not use this for production** +**This package is EXPERIMENTAL, but we encourage use of it to add support for the new backend system in your own plugins** -This package provides the core API used by Backstage backend plugins. +This package provides the core API used by Backstage backend plugins and modules. ## Installation -Add the library to your backend plugin package: +Add the library to your backend plugin or module package: ```bash # From your Backstage root directory -yarn add --cwd plugin/-backend @backstage/backend-plugin-api +yarn add --cwd plugins/-backend @backstage/backend-plugin-api ``` ## Documentation - [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md) - [Backstage Documentation](https://github.com/backstage/backstage/blob/master/docs/README.md) +- [Backstage Backend System](https://backstage.io/docs/backend-system/) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index a7700d81ac..5e64502b78 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -8,6 +8,7 @@ import { Config } from '@backstage/config'; import { Handler } from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; +import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -17,9 +18,7 @@ import { Readable } from 'stream'; // @public (undocumented) export interface BackendFeature { // (undocumented) - id: string; - // (undocumented) - register(reg: BackendRegistrationPoints): void; + $$type: '@backstage/BackendFeature'; } // @public @@ -47,7 +46,7 @@ export interface BackendModuleRegistrationPoints { // @public export interface BackendPluginConfig { - id: string; + pluginId: string; // (undocumented) register(reg: BackendPluginRegistrationPoints): void; } @@ -73,51 +72,27 @@ export interface BackendPluginRegistrationPoints { } // @public -export interface BackendRegistrationPoints { - // (undocumented) - registerExtensionPoint( - ref: ExtensionPoint, - impl: TExtensionPoint, - ): void; - // (undocumented) - registerInit< - Deps extends { - [name in string]: unknown; - }, - >(options: { - deps: { - [name in keyof Deps]: ServiceRef | ExtensionPoint; - }; - init(deps: Deps): Promise; - }): void; -} - -// @public -export interface CacheClient { +export interface CacheService { delete(key: string): Promise; - get(key: string): Promise; + get(key: string): Promise; set( key: string, value: JsonValue, - options?: CacheClientSetOptions, + options?: CacheServiceSetOptions, ): Promise; + withOptions(options: CacheServiceOptions): CacheService; } // @public -export type CacheClientOptions = { +export type CacheServiceOptions = { defaultTtl?: number; }; // @public -export type CacheClientSetOptions = { +export type CacheServiceSetOptions = { ttl?: number; }; -// @public -export interface CacheService { - getClient: (options?: CacheClientOptions) => CacheClient; -} - // @public (undocumented) export interface ConfigService extends Config {} @@ -166,7 +141,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: RootServiceFactoryConfig, -): () => ServiceFactory; +): () => ServiceFactory; // @public export function createServiceFactory< @@ -178,7 +153,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: (options?: TOpts) => RootServiceFactoryConfig, -): (options?: TOpts) => ServiceFactory; +): (options?: TOpts) => ServiceFactory; // @public export function createServiceFactory< @@ -190,7 +165,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: (options: TOpts) => RootServiceFactoryConfig, -): (options: TOpts) => ServiceFactory; +): (options: TOpts) => ServiceFactory; // @public export function createServiceFactory< @@ -203,7 +178,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: PluginServiceFactoryConfig, -): () => ServiceFactory; +): () => ServiceFactory; // @public export function createServiceFactory< @@ -218,7 +193,7 @@ export function createServiceFactory< config: ( options?: TOpts, ) => PluginServiceFactoryConfig, -): (options?: TOpts) => ServiceFactory; +): (options?: TOpts) => ServiceFactory; // @public export function createServiceFactory< @@ -235,7 +210,7 @@ export function createServiceFactory< | (( options: TOpts, ) => PluginServiceFactoryConfig), -): (options: TOpts) => ServiceFactory; +): (options: TOpts) => ServiceFactory; // @public export function createServiceRef( @@ -275,7 +250,7 @@ export type ExtensionPoint = { id: string; T: T; toString(): string; - $$ref: 'extension-point'; + $$type: '@backstage/ExtensionPoint'; }; // @public @@ -294,34 +269,34 @@ export interface IdentityService extends IdentityApi {} // @public (undocumented) export interface LifecycleService { - addShutdownHook(options: LifecycleServiceShutdownHook): void; + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void; } // @public (undocumented) -export type LifecycleServiceShutdownHook = { - fn: () => void | Promise; +export type LifecycleServiceShutdownHook = () => void | Promise; + +// @public (undocumented) +export interface LifecycleServiceShutdownOptions { logger?: LoggerService; -}; +} // @public export interface LoggerService { // (undocumented) - child(meta: LogMeta): LoggerService; + child(meta: JsonObject): LoggerService; // (undocumented) - debug(message: string, meta?: Error | LogMeta): void; + debug(message: string, meta?: Error | JsonObject): void; // (undocumented) - error(message: string, meta?: Error | LogMeta): void; + error(message: string, meta?: Error | JsonObject): void; // (undocumented) - info(message: string, meta?: Error | LogMeta): void; + info(message: string, meta?: Error | JsonObject): void; // (undocumented) - warn(message: string, meta?: Error | LogMeta): void; + warn(message: string, meta?: Error | JsonObject): void; } -// @public (undocumented) -export type LogMeta = { - [name: string]: unknown; -}; - // @public (undocumented) export interface PermissionsService extends PermissionEvaluator {} @@ -448,38 +423,18 @@ export type SearchResponseFile = { }; // @public (undocumented) -export type ServiceFactory = - | { - scope: 'root'; - service: ServiceRef; - deps: { - [key in string]: ServiceRef; - }; - factory(deps: { - [key in string]: unknown; - }): Promise; - } - | { - scope: 'plugin'; - service: ServiceRef; - deps: { - [key in string]: ServiceRef; - }; - createRootContext?(deps: { - [key in string]: unknown; - }): Promise; - factory( - deps: { - [key in string]: unknown; - }, - context: unknown, - ): Promise; - }; +export interface ServiceFactory< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', +> { + // (undocumented) + $$type: '@backstage/ServiceFactory'; + // (undocumented) + service: ServiceRef; +} // @public -export type ServiceFactoryOrFunction = - | ServiceFactory - | (() => ServiceFactory); +export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); // @public export type ServiceRef< @@ -490,7 +445,7 @@ export type ServiceRef< scope: TScope; T: TService; toString(): string; - $$ref: 'service'; + $$type: '@backstage/ServiceRef'; }; // @public (undocumented) @@ -498,7 +453,7 @@ export interface ServiceRefConfig { // (undocumented) defaultFactory?: ( service: ServiceRef, - ) => Promise>; + ) => Promise; // (undocumented) id: string; // (undocumented) @@ -508,7 +463,7 @@ export interface ServiceRefConfig { // @public export interface SharedBackendEnvironment { // (undocumented) - $$type: 'SharedBackendEnvironment'; + $$type: '@backstage/SharedBackendEnvironment'; } // @public @@ -525,11 +480,6 @@ export interface TokenManagerService { }>; } -// @public (undocumented) -export type TypesToServiceRef = { - [key in keyof T]: ServiceRef; -}; - // @public export interface UrlReaderService { readTree(url: string, options?: ReadTreeOptions): Promise; diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json index 954112bc2d..7243f3485c 100644 --- a/packages/backend-plugin-api/package.json +++ b/packages/backend-plugin-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-plugin-api", "description": "Core API used by Backstage backend plugins", - "version": "0.3.2-next.1", + "version": "0.4.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-plugin-api/src/services/definitions/CacheService.ts b/packages/backend-plugin-api/src/services/definitions/CacheService.ts index 2702fdac6d..ccf88823b0 100644 --- a/packages/backend-plugin-api/src/services/definitions/CacheService.ts +++ b/packages/backend-plugin-api/src/services/definitions/CacheService.ts @@ -17,29 +17,11 @@ import { JsonValue } from '@backstage/types'; /** - * Manages access to cache stores that plugins get. + * Options passed to {@link CacheService.set}. * * @public */ -export interface CacheService { - /** - * Provides backend plugins cache connections for themselves. - * - * @remarks - * - * The purpose of this method is to allow plugins to get isolated data stores - * so that plugins are discouraged from cache-level integration and/or cache - * key collisions. - */ - getClient: (options?: CacheClientOptions) => CacheClient; -} - -/** - * Options passed to {@link CacheClient.set}. - * - * @public - */ -export type CacheClientSetOptions = { +export type CacheServiceSetOptions = { /** * Optional TTL in milliseconds. Defaults to the TTL provided when the client * was set up (or no TTL if none are provided). @@ -48,17 +30,31 @@ export type CacheClientSetOptions = { }; /** - * A pre-configured, storage agnostic cache client suitable for use by + * Options passed to {@link CacheService.withOptions}. + * + * @public + */ +export type CacheServiceOptions = { + /** + * An optional default TTL (in milliseconds) to be set when getting a client + * instance. If not provided, data will persist indefinitely by default (or + * can be configured per entry at set-time). + */ + defaultTtl?: number; +}; + +/** + * A pre-configured, storage agnostic cache service suitable for use by * Backstage plugins. * * @public */ -export interface CacheClient { +export interface CacheService { /** * Reads data from a cache store for the given key. If no data was found, * returns undefined. */ - get(key: string): Promise; + get(key: string): Promise; /** * Writes the given data to a cache store, associated with the given key. An @@ -68,25 +64,16 @@ export interface CacheClient { set( key: string, value: JsonValue, - options?: CacheClientSetOptions, + options?: CacheServiceSetOptions, ): Promise; /** * Removes the given key from the cache store. */ delete(key: string): Promise; -} -/** - * Options given when constructing a {@link CacheClient}. - * - * @public - */ -export type CacheClientOptions = { /** - * An optional default TTL (in milliseconds) to be set when getting a client - * instance. If not provided, data will persist indefinitely by default (or - * can be configured per entry at set-time). + * Creates a new {@link CacheService} instance with the given options. */ - defaultTtl?: number; -}; + withOptions(options: CacheServiceOptions): CacheService; +} diff --git a/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts b/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts index 25cc47270e..89351194e6 100644 --- a/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts +++ b/packages/backend-plugin-api/src/services/definitions/LifecycleService.ts @@ -18,22 +18,28 @@ import { LoggerService } from './LoggerService'; /** * @public - **/ -export type LifecycleServiceShutdownHook = { - fn: () => void | Promise; + */ +export type LifecycleServiceShutdownHook = () => void | Promise; +/** + * @public + */ +export interface LifecycleServiceShutdownOptions { /** * Optional {@link LoggerService} that will be used for logging instead of the default logger. */ logger?: LoggerService; -}; +} /** * @public - **/ + */ export interface LifecycleService { /** * Register a function to be called when the backend is shutting down. */ - addShutdownHook(options: LifecycleServiceShutdownHook): void; + addShutdownHook( + hook: LifecycleServiceShutdownHook, + options?: LifecycleServiceShutdownOptions, + ): void; } diff --git a/packages/backend-plugin-api/src/services/definitions/LoggerService.ts b/packages/backend-plugin-api/src/services/definitions/LoggerService.ts index 269dfa0166..2e4edc60be 100644 --- a/packages/backend-plugin-api/src/services/definitions/LoggerService.ts +++ b/packages/backend-plugin-api/src/services/definitions/LoggerService.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -/** - * @public - */ -export type LogMeta = { [name: string]: unknown }; +import { JsonObject } from '@backstage/types'; /** * A service that provides a logging facility. @@ -25,10 +22,10 @@ export type LogMeta = { [name: string]: unknown }; * @public */ export interface LoggerService { - error(message: string, meta?: Error | LogMeta): void; - warn(message: string, meta?: Error | LogMeta): void; - info(message: string, meta?: Error | LogMeta): void; - debug(message: string, meta?: Error | LogMeta): void; + error(message: string, meta?: Error | JsonObject): void; + warn(message: string, meta?: Error | JsonObject): void; + info(message: string, meta?: Error | JsonObject): void; + debug(message: string, meta?: Error | JsonObject): void; - child(meta: LogMeta): LoggerService; + child(meta: JsonObject): LoggerService; } diff --git a/packages/backend-plugin-api/src/services/definitions/index.ts b/packages/backend-plugin-api/src/services/definitions/index.ts index c5af0967bd..0122bc0be8 100644 --- a/packages/backend-plugin-api/src/services/definitions/index.ts +++ b/packages/backend-plugin-api/src/services/definitions/index.ts @@ -17,9 +17,8 @@ export { coreServices } from './coreServices'; export type { CacheService, - CacheClient, - CacheClientOptions, - CacheClientSetOptions, + CacheServiceOptions, + CacheServiceSetOptions, } from './CacheService'; export type { ConfigService } from './ConfigService'; export type { DatabaseService } from './DatabaseService'; @@ -28,8 +27,9 @@ export type { HttpRouterService } from './HttpRouterService'; export type { LifecycleService, LifecycleServiceShutdownHook, + LifecycleServiceShutdownOptions, } from './LifecycleService'; -export type { LoggerService, LogMeta } from './LoggerService'; +export type { LoggerService } from './LoggerService'; export type { PermissionsService } from './PermissionsService'; export type { PluginMetadataService } from './PluginMetadataService'; export type { RootHttpRouterService } from './RootHttpRouterService'; diff --git a/packages/backend-plugin-api/src/services/system/index.ts b/packages/backend-plugin-api/src/services/system/index.ts index 74be9f8b11..2b91758cbc 100644 --- a/packages/backend-plugin-api/src/services/system/index.ts +++ b/packages/backend-plugin-api/src/services/system/index.ts @@ -17,7 +17,6 @@ export type { ServiceRef, ServiceRefConfig, - TypesToServiceRef, ServiceFactory, PluginServiceFactoryConfig, RootServiceFactoryConfig, diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 2615c5e3a3..190ef7c13e 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -44,41 +44,39 @@ export type ServiceRef< toString(): string; - $$ref: 'service'; + $$type: '@backstage/ServiceRef'; }; /** @public */ -export type TypesToServiceRef = { [key in keyof T]: ServiceRef }; +export interface ServiceFactory< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', +> { + $$type: '@backstage/ServiceFactory'; -/** @public */ -export type ServiceFactory = - | { - // This scope prop is needed in addition to the service ref, as TypeScript - // can't properly discriminate the two factory types otherwise. - scope: 'root'; - service: ServiceRef; - deps: { [key in string]: ServiceRef }; - factory(deps: { [key in string]: unknown }): Promise; - } - | { - scope: 'plugin'; - service: ServiceRef; - deps: { [key in string]: ServiceRef }; - createRootContext?(deps: { [key in string]: unknown }): Promise; - factory( - deps: { [key in string]: unknown }, - context: unknown, - ): Promise; - }; + service: ServiceRef; +} + +/** @internal */ +export interface InternalServiceFactory< + TService = unknown, + TScope extends 'plugin' | 'root' = 'plugin' | 'root', +> extends ServiceFactory { + version: 'v1'; + deps: { [key in string]: ServiceRef }; + createRootContext?(deps: { [key in string]: unknown }): Promise; + factory( + deps: { [key in string]: unknown }, + context: unknown, + ): Promise; +} /** * Represents either a {@link ServiceFactory} or a function that returns one. * * @public */ -export type ServiceFactoryOrFunction = - | ServiceFactory - | (() => ServiceFactory); +export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); /** @public */ export interface ServiceRefConfig { @@ -86,7 +84,7 @@ export interface ServiceRefConfig { scope?: TScope; defaultFactory?: ( service: ServiceRef, - ) => Promise>; + ) => Promise; } /** @@ -119,7 +117,7 @@ export function createServiceRef( toString() { return `serviceRef{${config.id}}`; }, - $$ref: 'service', // TODO: declare + $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, } as ServiceRef & { __defaultFactory?: ( @@ -178,7 +176,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: RootServiceFactoryConfig, -): () => ServiceFactory; +): () => ServiceFactory; /** * Creates a root scoped service factory with optional options. * @@ -192,7 +190,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: (options?: TOpts) => RootServiceFactoryConfig, -): (options?: TOpts) => ServiceFactory; +): (options?: TOpts) => ServiceFactory; /** * Creates a root scoped service factory with required options. * @@ -206,7 +204,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: (options: TOpts) => RootServiceFactoryConfig, -): (options: TOpts) => ServiceFactory; +): (options: TOpts) => ServiceFactory; /** * Creates a plugin scoped service factory without options. * @@ -221,7 +219,7 @@ export function createServiceFactory< TOpts extends object | undefined = undefined, >( config: PluginServiceFactoryConfig, -): () => ServiceFactory; +): () => ServiceFactory; /** * Creates a plugin scoped service factory with optional options. * @@ -238,7 +236,7 @@ export function createServiceFactory< config: ( options?: TOpts, ) => PluginServiceFactoryConfig, -): (options?: TOpts) => ServiceFactory; +): (options?: TOpts) => ServiceFactory; /** * Creates a plugin scoped service factory with required options. * @@ -257,7 +255,7 @@ export function createServiceFactory< | (( options: TOpts, ) => PluginServiceFactoryConfig), -): (options: TOpts) => ServiceFactory; +): (options: TOpts) => ServiceFactory; export function createServiceFactory< TService, TImpl extends TService, @@ -274,20 +272,40 @@ export function createServiceFactory< ) => PluginServiceFactoryConfig) | (() => RootServiceFactoryConfig) | (() => PluginServiceFactoryConfig), -): (options: TOpts) => ServiceFactory { +): (options: TOpts) => ServiceFactory { const configCallback = typeof config === 'function' ? config : () => config; - return (options: TOpts) => { - const c = configCallback(options); + return ( + options: TOpts, + ): InternalServiceFactory => { + const anyConf = configCallback(options); + if (anyConf.service.scope === 'root') { + const c = anyConf as RootServiceFactoryConfig; + return { + $$type: '@backstage/ServiceFactory', + version: 'v1', + service: c.service, + deps: c.deps, + factory: async (deps: TDeps) => c.factory(deps), + }; + } + const c = anyConf as PluginServiceFactoryConfig< + TService, + TContext, + TImpl, + TDeps + >; return { - ...c, + $$type: '@backstage/ServiceFactory', + version: 'v1', + service: c.service, ...('createRootContext' in c ? { createRootContext: async (deps: TDeps) => c?.createRootContext?.(deps), } : {}), + deps: c.deps, factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx), - scope: c.service.scope, - } as ServiceFactory; + }; }; } diff --git a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts index ec2cf1fe49..381a4799d2 100644 --- a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts +++ b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.test.ts @@ -48,7 +48,7 @@ describe('createSharedEnvironment', () => { expect(env).toBeDefined(); const internalEnv = env() as unknown as InternalSharedBackendEnvironment; expect(internalEnv).toEqual({ - $$type: 'SharedBackendEnvironment', + $$type: '@backstage/SharedBackendEnvironment', version: 'v1', services: undefined, }); diff --git a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts index 2eb2cdec6a..57f0533523 100644 --- a/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts +++ b/packages/backend-plugin-api/src/wiring/createSharedEnvironment.ts @@ -31,7 +31,7 @@ export interface SharedBackendEnvironmentConfig { * @public */ export interface SharedBackendEnvironment { - $$type: 'SharedBackendEnvironment'; + $$type: '@backstage/SharedBackendEnvironment'; // NOTE: This type is opaque in order to allow for future API evolution without // cluttering the external API. For example we might want to add support @@ -93,7 +93,7 @@ export function createSharedEnvironment< // Here to ensure type safety in this internal implementation. const env: SharedBackendEnvironment & InternalSharedBackendEnvironment = { - $$type: 'SharedBackendEnvironment', + $$type: '@backstage/SharedBackendEnvironment', version: 'v1', services, }; diff --git a/packages/backend-plugin-api/src/wiring/factories.test.ts b/packages/backend-plugin-api/src/wiring/factories.test.ts index dbdf0ead14..ae199d84e4 100644 --- a/packages/backend-plugin-api/src/wiring/factories.test.ts +++ b/packages/backend-plugin-api/src/wiring/factories.test.ts @@ -19,6 +19,7 @@ import { createBackendPlugin, createExtensionPoint, } from './factories'; +import { InternalBackendFeature } from './types'; describe('createExtensionPoint', () => { it('should create an ExtensionPoint', () => { @@ -33,12 +34,31 @@ describe('createExtensionPoint', () => { describe('createBackendPlugin', () => { it('should create a BackendPlugin', () => { const plugin = createBackendPlugin((_options: { a: string }) => ({ - id: 'x', - register() {}, + pluginId: 'x', + register(r) { + r.registerInit({ deps: {}, async init() {} }); + }, })); expect(plugin).toBeDefined(); expect(plugin({ a: 'a' })).toBeDefined(); - expect(plugin({ a: 'a' }).id).toBe('x'); + expect(plugin({ a: 'a' })).toEqual({ + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: expect.any(Function), + }); + expect( + (plugin({ a: 'a' }) as InternalBackendFeature).getRegistrations(), + ).toEqual([ + { + type: 'plugin', + pluginId: 'x', + extensionPoints: [], + init: { + deps: expect.any(Object), + func: expect.any(Function), + }, + }, + ]); // @ts-expect-error expect(plugin()).toBeDefined(); // @ts-expect-error @@ -47,7 +67,7 @@ describe('createBackendPlugin', () => { it('should create plugins with optional options', () => { const plugin = createBackendPlugin((_options?: { a: string }) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); @@ -59,7 +79,7 @@ describe('createBackendPlugin', () => { it('should create plugins without options', () => { const plugin = createBackendPlugin({ - id: 'x', + pluginId: 'x', register() {}, }); expect(plugin).toBeDefined(); @@ -74,12 +94,16 @@ describe('createBackendPlugin', () => { a: string; } const plugin = createBackendPlugin((_options: TestOptions) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); expect(plugin({ a: 'a' })).toBeDefined(); - expect(plugin({ a: 'a' }).id).toBe('x'); + expect(plugin({ a: 'a' })).toEqual({ + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: expect.any(Function), + }); // @ts-expect-error expect(plugin()).toBeDefined(); // @ts-expect-error @@ -91,7 +115,7 @@ describe('createBackendPlugin', () => { a: string; } const plugin = createBackendPlugin((_options?: TestOptions) => ({ - id: 'x', + pluginId: 'x', register() {}, })); expect(plugin).toBeDefined(); @@ -107,11 +131,30 @@ describe('createBackendModule', () => { const mod = createBackendModule((_options: { a: string }) => ({ pluginId: 'x', moduleId: 'y', - register() {}, + register(r) { + r.registerInit({ deps: {}, async init() {} }); + }, })); expect(mod).toBeDefined(); expect(mod({ a: 'a' })).toBeDefined(); - expect(mod({ a: 'a' }).id).toBe('x.y'); + expect(mod({ a: 'a' })).toEqual({ + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: expect.any(Function), + }); + expect( + (mod({ a: 'a' }) as InternalBackendFeature).getRegistrations(), + ).toEqual([ + { + type: 'module', + pluginId: 'x', + moduleId: 'y', + init: { + deps: expect.any(Object), + func: expect.any(Function), + }, + }, + ]); // @ts-expect-error expect(mod()).toBeDefined(); // @ts-expect-error @@ -155,7 +198,11 @@ describe('createBackendModule', () => { })); expect(mod).toBeDefined(); expect(mod({ a: 'a' })).toBeDefined(); - expect(mod({ a: 'a' }).id).toBe('x.y'); + expect(mod({ a: 'a' })).toEqual({ + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations: expect.any(Function), + }); // @ts-expect-error expect(mod()).toBeDefined(); // @ts-expect-error diff --git a/packages/backend-plugin-api/src/wiring/factories.ts b/packages/backend-plugin-api/src/wiring/factories.ts index d281335a8e..928c933af0 100644 --- a/packages/backend-plugin-api/src/wiring/factories.ts +++ b/packages/backend-plugin-api/src/wiring/factories.ts @@ -17,9 +17,11 @@ import { BackendModuleRegistrationPoints, BackendPluginRegistrationPoints, - BackendRegistrationPoints, BackendFeature, ExtensionPoint, + InternalBackendFeature, + InternalBackendModuleRegistration, + InternalBackendPluginRegistration, } from './types'; /** @@ -55,7 +57,7 @@ export function createExtensionPoint( toString() { return `extensionPoint{${config.id}}`; }, - $$ref: 'extension-point', // TODO: declare + $$type: '@backstage/ExtensionPoint', }; } @@ -72,7 +74,7 @@ export interface BackendPluginConfig { * * @see {@link https://backstage.io/docs/backend-system/architecture/naming-patterns | Recommended naming patterns} */ - id: string; + pluginId: string; register(reg: BackendPluginRegistrationPoints): void; } @@ -86,11 +88,62 @@ export interface BackendPluginConfig { export function createBackendPlugin( config: BackendPluginConfig | ((...params: TOptions) => BackendPluginConfig), ): (...params: TOptions) => BackendFeature { - if (typeof config === 'function') { - return config; - } + const configCallback = typeof config === 'function' ? config : () => config; + return (...options: TOptions): InternalBackendFeature => { + const c = configCallback(...options); - return () => config; + let registrations: InternalBackendPluginRegistration[]; + + return { + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations() { + if (registrations) { + return registrations; + } + const extensionPoints: InternalBackendPluginRegistration['extensionPoints'] = + []; + let init: InternalBackendPluginRegistration['init'] | undefined = + undefined; + + c.register({ + registerExtensionPoint(ext, impl) { + if (init) { + throw new Error( + 'registerExtensionPoint called after registerInit', + ); + } + extensionPoints.push([ext, impl]); + }, + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${c.pluginId}`, + ); + } + + registrations = [ + { + type: 'plugin', + pluginId: c.pluginId, + extensionPoints, + init, + }, + ]; + return registrations; + }, + }; + }; } /** @@ -124,21 +177,50 @@ export interface BackendModuleConfig { export function createBackendModule( config: BackendModuleConfig | ((...params: TOptions) => BackendModuleConfig), ): (...params: TOptions) => BackendFeature { - if (typeof config === 'function') { - return (...options: TOptions) => { - const c = config(...options); - return { - id: `${c.pluginId}.${c.moduleId}`, - register: c.register, - }; + const configCallback = typeof config === 'function' ? config : () => config; + return (...options: TOptions): InternalBackendFeature => { + const c = configCallback(...options); + + let registrations: InternalBackendModuleRegistration[]; + + return { + $$type: '@backstage/BackendFeature', + version: 'v1', + getRegistrations() { + if (registrations) { + return registrations; + } + let init: InternalBackendModuleRegistration['init'] | undefined = + undefined; + + c.register({ + registerInit(regInit) { + if (init) { + throw new Error('registerInit must only be called once'); + } + init = { + deps: regInit.deps, + func: regInit.init, + }; + }, + }); + + if (!init) { + throw new Error( + `registerInit was not called by register in ${c.moduleId} module for ${c.pluginId}`, + ); + } + + registrations = [ + { + type: 'module', + pluginId: c.pluginId, + moduleId: c.moduleId, + init, + }, + ]; + return registrations; + }, }; - } - return () => ({ - id: `${config.pluginId}.${config.moduleId}`, - register(register: BackendRegistrationPoints) { - return config.register({ - registerInit: register.registerInit.bind(register), - }); - }, - }); + }; } diff --git a/packages/backend-plugin-api/src/wiring/index.ts b/packages/backend-plugin-api/src/wiring/index.ts index e0c25eeb4e..242301ea4d 100644 --- a/packages/backend-plugin-api/src/wiring/index.ts +++ b/packages/backend-plugin-api/src/wiring/index.ts @@ -32,7 +32,6 @@ export { export type { BackendModuleRegistrationPoints, BackendPluginRegistrationPoints, - BackendRegistrationPoints, BackendFeature, ExtensionPoint, } from './types'; diff --git a/packages/backend-plugin-api/src/wiring/types.ts b/packages/backend-plugin-api/src/wiring/types.ts index 6e081f57e5..c81ee3ceb7 100644 --- a/packages/backend-plugin-api/src/wiring/types.ts +++ b/packages/backend-plugin-api/src/wiring/types.ts @@ -32,29 +32,9 @@ export type ExtensionPoint = { toString(): string; - $$ref: 'extension-point'; + $$type: '@backstage/ExtensionPoint'; }; -/** - * The callbacks passed to the `register` method of a backend feature; this is - * essentially a superset of {@link BackendPluginRegistrationPoints} and - * {@link BackendModuleRegistrationPoints}. - * - * @public - */ -export interface BackendRegistrationPoints { - registerExtensionPoint( - ref: ExtensionPoint, - impl: TExtensionPoint, - ): void; - registerInit(options: { - deps: { - [name in keyof Deps]: ServiceRef | ExtensionPoint; - }; - init(deps: Deps): Promise; - }): void; -} - /** * The callbacks passed to the `register` method of a backend plugin. * @@ -89,7 +69,36 @@ export interface BackendModuleRegistrationPoints { /** @public */ export interface BackendFeature { - // TODO(Rugvip): Try to get rid of the ID at this level, allowing for a feature to register multiple features as a bundle - id: string; - register(reg: BackendRegistrationPoints): void; + // NOTE: This type is opaque in order to simplify future API evolution. + $$type: '@backstage/BackendFeature'; +} + +/** @internal */ +export interface InternalBackendFeature extends BackendFeature { + version: 'v1'; + getRegistrations(): Array< + InternalBackendPluginRegistration | InternalBackendModuleRegistration + >; +} + +/** @internal */ +export interface InternalBackendPluginRegistration { + pluginId: string; + type: 'plugin'; + extensionPoints: Array, unknown]>; + init: { + deps: Record>; + func(deps: Record): Promise; + }; +} + +/** @internal */ +export interface InternalBackendModuleRegistration { + pluginId: string; + moduleId: string; + type: 'module'; + init: { + deps: Record | ExtensionPoint>; + func(deps: Record): Promise; + }; } diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md index 04ce4e9c3b..1e32f34978 100644 --- a/packages/backend-tasks/CHANGELOG.md +++ b/packages/backend-tasks/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/backend-tasks +## 0.4.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + ## 0.4.3-next.1 ### Patch Changes diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json index 574f3dc812..ee7d7d6e67 100644 --- a/packages/backend-tasks/package.json +++ b/packages/backend-tasks/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-tasks", "description": "Common distributed task management library for Backstage backends", - "version": "0.4.3-next.1", + "version": "0.4.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md index f532e08d81..bf7b6c6713 100644 --- a/packages/backend-test-utils/CHANGELOG.md +++ b/packages/backend-test-utils/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/backend-test-utils +## 0.1.34-next.2 + +### Patch Changes + +- baf6e4c96a: Removed unnecessary `@backstage/cli` dependency. +- 1835311713: Added explicit return type signature for `mockServices.config()`. +- e716946103: Updated usage of the lifecycle service. +- 7e7557a2be: Updated the `mockServices.rootLogger` options to accept a single level option instead. +- 610d65e143: Updates to match new `BackendFeature` type. +- e412d33025: Use the new `*ServiceFactory` exports from `@backstage/backend-app-api` +- f1adb2e36b: Removed the `ref` from all `mockServices`. +- 71a5ec0f06: Updated usages of `LogMeta`. +- Updated dependencies + - @backstage/backend-app-api@0.4.0-next.2 + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + ## 0.1.34-next.1 ### Patch Changes diff --git a/packages/backend-test-utils/README.md b/packages/backend-test-utils/README.md index 38756fa5d7..7ccf4e2798 100644 --- a/packages/backend-test-utils/README.md +++ b/packages/backend-test-utils/README.md @@ -7,7 +7,7 @@ Test helpers library for Backstage backends. Add the library as a `devDependency` to your backend package: ```sh -# From the Backstage root directory, go to your backend package, or to a backend plugin +# From your Backstage root directory, go to your backend package, or to a backend plugin cd plugins/my-plugin-backend yarn add --dev @backstage/backend-test-utils ``` diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 232f7e8136..c13d1bb859 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -6,7 +6,6 @@ import { Backend } from '@backstage/backend-app-api'; import { BackendFeature } from '@backstage/backend-plugin-api'; import { CacheService } from '@backstage/backend-plugin-api'; -import { ConfigReader } from '@backstage/config'; import { ConfigService } from '@backstage/backend-plugin-api'; import { DatabaseService } from '@backstage/backend-plugin-api'; import { ExtendedHttpServer } from '@backstage/backend-app-api'; @@ -20,7 +19,6 @@ import { LifecycleService } from '@backstage/backend-plugin-api'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PermissionsService } from '@backstage/backend-plugin-api'; import { RootLifecycleService } from '@backstage/backend-plugin-api'; -import { RootLoggerService } from '@backstage/backend-plugin-api'; import { SchedulerService } from '@backstage/backend-plugin-api'; import { ServiceFactory } from '@backstage/backend-plugin-api'; import { ServiceRef } from '@backstage/backend-plugin-api'; @@ -30,17 +28,15 @@ import { UrlReaderService } from '@backstage/backend-plugin-api'; // @public (undocumented) export function isDockerDisabledForTests(): boolean; -// @alpha (undocumented) +// @public (undocumented) export namespace mockServices { // (undocumented) export namespace cache { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) - export function config(options?: config.Options): ConfigReader; + export function config(options?: config.Options): ConfigService; // (undocumented) export namespace config { // (undocumented) @@ -48,62 +44,48 @@ export namespace mockServices { data?: JsonObject; }; const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: (options?: Options | undefined) => ServiceFactory; + factory: ( + options?: Options | undefined, + ) => ServiceFactory; } // (undocumented) export namespace database { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) export namespace httpRouter { - const // (undocumented) - ref: ServiceRef; const // (undocumented) factory: ( options?: HttpRouterFactoryOptions | undefined, - ) => ServiceFactory; + ) => ServiceFactory; } // (undocumented) export function identity(): IdentityService; // (undocumented) export namespace identity { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) export namespace lifecycle { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) export namespace logger { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) export namespace permissions { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) export namespace rootLifecycle { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) export function rootLogger(options?: rootLogger.Options): LoggerService; @@ -111,42 +93,29 @@ export namespace mockServices { export namespace rootLogger { // (undocumented) export type Options = { - levels: - | boolean - | { - error: boolean; - warn: boolean; - info: boolean; - debug: boolean; - }; + level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: (options?: Options | undefined) => ServiceFactory; + factory: ( + options?: Options | undefined, + ) => ServiceFactory; } // (undocumented) export namespace scheduler { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) export function tokenManager(): TokenManagerService; // (undocumented) export namespace tokenManager { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } // (undocumented) export namespace urlReader { const // (undocumented) - ref: ServiceRef; - const // (undocumented) - factory: () => ServiceFactory; + factory: () => ServiceFactory; } } @@ -157,7 +126,7 @@ export function setupRequestMockHandlers(worker: { resetHandlers: () => void; }): void; -// @alpha (undocumented) +// @public (undocumented) export function startTestBackend< TServices extends any[], TExtensionPoints extends any[], @@ -165,12 +134,12 @@ export function startTestBackend< options: TestBackendOptions, ): Promise; -// @alpha (undocumented) +// @public (undocumented) export interface TestBackend extends Backend { readonly server: ExtendedHttpServer; } -// @alpha (undocumented) +// @public (undocumented) export interface TestBackendOptions< TServices extends any[], TExtensionPoints extends any[], diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json index a01dbaf0fa..08e00e1c5e 100644 --- a/packages/backend-test-utils/package.json +++ b/packages/backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-test-utils", "description": "Test helpers library for Backstage backends", - "version": "0.1.34-next.1", + "version": "0.1.34-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { @@ -37,7 +37,6 @@ "@backstage/backend-app-api": "workspace:^", "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", - "@backstage/cli": "workspace:^", "@backstage/config": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", "@backstage/types": "workspace:^", diff --git a/packages/backend-test-utils/src/next/services/MockRootLoggerService.test.ts b/packages/backend-test-utils/src/next/services/MockRootLoggerService.test.ts new file mode 100644 index 0000000000..a3cbb5c384 --- /dev/null +++ b/packages/backend-test-utils/src/next/services/MockRootLoggerService.test.ts @@ -0,0 +1,131 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MockRootLoggerService } from './MockRootLoggerService'; + +describe('MockRootLoggerService', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should be silent by default', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create(); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set none level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'none' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).not.toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set error level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'error' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).toHaveBeenCalled(); + expect(console.warn).not.toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set warn level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'warn' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + expect(console.info).not.toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set info level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'info' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalled(); + expect(console.debug).not.toHaveBeenCalled(); + }); + + it('shuld be able to set debug level', () => { + jest.spyOn(console, 'error').mockImplementation(() => {}); + jest.spyOn(console, 'warn').mockImplementation(() => {}); + jest.spyOn(console, 'info').mockImplementation(() => {}); + jest.spyOn(console, 'debug').mockImplementation(() => {}); + + const logger = MockRootLoggerService.create({ level: 'debug' }); + logger.error('error'); + logger.warn('warn'); + logger.info('info'); + logger.debug('debug'); + + expect(console.error).toHaveBeenCalled(); + expect(console.warn).toHaveBeenCalled(); + expect(console.info).toHaveBeenCalled(); + expect(console.debug).toHaveBeenCalled(); + }); +}); diff --git a/packages/backend-test-utils/src/next/services/MockRootLoggerService.ts b/packages/backend-test-utils/src/next/services/MockRootLoggerService.ts index 780d330bfb..8b973eeeab 100644 --- a/packages/backend-test-utils/src/next/services/MockRootLoggerService.ts +++ b/packages/backend-test-utils/src/next/services/MockRootLoggerService.ts @@ -16,58 +16,65 @@ import { LoggerService, - LogMeta, RootLoggerService, } from '@backstage/backend-plugin-api'; +import { JsonObject } from '@backstage/types'; import type { mockServices } from './mockServices'; -export class MockRootLoggerService implements RootLoggerService { - #levels: Exclude; - #meta: LogMeta; +const levels = { + none: 0, + error: 1, + warn: 2, + info: 3, + debug: 4, +}; - error(message: string, meta?: LogMeta | Error | undefined): void { +export class MockRootLoggerService implements RootLoggerService { + #level: number; + #meta: JsonObject; + + static create( + options?: mockServices.rootLogger.Options, + ): MockRootLoggerService { + const level = options?.level ?? 'none'; + if (!(level in levels)) { + throw new Error(`Invalid log level '${level}'`); + } + return new MockRootLoggerService(levels[level], {}); + } + + error(message: string, meta?: JsonObject | Error | undefined): void { this.#log('error', message, meta); } - warn(message: string, meta?: LogMeta | Error | undefined): void { + warn(message: string, meta?: JsonObject | Error | undefined): void { this.#log('warn', message, meta); } - info(message: string, meta?: LogMeta | Error | undefined): void { + info(message: string, meta?: JsonObject | Error | undefined): void { this.#log('info', message, meta); } - debug(message: string, meta?: LogMeta | Error | undefined): void { + debug(message: string, meta?: JsonObject | Error | undefined): void { this.#log('debug', message, meta); } - child(meta: LogMeta): LoggerService { - return new MockRootLoggerService(this.#levels, { ...this.#meta, ...meta }); + child(meta: JsonObject): LoggerService { + return new MockRootLoggerService(this.#level, { ...this.#meta, ...meta }); } - constructor( - levels: mockServices.rootLogger.Options['levels'], - meta: LogMeta, - ) { - if (typeof levels === 'boolean') { - this.#levels = { - error: levels, - debug: levels, - info: levels, - warn: levels, - }; - } else { - this.#levels = levels; - } + private constructor(level: number, meta: JsonObject) { + this.#level = level; this.#meta = meta; } #log( level: 'error' | 'warn' | 'info' | 'debug', message: string, - meta?: LogMeta | Error | undefined, + meta?: JsonObject | Error | undefined, ) { - if (this.#levels[level]) { + const levelValue = levels[level] ?? 0; + if (levelValue <= this.#level) { const labels = Object.entries(this.#meta) .map(([key, value]) => `${key}=${value}`) .join(','); diff --git a/packages/backend-test-utils/src/next/services/mockServices.ts b/packages/backend-test-utils/src/next/services/mockServices.ts index f86d8bac55..8a3125214c 100644 --- a/packages/backend-test-utils/src/next/services/mockServices.ts +++ b/packages/backend-test-utils/src/next/services/mockServices.ts @@ -15,6 +15,7 @@ */ import { + ConfigService, coreServices, createServiceFactory, IdentityService, @@ -24,60 +25,60 @@ import { TokenManagerService, } from '@backstage/backend-plugin-api'; import { - cacheFactory, - databaseFactory, - httpRouterFactory, - lifecycleFactory, - loggerFactory, - permissionsFactory, - rootLifecycleFactory, - schedulerFactory, - urlReaderFactory, + cacheServiceFactory, + databaseServiceFactory, + httpRouterServiceFactory, + lifecycleServiceFactory, + loggerServiceFactory, + permissionsServiceFactory, + rootLifecycleServiceFactory, + schedulerServiceFactory, + urlReaderServiceFactory, } from '@backstage/backend-app-api'; import { ConfigReader } from '@backstage/config'; import { JsonObject } from '@backstage/types'; import { MockIdentityService } from './MockIdentityService'; import { MockRootLoggerService } from './MockRootLoggerService'; -function simpleFactory( - ref: ServiceRef, +function simpleFactory< + TService, + TScope extends 'root' | 'plugin', + TOptions extends [options?: object] = [], +>( + ref: ServiceRef, factory: (...options: TOptions) => TService, -): (...options: TOptions) => ServiceFactory { +): (...options: TOptions) => ServiceFactory { return createServiceFactory((options: unknown) => ({ service: ref as ServiceRef, deps: {}, async factory() { return (factory as any)(options); }, - })); + })) as (...options: TOptions) => ServiceFactory; } /** - * @alpha + * @public */ export namespace mockServices { - export function config(options?: config.Options) { + export function config(options?: config.Options): ConfigService { return new ConfigReader(options?.data, 'mock-config'); } export namespace config { export type Options = { data?: JsonObject }; - export const ref = coreServices.config; - export const factory = simpleFactory(ref, config); + export const factory = simpleFactory(coreServices.config, config); } export function rootLogger(options?: rootLogger.Options): LoggerService { - return new MockRootLoggerService(options?.levels ?? false, {}); + return MockRootLoggerService.create(options); } export namespace rootLogger { export type Options = { - levels: - | boolean - | { error: boolean; warn: boolean; info: boolean; debug: boolean }; + level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; - export const ref = coreServices.rootLogger; - export const factory = simpleFactory(ref, rootLogger); + export const factory = simpleFactory(coreServices.rootLogger, rootLogger); } export function tokenManager(): TokenManagerService { @@ -93,55 +94,47 @@ export namespace mockServices { }; } export namespace tokenManager { - export const ref = coreServices.tokenManager; - export const factory = simpleFactory(ref, tokenManager); + export const factory = simpleFactory( + coreServices.tokenManager, + tokenManager, + ); } export function identity(): IdentityService { return new MockIdentityService(); } export namespace identity { - export const ref = coreServices.identity; - export const factory = simpleFactory(ref, identity); + export const factory = simpleFactory(coreServices.identity, identity); } // TODO(Rugvip): Not all core services have implementations available here yet. // some may need a bit more refactoring for it to be simpler to // re-implement functioning mock versions here. export namespace cache { - export const ref = coreServices.cache; - export const factory = cacheFactory; + export const factory = cacheServiceFactory; } export namespace database { - export const ref = coreServices.database; - export const factory = databaseFactory; + export const factory = databaseServiceFactory; } export namespace httpRouter { - export const ref = coreServices.httpRouter; - export const factory = httpRouterFactory; + export const factory = httpRouterServiceFactory; } export namespace lifecycle { - export const ref = coreServices.lifecycle; - export const factory = lifecycleFactory; + export const factory = lifecycleServiceFactory; } export namespace logger { - export const ref = coreServices.logger; - export const factory = loggerFactory; + export const factory = loggerServiceFactory; } export namespace permissions { - export const ref = coreServices.permissions; - export const factory = permissionsFactory; + export const factory = permissionsServiceFactory; } export namespace rootLifecycle { - export const ref = coreServices.rootLifecycle; - export const factory = rootLifecycleFactory; + export const factory = rootLifecycleServiceFactory; } export namespace scheduler { - export const ref = coreServices.scheduler; - export const factory = schedulerFactory; + export const factory = schedulerServiceFactory; } export namespace urlReader { - export const ref = coreServices.urlReader; - export const factory = urlReaderFactory; + export const factory = urlReaderServiceFactory; } } diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts index dca463a9ae..aa93a7dae1 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.test.ts @@ -40,10 +40,8 @@ beforeAll(async () => { env.registerInit({ deps: { lifecycle: coreServices.lifecycle }, async init({ lifecycle }) { - lifecycle.addShutdownHook({ - fn() { - globalTestBackendHasBeenStopped = true; - }, + lifecycle.addShutdownHook(() => { + globalTestBackendHasBeenStopped = true; }); }, }); @@ -145,7 +143,7 @@ describe('TestBackend', () => { lifecycle: coreServices.lifecycle, }, async init({ lifecycle }) { - lifecycle.addShutdownHook({ fn: shutdownSpy }); + lifecycle.addShutdownHook(shutdownSpy); }, }); }, @@ -165,7 +163,7 @@ describe('TestBackend', () => { expect.assertions(2); const testPlugin = createBackendPlugin({ - id: 'test', + pluginId: 'test', register(env) { env.registerInit({ deps: { @@ -201,7 +199,7 @@ describe('TestBackend', () => { it('should allow making requests via supertest', async () => { const testPlugin = createBackendPlugin({ - id: 'test', + pluginId: 'test', register(env) { env.registerInit({ deps: { diff --git a/packages/backend-test-utils/src/next/wiring/TestBackend.ts b/packages/backend-test-utils/src/next/wiring/TestBackend.ts index db1016fcb6..66bbb62193 100644 --- a/packages/backend-test-utils/src/next/wiring/TestBackend.ts +++ b/packages/backend-test-utils/src/next/wiring/TestBackend.ts @@ -30,12 +30,13 @@ import { BackendFeature, ExtensionPoint, coreServices, + createBackendPlugin, } from '@backstage/backend-plugin-api'; import { mockServices } from '../services'; import { ConfigReader } from '@backstage/config'; import express from 'express'; -/** @alpha */ +/** @public */ export interface TestBackendOptions< TServices extends any[], TExtensionPoints extends any[], @@ -59,7 +60,7 @@ export interface TestBackendOptions< features?: BackendFeature[]; } -/** @alpha */ +/** @public */ export interface TestBackend extends Backend { /** * Provides access to the underling HTTP server for use with utilities @@ -88,7 +89,7 @@ const defaultServiceFactories = [ const backendInstancesToCleanUp = new Array(); -/** @alpha */ +/** @public */ export async function startTestBackend< TServices extends any[], TExtensionPoints extends any[], @@ -129,12 +130,7 @@ export async function startTestBackend< { logger }, ); - lifecycle.addShutdownHook({ - async fn() { - await server.stop(); - }, - logger, - }); + lifecycle.addShutdownHook(() => server.stop(), { logger }); await server.start(); @@ -198,16 +194,18 @@ export async function startTestBackend< backendInstancesToCleanUp.push(backend); - backend.add({ - id: `---test-extension-point-registrar`, - register(reg) { - for (const [ref, impl] of extensionPoints) { - reg.registerExtensionPoint(ref, impl); - } + backend.add( + createBackendPlugin({ + pluginId: `---test-extension-point-registrar`, + register(reg) { + for (const [ref, impl] of extensionPoints) { + reg.registerExtensionPoint(ref, impl); + } - reg.registerInit({ deps: {}, async init() {} }); - }, - }); + reg.registerInit({ deps: {}, async init() {} }); + }, + })(), + ); for (const feature of features) { backend.add(feature); diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 24b00f192c..7053244da9 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,54 @@ # example-backend +## 0.2.80-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-backend@0.1.0-next.0 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/plugin-search-backend@1.2.3-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-events-backend@0.2.3-next.2 + - @backstage/plugin-kafka-backend@0.2.35-next.2 + - @backstage/plugin-proxy-backend@0.2.36-next.2 + - @backstage/plugin-app-backend@0.3.42-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-kubernetes-backend@0.9.3-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-backend@0.17.5-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/plugin-playlist-backend@0.2.5-next.2 + - @backstage/plugin-rollbar-backend@0.1.39-next.2 + - @backstage/plugin-search-backend-module-pg@0.5.3-next.2 + - @backstage/plugin-tech-insights-backend@0.5.8-next.2 + - @backstage/plugin-techdocs-backend@1.5.3-next.2 + - example-app@0.2.80-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-backend@0.2.7-next.2 + - @backstage/plugin-azure-devops-backend@0.3.21-next.2 + - @backstage/plugin-azure-sites-backend@0.1.4-next.2 + - @backstage/plugin-badges-backend@0.1.36-next.2 + - @backstage/plugin-code-coverage-backend@0.2.8-next.2 + - @backstage/plugin-explore-backend@0.0.4-next.2 + - @backstage/plugin-graphql-backend@0.1.32-next.2 + - @backstage/plugin-jenkins-backend@0.1.32-next.2 + - @backstage/plugin-permission-backend@0.5.17-next.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-scaffolder-backend-module-rails@0.4.10-next.2 + - @backstage/plugin-search-backend-module-elasticsearch@1.1.3-next.2 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.26-next.2 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + - @backstage/plugin-todo-backend@0.1.39-next.2 + ## 0.2.80-next.1 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index e554036ede..76c7e31fc5 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.80-next.1", + "version": "0.2.80-next.2", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -42,6 +42,7 @@ "@backstage/plugin-catalog-backend": "workspace:^", "@backstage/plugin-catalog-node": "workspace:^", "@backstage/plugin-code-coverage-backend": "workspace:^", + "@backstage/plugin-entity-feedback-backend": "workspace:^", "@backstage/plugin-events-backend": "workspace:^", "@backstage/plugin-events-node": "workspace:^", "@backstage/plugin-explore-backend": "workspace:^", @@ -50,6 +51,7 @@ "@backstage/plugin-kafka-backend": "workspace:^", "@backstage/plugin-kubernetes-backend": "workspace:^", "@backstage/plugin-lighthouse-backend": "workspace:^", + "@backstage/plugin-linguist-backend": "workspace:^", "@backstage/plugin-permission-backend": "workspace:^", "@backstage/plugin-permission-common": "workspace:^", "@backstage/plugin-permission-node": "workspace:^", diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index b68b162e52..011be941cf 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -44,6 +44,7 @@ import azureDevOps from './plugins/azure-devops'; import catalog from './plugins/catalog'; import catalogEventBasedProviders from './plugins/catalogEventBasedProviders'; import codeCoverage from './plugins/codecoverage'; +import entityFeedback from './plugins/entityFeedback'; import events from './plugins/events'; import explore from './plugins/explore'; import kubernetes from './plugins/kubernetes'; @@ -63,6 +64,7 @@ import permission from './plugins/permission'; import playlist from './plugins/playlist'; import adr from './plugins/adr'; import lighthouse from './plugins/lighthouse'; +import linguist from './plugins/linguist'; import { PluginEnvironment } from './types'; import { ServerPermissionClient } from '@backstage/plugin-permission-node'; import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; @@ -147,6 +149,9 @@ async function main() { ); const permissionEnv = useHotMemoize(module, () => createEnv('permission')); const playlistEnv = useHotMemoize(module, () => createEnv('playlist')); + const entityFeedbackEnv = useHotMemoize(module, () => + createEnv('entityFeedback'), + ); const eventsEnv = useHotMemoize(module, () => createEnv('events')); const exploreEnv = useHotMemoize(module, () => createEnv('explore')); const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse')); @@ -154,6 +159,7 @@ async function main() { const eventBasedEntityProviders = await catalogEventBasedProviders( catalogEnv, ); + const linguistEnv = useHotMemoize(module, () => createEnv('linguist')); const apiRouter = Router(); apiRouter.use( @@ -179,7 +185,9 @@ async function main() { apiRouter.use('/permission', await permission(permissionEnv)); apiRouter.use('/playlist', await playlist(playlistEnv)); apiRouter.use('/explore', await explore(exploreEnv)); + apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv)); apiRouter.use('/adr', await adr(adrEnv)); + apiRouter.use('/linguist', await linguist(linguistEnv)); apiRouter.use(notFoundHandler()); await lighthouse(lighthouseEnv); diff --git a/packages/backend/src/plugins/auth.ts b/packages/backend/src/plugins/auth.ts index 8beda6f3fb..0d92315f92 100644 --- a/packages/backend/src/plugins/auth.ts +++ b/packages/backend/src/plugins/auth.ts @@ -114,6 +114,13 @@ export default async function createPlugin( }, }), + bitbucketServer: providers.bitbucketServer.create({ + signIn: { + resolver: + providers.bitbucketServer.resolvers.emailMatchingUserEntityProfileEmail(), + }, + }), + // This is an example of how to configure the OAuth2Proxy provider as well // as how to sign a user in without a matching user entity in the catalog. // You can try it out using `` diff --git a/packages/backend/src/plugins/entityFeedback.ts b/packages/backend/src/plugins/entityFeedback.ts new file mode 100644 index 0000000000..c37c3c9a97 --- /dev/null +++ b/packages/backend/src/plugins/entityFeedback.ts @@ -0,0 +1,28 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRouter } from '@backstage/plugin-entity-feedback-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default function createPlugin(env: PluginEnvironment): Promise { + return createRouter({ + database: env.database, + discovery: env.discovery, + identity: env.identity, + logger: env.logger, + }); +} diff --git a/packages/backend/src/plugins/linguist.ts b/packages/backend/src/plugins/linguist.ts new file mode 100644 index 0000000000..2c8713a7e3 --- /dev/null +++ b/packages/backend/src/plugins/linguist.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2022 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 { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import { createRouter } from '@backstage/plugin-linguist-backend'; +import { Router } from 'express'; +import type { PluginEnvironment } from '../types'; + +export default async function createPlugin( + env: PluginEnvironment, +): Promise { + const schedule: TaskScheduleDefinition = { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 15 }, + }; + + return createRouter( + { + schedule: schedule, + age: { days: 30 }, + batchSize: 2, + useSourceLocation: false, + }, + { ...env }, + ); +} diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md index cf61dee463..1602dfbb79 100644 --- a/packages/catalog-client/CHANGELOG.md +++ b/packages/catalog-client/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-client +## 1.3.1-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/errors@1.1.4 + ## 1.3.1-next.0 ### Patch Changes diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index 8b7c09d2ef..ac233e293a 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-client", "description": "An isomorphic client for the catalog backend", - "version": "1.3.1-next.0", + "version": "1.3.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 5bb6033940..1f14aba820 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/catalog-model +## 1.2.0-next.1 + +### Minor Changes + +- d51668882d: Adds the tRPC API type to the catalog. + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + ## 1.1.6-next.0 ### Patch Changes diff --git a/packages/catalog-model/examples/all-apis.yaml b/packages/catalog-model/examples/all-apis.yaml index e23f1b3656..472897bfac 100644 --- a/packages/catalog-model/examples/all-apis.yaml +++ b/packages/catalog-model/examples/all-apis.yaml @@ -6,6 +6,7 @@ metadata: spec: targets: - ./apis/hello-world-api.yaml + - ./apis/hello-world-trpc-api.yaml - ./apis/petstore-api.yaml - ./apis/spotify-api.yaml - ./apis/streetlights-api.yaml diff --git a/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml b/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml new file mode 100644 index 0000000000..f5807537b3 --- /dev/null +++ b/packages/catalog-model/examples/apis/hello-world-trpc-api.yaml @@ -0,0 +1,25 @@ +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: hello-world-trpc + description: Hello World example for tRPC +spec: + type: trpc + lifecycle: experimental + owner: team-c + definition: | + import { z } from 'zod'; + import { publicProcedure, router } from '../trpc'; + + export const apiRouter = router({ + version: publicProcedure.query(() => { + return { version: '0.42.0' }; + }), + hello: publicProcedure + .input(z.object({ username: z.string().nullish() }).nullish()) + .query(({ input, ctx }) => { + return { + text: `hello ${input?.username ?? ctx.user?.name ?? 'world'}`, + }; + }), + }); diff --git a/packages/catalog-model/examples/components/artist-lookup-component.yaml b/packages/catalog-model/examples/components/artist-lookup-component.yaml index c0022762b4..a3a3780c84 100644 --- a/packages/catalog-model/examples/components/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/components/artist-lookup-component.yaml @@ -28,6 +28,8 @@ metadata: - url: https://example.com/alert title: Alerts icon: alert + annotations: + backstage.io/linguist: 'https://github.com/backstage/backstage/tree/master/plugins/playlist' spec: type: service lifecycle: experimental diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index 5dba193a61..117035a82e 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/catalog-model", "description": "Types and validators that help describe the model of a Backstage Catalog", - "version": "1.1.6-next.0", + "version": "1.2.0-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/catalog-model/src/location/helpers.test.ts b/packages/catalog-model/src/location/helpers.test.ts index 91a2137611..28267dcf37 100644 --- a/packages/catalog-model/src/location/helpers.test.ts +++ b/packages/catalog-model/src/location/helpers.test.ts @@ -50,6 +50,9 @@ describe('parseLocationRef', () => { expect(() => parseLocationRef('https://bleh')).toThrow( "Invalid location ref 'https://bleh', please prefix it with 'url:', e.g. 'url:https://bleh'", ); + expect(() => parseLocationRef('url:javascript:alert()')).toThrow( + "Invalid location ref 'url:javascript:alert()', target is a javascript: URL", + ); }); }); @@ -70,6 +73,12 @@ describe('stringifyLocationRef', () => { expect(() => stringifyLocationRef({ type: 'hello', target: '' })).toThrow( 'Unable to stringify location ref, empty target', ); + expect(() => + // eslint-disable-next-line no-script-url + stringifyLocationRef({ type: 'url', target: 'javascript:alert()' }), + ).toThrow( + "Invalid location ref 'url:javascript:alert()', target is a javascript: URL", + ); }); }); diff --git a/packages/catalog-model/src/location/helpers.ts b/packages/catalog-model/src/location/helpers.ts index 6f9800c5a2..a2f4d84f6f 100644 --- a/packages/catalog-model/src/location/helpers.ts +++ b/packages/catalog-model/src/location/helpers.ts @@ -17,6 +17,11 @@ import { Entity, stringifyEntityRef } from '../entity'; import { ANNOTATION_LOCATION, ANNOTATION_SOURCE_LOCATION } from './annotation'; +// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js +const scriptProtocolPattern = + // eslint-disable-next-line no-control-regex + /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + /** * Parses a string form location reference. * @@ -56,6 +61,12 @@ export function parseLocationRef(ref: string): { ); } + if (scriptProtocolPattern.test(target)) { + throw new TypeError( + `Invalid location ref '${ref}', target is a javascript: URL`, + ); + } + return { type, target }; } @@ -78,6 +89,12 @@ export function stringifyLocationRef(ref: { throw new TypeError(`Unable to stringify location ref, empty target`); } + if (scriptProtocolPattern.test(target)) { + throw new TypeError( + `Invalid location ref '${type}:${target}', target is a javascript: URL`, + ); + } + return `${type}:${target}`; } diff --git a/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json b/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json index fae1225764..3d5af710e0 100644 --- a/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json +++ b/packages/catalog-model/src/schema/kinds/API.v1alpha1.schema.json @@ -46,7 +46,7 @@ "type": { "type": "string", "description": "The type of the API definition.", - "examples": ["openapi", "asyncapi", "graphql", "grpc"], + "examples": ["openapi", "asyncapi", "graphql", "grpc", "trpc"], "minLength": 1 }, "lifecycle": { diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index caf890a14f..09e7f44eb4 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/cli +## 0.22.2-next.1 + +### Patch Changes + +- dd8a9afe66: Replaced several monorepo lint rules with new rules from `@backstage/eslint-plugin`. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for a full list of rules. +- 90616df9a8: Added an experimental mode for the `package start` command for backend packages. Enabled by setting `EXPERIMENTAL_BACKEND_START`. +- Updated dependencies + - @backstage/eslint-plugin@0.1.0-next.0 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/errors@1.1.4 + - @backstage/release-manifests@0.0.8 + - @backstage/types@1.0.2 + ## 0.22.2-next.0 ### Patch Changes diff --git a/packages/cli/asset-types/asset-types.d.ts b/packages/cli/asset-types/asset-types.d.ts index 0bd0922004..9bd158bb8e 100644 --- a/packages/cli/asset-types/asset-types.d.ts +++ b/packages/cli/asset-types/asset-types.d.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /// /// diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js index a40aa79fb3..dde853fdff 100644 --- a/packages/cli/config/eslint-factory.js +++ b/packages/cli/config/eslint-factory.js @@ -60,7 +60,7 @@ function createConfig(dir, extraConfig = {}) { '@spotify/eslint-config-typescript', 'prettier', 'plugin:jest/recommended', - 'plugin:monorepo/recommended', + 'plugin:@backstage/recommended', ...(extraExtends ?? []), ], parser: '@typescript-eslint/parser', @@ -88,32 +88,6 @@ function createConfig(dir, extraConfig = {}) { '@typescript-eslint/no-redeclare': 'error', 'no-undef': 'off', 'import/newline-after-import': 'error', - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: dir - ? [ - `!${joinPath(dir, 'src/**')}`, - joinPath(dir, 'src/**/*.test.*'), - joinPath(dir, 'src/**/*.stories.*'), - joinPath(dir, 'src/**/__testUtils__/**'), - joinPath(dir, 'src/**/__mocks__/**'), - joinPath(dir, 'src/setupTests.*'), - ] - : [ - // Legacy config for packages that don't provide a dir - '**/*.test.*', - '**/*.stories.*', - '**/__testUtils__/**', - '**/__mocks__/**', - '**/src/setupTests.*', - '**/dev/**', - ], - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], 'no-unused-expressions': 'off', '@typescript-eslint/no-unused-expressions': 'error', '@typescript-eslint/consistent-type-assertions': 'error', @@ -135,9 +109,6 @@ function createConfig(dir, extraConfig = {}) { ...(restrictedSrcImports ?? []), ], patterns: [ - // Avoid cross-package imports - '**/../../**/*/src/**', - '**/../../**/*/src', // Prevent imports of stories or tests '*.stories*', '*.test*', @@ -187,8 +158,6 @@ function createConfig(dir, extraConfig = {}) { ...(restrictedImports ?? []), ...(restrictedTestImports ?? []), ], - // Avoid cross-package imports - patterns: ['**/../../**/*/src/**', '**/../../**/*/src'], }, ], }, diff --git a/packages/cli/package.json b/packages/cli/package.json index 909ac8460c..a3020de1da 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/cli", "description": "CLI for developing Backstage plugins and apps", - "version": "0.22.2-next.0", + "version": "0.22.2-next.1", "publishConfig": { "access": "public" }, @@ -34,8 +34,10 @@ "@backstage/config": "workspace:^", "@backstage/config-loader": "workspace:^", "@backstage/errors": "workspace:^", + "@backstage/eslint-plugin": "workspace:^", "@backstage/release-manifests": "workspace:^", "@backstage/types": "workspace:^", + "@esbuild-kit/cjs-loader": "^2.4.1", "@manypkg/get-packages": "^1.1.3", "@octokit/request": "^6.0.0", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.7", @@ -65,6 +67,7 @@ "chalk": "^4.0.0", "chokidar": "^3.3.1", "commander": "^9.1.0", + "cross-spawn": "^7.0.3", "css-loader": "^6.5.1", "diff": "^5.0.0", "esbuild": "^0.17.0", @@ -76,7 +79,6 @@ "eslint-plugin-import": "^2.25.4", "eslint-plugin-jest": "^27.0.0", "eslint-plugin-jsx-a11y": "^6.5.1", - "eslint-plugin-monorepo": "^0.3.2", "eslint-plugin-react": "^7.28.0", "eslint-plugin-react-hooks": "^4.3.0", "eslint-webpack-plugin": "^3.1.1", @@ -136,6 +138,7 @@ "@backstage/dev-utils": "workspace:^", "@backstage/test-utils": "workspace:^", "@backstage/theme": "workspace:^", + "@types/cross-spawn": "^6.0.2", "@types/diff": "^5.0.0", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/cli/src/commands/start/startBackend.ts b/packages/cli/src/commands/start/startBackend.ts index 61ada7a291..3e6a13be96 100644 --- a/packages/cli/src/commands/start/startBackend.ts +++ b/packages/cli/src/commands/start/startBackend.ts @@ -17,6 +17,7 @@ import fs from 'fs-extra'; import { paths } from '../../lib/paths'; import { serveBackend } from '../../lib/bundler'; +import { startBackendExperimental } from '../../lib/experimental/startBackendExperimental'; interface StartBackendOptions { checksEnabled: boolean; @@ -25,17 +26,28 @@ interface StartBackendOptions { } export async function startBackend(options: StartBackendOptions) { - // Cleaning dist/ before we start the dev process helps work around an issue - // where we end up with the entrypoint executing multiple times, causing - // a port bind conflict among other things. - await fs.remove(paths.resolveTarget('dist')); + if (process.env.EXPERIMENTAL_BACKEND_START) { + const waitForExit = await startBackendExperimental({ + entry: 'src/index', + checksEnabled: false, // not supported + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); - const waitForExit = await serveBackend({ - entry: 'src/index', - checksEnabled: options.checksEnabled, - inspectEnabled: options.inspectEnabled, - inspectBrkEnabled: options.inspectBrkEnabled, - }); + await waitForExit(); + } else { + // Cleaning dist/ before we start the dev process helps work around an issue + // where we end up with the entrypoint executing multiple times, causing + // a port bind conflict among other things. + await fs.remove(paths.resolveTarget('dist')); - await waitForExit(); + const waitForExit = await serveBackend({ + entry: 'src/index', + checksEnabled: options.checksEnabled, + inspectEnabled: options.inspectEnabled, + inspectBrkEnabled: options.inspectBrkEnabled, + }); + + await waitForExit(); + } } diff --git a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts index 2533131a40..cf37c2a664 100644 --- a/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts +++ b/packages/cli/src/lib/builder/buildTypeDefinitionsWorker.ts @@ -50,7 +50,7 @@ export async function buildTypeDefinitionsWorker( */ const { PackageJsonLookup, - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports } = require('@rushstack/node-core-library/lib/PackageJsonLookup'); const old = PackageJsonLookup.prototype.tryGetPackageJsonFilePathFor; diff --git a/packages/cli/src/lib/bundler/transforms.ts b/packages/cli/src/lib/bundler/transforms.ts index 9a93f550dd..e0308f5c2d 100644 --- a/packages/cli/src/lib/bundler/transforms.ts +++ b/packages/cli/src/lib/bundler/transforms.ts @@ -107,7 +107,7 @@ export const transforms = (options: TransformOptions): Transforms => { ], }, { - test: /\.(js|mjs|cjs)/, + test: /\.(js|mjs|cjs)$/, resolve: { fullySpecified: false, }, @@ -141,9 +141,10 @@ export const transforms = (options: TransformOptions): Transforms => { /\.gif$/, /\.jpe?g$/, /\.png$/, - /\.frag/, - { and: [/\.svg/, { not: [/\.icon\.svg/] }] }, - /\.xml/, + /\.frag$/, + /\.vert$/, + { and: [/\.svg$/, { not: [/\.icon\.svg$/] }] }, + /\.xml$/, ], type: 'asset/resource', generator: { diff --git a/packages/cli/src/lib/experimental/IpcServer.ts b/packages/cli/src/lib/experimental/IpcServer.ts new file mode 100644 index 0000000000..8ceb72b046 --- /dev/null +++ b/packages/cli/src/lib/experimental/IpcServer.ts @@ -0,0 +1,102 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { serializeError } from '@backstage/errors'; +import { ChildProcess } from 'child_process'; + +interface RequestMeta { + generation: number; +} + +type MethodHandler = ( + req: TRequest, + meta: RequestMeta, +) => Promise; + +interface Request { + id: number; + method: string; + body: unknown; + type: string; +} + +const requestType = '@backstage/cli/channel/request'; +const responseType = '@backstage/cli/channel/response'; + +export class IpcServer { + #generation = 1; + #methods = new Map>(); + + addChild(child: ChildProcess) { + const generation = this.#generation++; + const sendMessage = child.send?.bind(child); + if (!sendMessage) { + return; + } + + const messageListener = (request: Request) => { + if (request.type !== requestType) { + return; + } + + const handler = this.#methods.get(request.method); + if (!handler) { + sendMessage({ + type: responseType, + id: request.id, + error: { + name: 'NotFoundError', + message: `No handler registered for method ${request.method}`, + }, + }); + return; + } + + Promise.resolve() + .then(() => handler(request.body, { generation })) + .then(response => + sendMessage({ + type: responseType, + id: request.id, + body: response, + }), + ) + .catch(error => + sendMessage({ + type: responseType, + id: request.id, + error: serializeError(error), + }), + ); + }; + + child.addListener('message', messageListener as (req: unknown) => void); + + child.addListener('exit', () => { + child.removeListener('message', messageListener); + }); + } + + registerMethod( + method: string, + handler: MethodHandler, + ) { + if (this.#methods.has(method)) { + throw new Error(`A handler is already registered for method ${method}`); + } + this.#methods.set(method, handler); + } +} diff --git a/packages/cli/src/lib/experimental/ServerDataStore.ts b/packages/cli/src/lib/experimental/ServerDataStore.ts new file mode 100644 index 0000000000..c0232157cb --- /dev/null +++ b/packages/cli/src/lib/experimental/ServerDataStore.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { IpcServer } from './IpcServer'; + +interface StorageItem { + generation: number; + data: unknown; +} + +interface SaveRequest { + key: string; + data: unknown; +} + +interface SaveResponse { + saved: boolean; +} + +interface LoadRequest { + key: string; +} + +interface LoadResponse { + loaded: boolean; + data: unknown; +} + +export class ServerDataStore { + static bind(server: IpcServer): void { + const store = new Map(); + + server.registerMethod( + 'DevDataStore.save', + async (request, { generation }) => { + const { key, data } = request; + if (!key) { + throw new Error('Key is required in DevDataStore.save'); + } + + const item = store.get(key); + + if (!item) { + store.set(key, { generation, data }); + return { saved: true }; + } + + if (item.generation > generation) { + return { saved: false }; + } + + store.set(key, { generation, data }); + return { saved: true }; + }, + ); + + server.registerMethod( + 'DevDataStore.load', + async request => { + const item = store.get(request.key); + return { loaded: Boolean(item), data: item?.data }; + }, + ); + } +} diff --git a/packages/cli/src/lib/experimental/startBackendExperimental.ts b/packages/cli/src/lib/experimental/startBackendExperimental.ts new file mode 100644 index 0000000000..c08fbda0da --- /dev/null +++ b/packages/cli/src/lib/experimental/startBackendExperimental.ts @@ -0,0 +1,138 @@ +/* + * 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 { BackendServeOptions } from '../bundler/types'; +import type { ChildProcess } from 'child_process'; +import { fileURLToPath } from 'url'; +import { isAbsolute as isAbsolutePath } from 'path'; +import { FSWatcher, watch } from 'chokidar'; +import { IpcServer } from './IpcServer'; +import { ServerDataStore } from './ServerDataStore'; +import debounce from 'lodash/debounce'; +import spawn from 'cross-spawn'; +import { paths } from '../paths'; + +const loaderArgs = ['--require', require.resolve('@esbuild-kit/cjs-loader')]; + +export async function startBackendExperimental(options: BackendServeOptions) { + const envEnv = process.env as { NODE_ENV: string }; + if (!envEnv.NODE_ENV) { + envEnv.NODE_ENV = 'development'; + } + + // Set up the parent IPC server and bind the available services + const server = new IpcServer(); + ServerDataStore.bind(server); + + let exiting = false; + let child: ChildProcess | undefined; + let watcher: FSWatcher | undefined = undefined; + let shutdownPromise: Promise | undefined = undefined; + + const restart = debounce(async () => { + // If a re-trigger happens during an existing shutdown, we just ignore it + if (shutdownPromise) { + return; + } + + if (child && !child.killed && child.exitCode === null) { + // We always wait for the existing process to exit, to make sure we don't get IPC conflicts + shutdownPromise = new Promise(resolve => child!.once('exit', resolve)); + child.kill(); + await shutdownPromise; + shutdownPromise = undefined; + } + + // We've received a shutdown signal + if (exiting) { + return; + } + + const optionArgs = new Array(); + if (options.inspectEnabled) { + optionArgs.push('--inspect'); + } else if (options.inspectBrkEnabled) { + optionArgs.push('--inspect-brk'); + } + + const userArgs = process.argv + .slice(['node', 'backstage-cli', 'package', 'start'].length) + .filter(arg => !optionArgs.includes(arg)); + + child = spawn( + process.execPath, + [...loaderArgs, ...optionArgs, options.entry, ...userArgs], + { + stdio: ['inherit', 'inherit', 'inherit', 'ipc'], + env: { + ...process.env, + BACKSTAGE_CLI_CHANNEL: '1', + ESBK_TSCONFIG_PATH: paths.resolveTargetRoot('tsconfig.json'), + }, + serialization: 'advanced', + }, + ); + + server.addChild(child); + + // This captures messages sent by @esbuild-kit/cjs-loader + child.on('message', (data: { type?: string } | null) => { + if (typeof data === 'object' && data?.type === 'dependency') { + let path = (data as { path: string }).path; + if (path.startsWith('file:')) { + path = fileURLToPath(path); + } + + if (isAbsolutePath(path)) { + watcher?.add(path); + } + } + }); + }, 100); + + restart(); + + watcher = watch([paths.targetDir], { + cwd: process.cwd(), + ignored: ['**/.*/**', '**/node_modules/**'], + ignoreInitial: true, + ignorePermissionErrors: true, + }).on('all', restart); + + // Trigger restart on hitting enter in the terminal + process.stdin.on('data', restart); + + const exitPromise = new Promise(resolveExitPromise => { + async function handleSignal(signal: NodeJS.Signals) { + exiting = true; + + // Forward signals to child and wait for it to exit if still running + if (child && child.exitCode === null) { + await new Promise(resolve => { + child!.on('close', resolve); + child!.kill(signal); + }); + } + + resolveExitPromise(); + } + + process.once('SIGINT', handleSignal); + process.once('SIGTERM', handleSignal); + }); + + return () => exitPromise; +} diff --git a/packages/cli/src/lib/version.test.ts b/packages/cli/src/lib/version.test.ts index fb9bf83e7c..604129679b 100644 --- a/packages/cli/src/lib/version.test.ts +++ b/packages/cli/src/lib/version.test.ts @@ -17,7 +17,6 @@ import mockFs from 'mock-fs'; import { packageVersions, createPackageVersionProvider } from './version'; import { Lockfile } from './versioning'; -// eslint-disable-next-line monorepo/no-internal-import import corePluginApiPkg from '@backstage/core-plugin-api/package.json'; describe('createPackageVersionProvider', () => { diff --git a/packages/cli/src/lib/version.ts b/packages/cli/src/lib/version.ts index 5a199236f5..bf89842d5f 100644 --- a/packages/cli/src/lib/version.ts +++ b/packages/cli/src/lib/version.ts @@ -19,7 +19,7 @@ import semver from 'semver'; import { paths } from './paths'; import { Lockfile } from './versioning'; -/* eslint-disable monorepo/no-relative-import */ +/* eslint-disable @backstage/no-relative-monorepo-imports */ /* This is a list of all packages used by the templates. If dependencies are added or removed, diff --git a/packages/cli/src/types.d.ts b/packages/cli/src/types.d.ts index fedbe61f02..1222b2b4b8 100644 --- a/packages/cli/src/types.d.ts +++ b/packages/cli/src/types.d.ts @@ -258,3 +258,5 @@ declare module 'webpack-node-externals' { } } } + +declare module '@esbuild-kit/cjs-loader' {} diff --git a/packages/codemods/.eslintrc.js b/packages/codemods/.eslintrc.js index f9fb2e27c9..e2a53a6ad2 100644 --- a/packages/codemods/.eslintrc.js +++ b/packages/codemods/.eslintrc.js @@ -1,14 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], - }, -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/codemods/CHANGELOG.md b/packages/codemods/CHANGELOG.md index 2481aff8b1..5dc610d5c1 100644 --- a/packages/codemods/CHANGELOG.md +++ b/packages/codemods/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/codemods +## 0.1.43-next.0 + +### Patch Changes + +- 02f1316e57: Moved `commander` to being a regular dependency. +- Updated dependencies + - @backstage/cli-common@0.1.11 + ## 0.1.42 ### Patch Changes diff --git a/packages/codemods/package.json b/packages/codemods/package.json index cbcc0613d4..7c6af890b0 100644 --- a/packages/codemods/package.json +++ b/packages/codemods/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/codemods", "description": "A collection of codemods for Backstage projects", - "version": "0.1.42", + "version": "0.1.43-next.0", "publishConfig": { "access": "public", "main": "dist/index.cjs.js" @@ -35,6 +35,7 @@ "dependencies": { "@backstage/cli-common": "workspace:^", "chalk": "^4.0.0", + "commander": "^9.1.0", "jscodeshift": "^0.14.0", "jscodeshift-add-imports": "^1.0.10" }, @@ -42,7 +43,6 @@ "@backstage/cli": "workspace:^", "@types/jscodeshift": "^0.11.0", "@types/node": "^16.11.26", - "commander": "^9.1.0", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md index 133ac22e5b..37f7a98de0 100644 --- a/packages/core-app-api/api-report.md +++ b/packages/core-app-api/api-report.md @@ -22,6 +22,7 @@ import { BackstageIdentityApi } from '@backstage/core-plugin-api'; import { BackstageIdentityResponse } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { bitbucketAuthApiRef } from '@backstage/core-plugin-api'; +import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api'; import { ComponentType } from 'react'; import { Config } from '@backstage/config'; import { ConfigReader } from '@backstage/config'; @@ -280,6 +281,25 @@ export class BitbucketAuth { static create(options: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T; } +// @public +export class BitbucketServerAuth { + // (undocumented) + static create( + options: OAuthApiCreateOptions, + ): typeof bitbucketServerAuthApiRef.T; +} + +// @public +export type BitbucketServerSession = { + providerInfo: { + accessToken: string; + scopes: Set; + expiresAt?: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; + // @public export type BitbucketSession = { providerInfo: { diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts new file mode 100644 index 0000000000..32e7755a00 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts @@ -0,0 +1,51 @@ +/* + * 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 MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; +import { UrlPatternDiscovery } from '../../DiscoveryApi'; +import BitbucketServerAuth from './BitbucketServerAuth'; + +const getSession = jest.fn(); + +jest.mock('../../../../lib/AuthSessionManager', () => ({ + ...(jest.requireActual('../../../../lib/AuthSessionManager') as any), + RefreshingAuthSessionManager: class { + getSession = getSession; + }, +})); + +describe('BitbucketServerAuth', () => { + afterEach(() => { + jest.resetAllMocks(); + }); + + it.each([ + ['PUBLIC_REPOS', ['PUBLIC_REPOS']], + ['PROJECT_ADMIN REPO_READ', ['PROJECT_ADMIN', 'REPO_READ']], + [ + 'PROJECT_ADMIN REPO_READ ACCOUNT_WRITE', + ['PROJECT_ADMIN', 'REPO_READ', 'ACCOUNT_WRITE'], + ], + ])(`should normalize scopes correctly - %p`, (scope, scopes) => { + const bitbucketServerAuth = BitbucketServerAuth.create({ + oauthRequestApi: new MockOAuthApi(), + discoveryApi: UrlPatternDiscovery.compile('http://example.com'), + }); + + bitbucketServerAuth.getAccessToken(scope); + expect(getSession).toHaveBeenCalledWith({ scopes: new Set(scopes) }); + }); +}); diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts new file mode 100644 index 0000000000..dc50eff6ab --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts @@ -0,0 +1,66 @@ +/* + * 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 { + BackstageIdentityResponse, + bitbucketServerAuthApiRef, + ProfileInfo, +} from '@backstage/core-plugin-api'; + +import { OAuthApiCreateOptions } from '../types'; +import { OAuth2 } from '../oauth2'; + +export type BitbucketServerAuthResponse = { + providerInfo: { + accessToken: string; + scope: string; + expiresInSeconds: number; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; + +const DEFAULT_PROVIDER = { + id: 'bitbucketServer', + title: 'Bitbucket Server', + icon: () => null, +}; + +/** + * Implements the OAuth flow to Bitbucket Server. + * @public + */ +export default class BitbucketServerAuth { + static create( + options: OAuthApiCreateOptions, + ): typeof bitbucketServerAuthApiRef.T { + const { + discoveryApi, + environment = 'development', + provider = DEFAULT_PROVIDER, + oauthRequestApi, + defaultScopes = ['PROJECT_ADMIN'], + } = options; + + return OAuth2.create({ + discoveryApi, + oauthRequestApi, + provider, + environment, + defaultScopes, + }); + } +} diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/index.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/index.ts new file mode 100644 index 0000000000..5fcb8f72f2 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; +export { default as BitbucketServerAuth } from './BitbucketServerAuth'; diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/types.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/types.ts new file mode 100644 index 0000000000..1041aa8110 --- /dev/null +++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/types.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + ProfileInfo, + BackstageIdentityResponse, +} from '@backstage/core-plugin-api'; + +/** + * Session information for Bitbucket Server auth. + * + * @public + */ +export type BitbucketServerSession = { + providerInfo: { + accessToken: string; + scopes: Set; + expiresAt?: Date; + }; + profile: ProfileInfo; + backstageIdentity: BackstageIdentityResponse; +}; diff --git a/packages/core-app-api/src/apis/implementations/auth/index.ts b/packages/core-app-api/src/apis/implementations/auth/index.ts index c4cf520db5..b54e0424da 100644 --- a/packages/core-app-api/src/apis/implementations/auth/index.ts +++ b/packages/core-app-api/src/apis/implementations/auth/index.ts @@ -23,5 +23,6 @@ export * from './saml'; export * from './microsoft'; export * from './onelogin'; export * from './bitbucket'; +export * from './bitbucketServer'; export * from './atlassian'; export type { OAuthApiCreateOptions, AuthApiCreateOptions } from './types'; diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md index efded8f3c1..30c4e92173 100644 --- a/packages/core-components/CHANGELOG.md +++ b/packages/core-components/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/core-components +## 0.12.4-next.1 + +### Patch Changes + +- 66e2aab4c4: Navigation items in mobile sidebar now have aria label. +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/version-bridge@1.0.3 + ## 0.12.4-next.0 ### Patch Changes diff --git a/packages/core-components/package.json b/packages/core-components/package.json index dc65108d4a..7eedac93ac 100644 --- a/packages/core-components/package.json +++ b/packages/core-components/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-components", "description": "Core components used by Backstage plugins and apps", - "version": "0.12.4-next.0", + "version": "0.12.4-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/core-components/src/components/Link/Link.test.tsx b/packages/core-components/src/components/Link/Link.test.tsx index 3538b79052..682c4bea8a 100644 --- a/packages/core-components/src/components/Link/Link.test.tsx +++ b/packages/core-components/src/components/Link/Link.test.tsx @@ -173,4 +173,24 @@ describe('', () => { }); }); }); + + it('throws an error when attempting to link to script code', () => { + expect(() => + // eslint-disable-next-line no-script-url + render(wrapInTestApp(Script)), + ).toThrowErrorMatchingInlineSnapshot( + `"Link component rejected javascript: URL as a security precaution"`, + ); + }); +}); + +describe('window.open', () => { + it('throws an error when attempting to open script code', () => { + expect(() => + // eslint-disable-next-line no-script-url + window.open("javascript:alert('hello')"), + ).toThrowErrorMatchingInlineSnapshot( + `"Rejected window.open() with a javascript: URL as a security precaution"`, + ); + }); }); diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx index 440fcb811f..be91753ea3 100644 --- a/packages/core-components/src/components/Link/Link.tsx +++ b/packages/core-components/src/components/Link/Link.tsx @@ -55,6 +55,32 @@ const useStyles = makeStyles( export const isExternalUri = (uri: string) => /^([a-z+.-]+):/.test(uri); +// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js +const scriptProtocolPattern = + // eslint-disable-next-line no-control-regex + /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + +// We install this globally in order to prevent javascript: URL XSS attacks via window.open +const originalWindowOpen = window.open as typeof window.open & { + __backstage?: true; +}; +if (originalWindowOpen && !originalWindowOpen.__backstage) { + const newOpen = function open( + this: Window, + ...args: Parameters + ) { + const url = String(args[0]); + if (scriptProtocolPattern.test(url)) { + throw new Error( + 'Rejected window.open() with a javascript: URL as a security precaution', + ); + } + return originalWindowOpen.apply(this, args); + }; + newOpen.__backstage = true; + window.open = newOpen; +} + export type LinkProps = Omit & Omit & { to: string; @@ -144,6 +170,12 @@ export const Link = React.forwardRef( const external = isExternalUri(to); const newWindow = external && !!/^https?:/.exec(to); + if (scriptProtocolPattern.test(to)) { + throw new Error( + 'Link component rejected javascript: URL as a security precaution', + ); + } + const handleClick = (event: React.MouseEvent) => { onClick?.(event); if (!noTrack) { diff --git a/packages/core-components/src/components/LogViewer/LogLine.tsx b/packages/core-components/src/components/LogViewer/LogLine.tsx index 8f3a2ce65c..c8ff5cbceb 100644 --- a/packages/core-components/src/components/LogViewer/LogLine.tsx +++ b/packages/core-components/src/components/LogViewer/LogLine.tsx @@ -15,7 +15,6 @@ */ import React, { useMemo } from 'react'; -import Typography from '@material-ui/core/Typography'; import { AnsiChunk, AnsiLine, ChunkModifiers } from './AnsiProcessor'; import startCase from 'lodash/startCase'; import classnames from 'classnames'; @@ -159,8 +158,8 @@ export function LogLine({ const elements = useMemo( () => chunks.map(({ text, modifiers, highlight }, index) => ( - {text} - + )), [chunks, highlightResultIndex, classes], ); diff --git a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx index dd707778e1..e1aac76381 100644 --- a/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx +++ b/packages/core-components/src/components/LogViewer/RealLogViewer.test.tsx @@ -18,7 +18,7 @@ import React, { ReactNode } from 'react'; import userEvent from '@testing-library/user-event'; import { renderInTestApp } from '@backstage/test-utils'; import { RealLogViewer } from './RealLogViewer'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import copyToClipboard from 'copy-to-clipboard'; // Used by useCopyToClipboard diff --git a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx index 7e00d50f33..93a6dfd7e9 100644 --- a/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx +++ b/packages/core-components/src/components/LogViewer/useLogViewerSelection.test.tsx @@ -20,7 +20,7 @@ import { TestApiProvider, MockErrorApi } from '@backstage/test-utils'; import { errorApiRef } from '@backstage/core-plugin-api'; import { AnsiLine } from './AnsiProcessor'; import { useLogViewerSelection } from './useLogViewerSelection'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import copyToClipboard from 'copy-to-clipboard'; // Used by useCopyToClipboard diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx index 7bb9aab9b5..35cf228f52 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx @@ -16,8 +16,6 @@ import { useApp } from '@backstage/core-plugin-api'; import { BackstageTheme } from '@backstage/theme'; -import { makeStyles } from '@material-ui/core/styles'; -import useMediaQuery from '@material-ui/core/useMediaQuery'; import Box from '@material-ui/core/Box'; import Button from '@material-ui/core/Button'; import DialogActions from '@material-ui/core/DialogActions'; @@ -27,7 +25,9 @@ import ListItem from '@material-ui/core/ListItem'; import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import Popover from '@material-ui/core/Popover'; +import { makeStyles } from '@material-ui/core/styles'; import Typography from '@material-ui/core/Typography'; +import useMediaQuery from '@material-ui/core/useMediaQuery'; import React, { MouseEventHandler, useState } from 'react'; import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks'; import { HelpIcon } from '../../icons'; @@ -111,13 +111,14 @@ export function SupportButton(props: SupportButtonProps) { size="small" onClick={onClickHandler} data-testid="support-button" + aria-label="Support" > ) : ( diff --git a/packages/core-components/src/layout/InfoCard/InfoCard.tsx b/packages/core-components/src/layout/InfoCard/InfoCard.tsx index 95d474aae4..3d8d0f3bb0 100644 --- a/packages/core-components/src/layout/InfoCard/InfoCard.tsx +++ b/packages/core-components/src/layout/InfoCard/InfoCard.tsx @@ -188,12 +188,12 @@ export function InfoCard(props: Props): JSX.Element { variants.forEach(name => { calculatedStyle = { ...calculatedStyle, - ...VARIANT_STYLES.card[name as keyof typeof VARIANT_STYLES['card']], + ...VARIANT_STYLES.card[name as keyof (typeof VARIANT_STYLES)['card']], }; calculatedCardStyle = { ...calculatedCardStyle, ...VARIANT_STYLES.cardContent[ - name as keyof typeof VARIANT_STYLES['cardContent'] + name as keyof (typeof VARIANT_STYLES)['cardContent'] ], }; }); diff --git a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx index 5254199563..1eb1423c61 100644 --- a/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx +++ b/packages/core-components/src/layout/Sidebar/SidebarGroup.tsx @@ -23,7 +23,7 @@ import { makeStyles } from '@material-ui/core/styles'; import React, { useContext } from 'react'; import { useLocation } from 'react-router-dom'; import { Link } from '../../components'; -import { SidebarConfigContext, SidebarConfig } from './config'; +import { SidebarConfig, SidebarConfigContext } from './config'; import { MobileSidebarContext } from './MobileSidebar'; import { useSidebarPinState } from './SidebarPinStateContext'; @@ -100,6 +100,7 @@ const MobileSidebarGroup = (props: SidebarGroupProps) => { return ( // Material UI issue: https://github.com/mui-org/material-ui/issues/27820 ; +// @public +export const bitbucketServerAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +>; + // @public export type BootErrorPageProps = { step: 'load-config' | 'load-chunk'; diff --git a/packages/core-plugin-api/src/apis/definitions/auth.ts b/packages/core-plugin-api/src/apis/definitions/auth.ts index 163ae74806..43597639b6 100644 --- a/packages/core-plugin-api/src/apis/definitions/auth.ts +++ b/packages/core-plugin-api/src/apis/definitions/auth.ts @@ -412,6 +412,21 @@ export const bitbucketAuthApiRef: ApiRef< id: 'core.auth.bitbucket', }); +/** + * Provides authentication towards Bitbucket Server APIs. + * + * @public + * @remarks + * + * See {@link https://confluence.atlassian.com/bitbucketserver/bitbucket-oauth-2-0-provider-api-1108483661.html#BitbucketOAuth2.0providerAPI-scopes} + * for a full list of supported scopes. + */ +export const bitbucketServerAuthApiRef: ApiRef< + OAuthApi & ProfileInfoApi & BackstageIdentityApi & SessionApi +> = createApiRef({ + id: 'core.auth.bitbucket-server', +}); + /** * Provides authentication towards Atlassian APIs. * diff --git a/packages/core-plugin-api/src/app/types.ts b/packages/core-plugin-api/src/app/types.ts index 2a71eda1f4..f3e0547ca4 100644 --- a/packages/core-plugin-api/src/app/types.ts +++ b/packages/core-plugin-api/src/app/types.ts @@ -17,7 +17,7 @@ // This is a bit of a hack that we use to avoid having to redeclare these types // within this package or have an explicit dependency on core-app-api. // These types end up being inlined and duplicated into this package at build time. -// eslint-disable-next-line no-restricted-imports +// eslint-disable-next-line @backstage/no-relative-monorepo-imports export type { BootErrorPageProps, SignInPageProps, diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 4b757a47c9..198d4d92a9 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/create-app +## 0.4.37-next.2 + +### Patch Changes + +- 02f1316e57: Updated `packages/app/cypress/.eslintrc.json` to remove the unnecessary `import/no-extraneous-dependencies` rule. +- Updated dependencies + - @backstage/cli-common@0.1.11 + ## 0.4.37-next.1 ### Patch Changes diff --git a/packages/create-app/bin/backstage-create-app b/packages/create-app/bin/backstage-create-app index 60dfd4990a..4639ea96dc 100755 --- a/packages/create-app/bin/backstage-create-app +++ b/packages/create-app/bin/backstage-create-app @@ -25,7 +25,7 @@ if (!isLocal || process.env.BACKSTAGE_E2E_CLI_TEST) { require('..'); } else { // Only used for development, so should be a devDependency - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports require('ts-node').register({ transpileOnly: true, project: path.resolve(__dirname, '../../../tsconfig.json'), diff --git a/packages/create-app/package.json b/packages/create-app/package.json index e87dc7b0ac..cfdccc5a2b 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "A CLI that helps you create your own Backstage app", - "version": "0.4.37-next.1", + "version": "0.4.37-next.2", "publishConfig": { "access": "public" }, diff --git a/packages/create-app/src/index.ts b/packages/create-app/src/index.ts index 216fab6740..fa235e6175 100644 --- a/packages/create-app/src/index.ts +++ b/packages/create-app/src/index.ts @@ -22,6 +22,7 @@ import { program } from 'commander'; import { exitWithError } from './lib/errors'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports import { version } from '../../../package.json'; import createApp from './createApp'; diff --git a/packages/create-app/src/lib/versions.ts b/packages/create-app/src/lib/versions.ts index b89b1d5463..72f5c2c9d5 100644 --- a/packages/create-app/src/lib/versions.ts +++ b/packages/create-app/src/lib/versions.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable monorepo/no-relative-import */ +/* eslint-disable @backstage/no-relative-monorepo-imports */ /* This is a list of all packages used by the template. If dependencies are added or removed, diff --git a/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json b/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json +++ b/packages/create-app/templates/default-app/packages/app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index 0b66dd3019..6996180db5 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/dev-utils +## 1.0.12-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 1.0.12-next.1 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index b00f5e58a2..773dc3cac0 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/dev-utils", "description": "Utilities for developing Backstage plugins.", - "version": "1.0.12-next.1", + "version": "1.0.12-next.2", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/packages/e2e-test/.eslintrc.js b/packages/e2e-test/.eslintrc.js index f9fb2e27c9..e2a53a6ad2 100644 --- a/packages/e2e-test/.eslintrc.js +++ b/packages/e2e-test/.eslintrc.js @@ -1,14 +1 @@ -module.exports = require('@backstage/cli/config/eslint-factory')(__dirname, { - rules: { - 'no-console': 0, - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: false, - peerDependencies: false, - bundledDependencies: false, - }, - ], - }, -}); +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json index 374eaa6d18..ee447a1736 100644 --- a/packages/e2e-test/package.json +++ b/packages/e2e-test/package.json @@ -26,22 +26,24 @@ "clean": "backstage-cli package clean" }, "bin": "bin/e2e-test", - "devDependencies": { - "@backstage/cli": "workspace:^", + "dependencies": { "@backstage/cli-common": "workspace:^", "@backstage/errors": "workspace:^", - "@types/fs-extra": "^9.0.1", - "@types/node": "^16.11.26", - "@types/puppeteer": "^5.4.4", "chalk": "^4.0.0", "commander": "^9.1.0", "cross-fetch": "^3.1.5", "fs-extra": "10.1.0", "handlebars": "^4.7.3", - "nodemon": "^2.0.2", "pgtools": "^0.3.0", "puppeteer": "^17.0.0", - "tree-kill": "^1.2.2", + "tree-kill": "^1.2.2" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/fs-extra": "^9.0.1", + "@types/node": "^16.11.26", + "@types/puppeteer": "^5.4.4", + "nodemon": "^2.0.2", "ts-node": "^10.0.0" }, "nodemonConfig": { diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 3eedb90b21..b3fb8d9a3c 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -159,7 +159,7 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { appendDeps(pkg); } - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-forbidden-package-imports appendDeps(require('@backstage/create-app/package.json')); print(`Preparing workspace`); @@ -490,6 +490,17 @@ async function testBackendStart(appDir: string, ...args: string[]) { !l.includes('Update this package.json to use a subpath') && !l.includes( '(Use `node --trace-deprecation ...` to show where the warning was created)', + ) && + // These 4 are all for the AWS SDK v2 deprecation + !l.includes( + 'The AWS SDK for JavaScript (v2) will be put into maintenance mode', + ) && + !l.includes( + 'Please migrate your code to use AWS SDK for JavaScript', + ) && + !l.includes('check the migration guide at https://a.co/7PzMCcy') && + !l.includes( + '(Use `node --trace-warnings ...` to show where the warning was created)', ), ).length !== 0 ); diff --git a/packages/eslint-plugin/.eslintrc.js b/packages/eslint-plugin/.eslintrc.js new file mode 100644 index 0000000000..6b9de4544e --- /dev/null +++ b/packages/eslint-plugin/.eslintrc.js @@ -0,0 +1,34 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Custom lint config here since source is ES5 +module.exports = { + plugins: ['@backstage'], + env: { + node: true, + es2021: true, + }, + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + lib: require('@backstage/cli/config/tsconfig.json').compilerOptions.lib, + }, + rules: { + '@backstage/no-undeclared-imports': ['error'], + 'no-unused-expressions': 'off', + }, +}; diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md new file mode 100644 index 0000000000..35e3c8f0ea --- /dev/null +++ b/packages/eslint-plugin/CHANGELOG.md @@ -0,0 +1,7 @@ +# @backstage/eslint-plugin + +## 0.1.0-next.0 + +### Minor Changes + +- 179d301518: Added a new ESLint plugin with common rules for Backstage projects. See the [README](https://github.com/import-js/eslint-plugin-import/blob/main/packages/eslint-plugin/README.md) for more details. diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md new file mode 100644 index 0000000000..c311670603 --- /dev/null +++ b/packages/eslint-plugin/README.md @@ -0,0 +1,42 @@ +# @backstage/eslint-plugin + +A collection of ESLint rules useful to Backstage projects. + +## Usage + +This ESLint plugin is part of the default lint configuration provided by the [Backstage CLI](https://www.npmjs.com/package/@backstage/cli), so you generally do not need to install it manually. + +If you do wish to install this plugin manually, start by adding it as a development dependency to your project: + +```sh +yarn add --dev @backstage/eslint-plugin +``` + +Then add it to your ESLint configuration: + +```js +extends: [ + 'plugin:@backstage/recommended', +], +``` + +Alternatively, if you want to install in individual rules manually: + +```js +plugins: [ + '@backstage', +], +rules: { + '@backstage/no-forbidden-package-imports': 'error', +} +``` + +## Rules + +The following rules are provided by this plugin: + +| Rule | Description | +| --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | +| [@backstage/no-forbidden-package-imports](./docs/rules/no-forbidden-package-imports.md) | Disallow internal monorepo imports from package subpaths that are not exported. | +| [@backstage/no-relative-monorepo-imports](./docs/rules/no-relative-monorepo-imports.md) | Forbid relative imports that reach outside of the package in a monorepo. | +| [@backstage/no-undeclared-imports](./docs/rules/no-undeclared-imports.md) | Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. | diff --git a/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md b/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md new file mode 100644 index 0000000000..7a6ae11c37 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md @@ -0,0 +1,52 @@ +# @backstage/no-forbidden-package-imports + +Disallow internal monorepo imports from package subpaths that are not exported. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-forbidden-package-imports": ["error"] +``` + +## Rule Details + +Given the following two target packages: + +```json +{ + "name": "@backstage/plugin-foo", + "files": ["dist", "type-utils"] +} +``` + +```json +{ + "name": "@backstage/plugin-bar", + "exports": { + ".": "./src/index.ts", + "./testUtils": "./src/testUtils/index.ts", + "./package.json": "./package.json" + } +} +``` + +### Fail + +```ts +import { FooCard } from '@backstage/plugin-foo/src/components'; +import { BarCard } from '@backstage/plugin-bar/src/components'; +``` + +### Pass + +```ts +import { FooCard } from '@backstage/plugin-foo'; +import { FooType } from '@backstage/plugin-foo/type-utils'; +import fooPkg from '@backstage/plugin-foo/package.json'; + +import { BarCard } from '@backstage/plugin-bar'; +import { renderBarCardExtension } from '@backstage/plugin-bar/testUtils'; +import barPkg from '@backstage/plugin-bar/package.json'; +``` diff --git a/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md b/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md new file mode 100644 index 0000000000..6a41a73fb6 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md @@ -0,0 +1,43 @@ +# @backstage/no-relative-monorepo-imports + +Forbid relative imports that reach outside of the package in a monorepo. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-relative-monorepo-imports": ["error"] +``` + +The following patterns are considered files used during development, and only need dependencies to be declared in devDependencies: + +```python +!src/** # Any files outside of src are considered dev files +src/**/*.test.* +src/**/*.stories.* +src/**/__testUtils__/** +src/**/__mocks__/** +src/setupTests.* +``` + +## Rule Details + +Assuming an import from for example `plugins/bar/src/index.ts`: + +### Fail + +```ts +import { FooCard } from '../../foo'; + +import { FooCard } from '../../foo/src/components/FooCard'; +``` + +### Pass + +```ts +import { FooCard } from '@internal/plugin-foo'; + +// This is allowed by this rule, but not by no-forbidden-package-imports +import { FooCard } from '@internal/plugin-foo/src/components/FooCard'; +``` diff --git a/packages/eslint-plugin/docs/rules/no-undeclared-imports.md b/packages/eslint-plugin/docs/rules/no-undeclared-imports.md new file mode 100644 index 0000000000..6102c772e6 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-undeclared-imports.md @@ -0,0 +1,84 @@ +# @backstage/no-undeclared-imports + +Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`. + +## Usage + +Add the rules as follows, it has no options: + +```js +"@backstage/no-undeclared-imports": ["error"] +``` + +The following patterns are considered files used during development, and only need dependencies to be declared in devDependencies: + +```python +!src/** # Any files outside of src are considered dev files +src/**/*.test.* +src/**/*.stories.* +src/**/__testUtils__/** +src/**/__mocks__/** +src/setupTests.* +``` + +## Rule Details + +Given the following `package.json`: + +```json +{ + "name": "@backstage/plugin-foo", + "backstage": { + "role": "frontend-plugin" + }, + "dependencies": { + "react": "^17.0.0" + }, + "devDependencies": { + "@backstage/core-plugin-api": "^1.0.0" + }, + "peerDependencies": { + "@backstage/config": "*" + } +} +``` + +### Fail + +Inside `src/my-plugin.ts`: + +```ts +// Should be declared as a dependency +const _ = require('lodash'); +import _ from 'lodash'; + +// React should be a peer dependency in frontend plugins +import react from 'react'; + +// Should be declared as a dependency, not a dev dependency +import { useApi } from '@backstage/core-plugin-api'; +``` + +Inside `src/my-plugin.test.ts` (a test file): + +```ts +// Should be declared as a dev dependency +const _ = require('lodash'); +import _ from 'lodash'; +``` + +### Pass + +Inside `src/my-plugin.ts`: + +```ts +// Declared in peerDependencies, so it is allowed +import { ConfigReader } from '@backstage/config'; +``` + +Inside `src/my-plugin.test.ts` (a test file): + +```ts +// Declared as a dev dependency inside a test file +import { useApi } from '@backstage/core-plugin-api'; +``` diff --git a/packages/eslint-plugin/index.js b/packages/eslint-plugin/index.js new file mode 100644 index 0000000000..20a0ff2d18 --- /dev/null +++ b/packages/eslint-plugin/index.js @@ -0,0 +1,33 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +module.exports = { + configs: { + recommended: { + plugins: ['@backstage'], + rules: { + '@backstage/no-forbidden-package-imports': 'error', + '@backstage/no-relative-monorepo-imports': 'error', + '@backstage/no-undeclared-imports': 'error', + }, + }, + }, + rules: { + 'no-forbidden-package-imports': require('./rules/no-forbidden-package-imports'), + 'no-relative-monorepo-imports': require('./rules/no-relative-monorepo-imports'), + 'no-undeclared-imports': require('./rules/no-undeclared-imports'), + }, +}; diff --git a/packages/eslint-plugin/lib/getPackages.js b/packages/eslint-plugin/lib/getPackages.js new file mode 100644 index 0000000000..f5f2c63cc1 --- /dev/null +++ b/packages/eslint-plugin/lib/getPackages.js @@ -0,0 +1,71 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const manypkg = require('@manypkg/get-packages'); + +/** + * @typedef ExtendedPackage + * @type {import('@manypkg/get-packages').Package & { packageJson: { exports?: Record, files?: Array }}} packageJson + */ + +/** + * @typedef PackageMap + * @type object + * + * @property {ExtendedPackage} root + * @property {ExtendedPackage[]} list + * @property {Map} map + * @property {(path: string) => ExtendedPackage | undefined} byPath + */ + +// Loads all packages in the monorepo once, and caches the result +module.exports = (function () { + /** @type {PackageMap | undefined} */ + let result = undefined; + /** @type {number} */ + let lastLoadAt = 0; + + /** @returns {PackageMap | undefined} */ + return function getPackages(/** @type {string} */ dir) { + if (result) { + // Only cache for 5 seconds, to avoid the need to reload ESLint servers + if (Date.now() - lastLoadAt > 5000) { + result = undefined; + } else { + return result; + } + } + const packages = manypkg.getPackagesSync(dir); + if (!packages) { + return undefined; + } + result = { + map: new Map(packages.packages.map(pkg => [pkg.packageJson.name, pkg])), + list: packages.packages, + root: packages.root, + byPath(filePath) { + return packages.packages.find( + pkg => !path.relative(pkg.dir, filePath).startsWith('..'), + ); + }, + }; + lastLoadAt = Date.now(); + return result; + }; +})(); diff --git a/packages/eslint-plugin/lib/visitImports.js b/packages/eslint-plugin/lib/visitImports.js new file mode 100644 index 0000000000..43401bf5f9 --- /dev/null +++ b/packages/eslint-plugin/lib/visitImports.js @@ -0,0 +1,173 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const { builtinModules } = require('module'); +const getPackages = require('./getPackages'); + +/** + * @typedef LocalImport + * @type {object} + * @property {'local'} type + * @property {'value' | 'type'} kind + * @property {string} path + */ + +/** + * @typedef InternalImport + * @type {object} + * @property {'internal'} type + * @property {'value' | 'type'} kind + * @property {string} path + * @property {import('./getPackages').ExtendedPackage} package + */ + +/** + * @typedef ExternalImport + * @type {object} + * @property {'external'} type + * @property {'value' | 'type'} kind + * @property {string} path + * @property {string} packageName + */ + +/** + * @typedef BuiltinImport + * @type {object} + * @property {'builtin'} type + * @property {'value' | 'type'} kind + * @property {string} path + * @property {string} packageName + */ + +/** + * @callback ImportVisitor + * @param {ConsideredNode} node + * @param {LocalImport | InternalImport | ExternalImport | BuiltinImport} import + */ + +/** + * @typedef ConsideredNode + * @type {import('estree').ImportDeclaration | import('estree').ExportAllDeclaration | import('estree').ExportNamedDeclaration | import('estree').ImportExpression | import('estree').SimpleCallExpression} + */ + +/** + * @param {ConsideredNode} node + * @returns {undefined | {path: string, kind: 'type' | 'value'}} + */ +function getImportInfo(node) { + /** @type {import('estree').Expression | import('estree').SpreadElement | undefined | null} */ + let pathNode; + + if (node.type === 'CallExpression') { + if ( + node.callee.type === 'Identifier' && + node.callee.name == 'require' && + node.arguments.length === 1 + ) { + pathNode = node.arguments[0]; + } + } else { + pathNode = node.source; + } + + if (pathNode?.type !== 'Literal') { + return undefined; + } + if (typeof pathNode.value !== 'string') { + return undefined; + } + + /** @type {any} */ + const anyNode = node; + return { path: pathNode.value, kind: anyNode.importKind ?? 'value' }; +} + +/** + * @param visitor - Visitor callback + * @param {import('eslint').Rule.RuleContext} context + * @param {ImportVisitor} visitor + */ +module.exports = function visitImports(context, visitor) { + const packages = getPackages(context.getCwd()); + if (!packages) { + return; + } + + /** + * @param {ConsideredNode} node + */ + function visit(node) { + const info = getImportInfo(node); + if (!info) { + return; + } + + if (info.path[0] === '.') { + return visitor(node, { type: 'local', ...info }); + } + + const pathParts = info.path.split('/'); + + // Check for match with plain name, then namespaced name + let packageName; + let subPath; + if (info.path[0] === '@') { + packageName = pathParts.slice(0, 2).join('/'); + subPath = pathParts.slice(2).join('/'); + } else { + packageName = pathParts[0]; + subPath = pathParts.slice(1).join('/'); + } + const pkg = packages?.map.get(packageName); + if (!pkg) { + if ( + packageName.startsWith('node:') || + builtinModules.includes(packageName) + ) { + return visitor(node, { + type: 'builtin', + kind: info.kind, + path: subPath, + packageName, + }); + } + + return visitor(node, { + type: 'external', + kind: info.kind, + path: subPath, + packageName, + }); + } + + return visitor(node, { + type: 'internal', + kind: info.kind, + path: subPath, + package: pkg, + }); + } + + return { + ImportDeclaration: visit, + ExportAllDeclaration: visit, + ExportNamedDeclaration: visit, + ImportExpression: visit, + CallExpression: visit, + }; +}; diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json new file mode 100644 index 0000000000..0aea11d370 --- /dev/null +++ b/packages/eslint-plugin/package.json @@ -0,0 +1,28 @@ +{ + "name": "@backstage/eslint-plugin", + "description": "Backstage ESLint plugin", + "version": "0.1.0-next.0", + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "packages/eslint-plugin" + }, + "license": "Apache-2.0", + "main": "./index.js", + "scripts": { + "lint": "backstage-cli package lint", + "test": "backstage-cli package test" + }, + "dependencies": { + "@manypkg/get-packages": "^1.1.3", + "minimatch": "^5.1.2" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "eslint": "^8.33.0" + } +} diff --git a/packages/eslint-plugin/rules/no-forbidden-package-imports.js b/packages/eslint-plugin/rules/no-forbidden-package-imports.js new file mode 100644 index 0000000000..e7c1acca27 --- /dev/null +++ b/packages/eslint-plugin/rules/no-forbidden-package-imports.js @@ -0,0 +1,74 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const visitImports = require('../lib/visitImports'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + forbidden: '{{packageName}} does not export {{subPath}}', + }, + docs: { + description: + 'Disallow internal monorepo imports from package subpaths that are not exported.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-forbidden-package-imports.md', + }, + }, + create(context) { + return visitImports(context, (node, imp) => { + if (imp.type !== 'internal') { + return; + } + // Empty subpaths are always allowed + if (!imp.path) { + return; + } + + // If the import is listed in the package.json exports field, we allow it + const exp = imp.package.packageJson.exports; + if (exp && (exp[imp.path] || exp['./' + imp.path])) { + return; + } + if (!exp) { + // If there's no exports field, we allow anything listed in files, except dist + const files = imp.package.packageJson.files; + if ( + !files || + files.some(f => !f.startsWith('dist') && imp.path.startsWith(f)) + ) { + return; + } + // And also package.json + if (imp.path === 'package.json') { + return; + } + } + + context.report({ + node: node, + messageId: 'forbidden', + data: { + packageName: imp.package.packageJson.name || imp.package.dir, + subPath: imp.path, + }, + }); + }); + }, +}; diff --git a/packages/eslint-plugin/rules/no-relative-monorepo-imports.js b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js new file mode 100644 index 0000000000..df30d35f67 --- /dev/null +++ b/packages/eslint-plugin/rules/no-relative-monorepo-imports.js @@ -0,0 +1,85 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const visitImports = require('../lib/visitImports'); +const getPackageMap = require('../lib/getPackages'); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + outside: 'Import of {{path}} is outside of any known monorepo package', + forbidden: + "Relative imports of monorepo packages are forbidden, use '{{newImport}}' instead", + }, + docs: { + description: + 'Forbid relative imports that reach outside of the package in a monorepo.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-relative-monorepo-imports.md', + }, + }, + create(context) { + const packages = getPackageMap(context.getCwd()); + if (!packages) { + return {}; + } + const filePath = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(); + + const localPkg = packages.byPath(filePath); + if (!localPkg) { + return {}; + } + + return visitImports(context, (node, imp) => { + if (imp.type !== 'local') { + return; + } + + const target = path.resolve(path.dirname(filePath), imp.path); + if (!path.relative(localPkg.dir, target).startsWith('..')) { + return; + } + + const targetPkg = packages.byPath(target); + if (!targetPkg) { + context.report({ + node: node, + messageId: 'outside', + data: { + path: target, + }, + }); + return; + } + + const targetPath = path.relative(targetPkg.dir, target); + const targetName = targetPkg.packageJson.name ?? ''; + context.report({ + node: node, + messageId: 'forbidden', + data: { + newImport: targetPath ? `${targetName}/${targetPath}` : targetName, + }, + }); + }); + }, +}; diff --git a/packages/eslint-plugin/rules/no-undeclared-imports.js b/packages/eslint-plugin/rules/no-undeclared-imports.js new file mode 100644 index 0000000000..42624beb5c --- /dev/null +++ b/packages/eslint-plugin/rules/no-undeclared-imports.js @@ -0,0 +1,205 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +const path = require('path'); +const getPackageMap = require('../lib/getPackages'); +const visitImports = require('../lib/visitImports'); +const minimatch = require('minimatch'); + +const depFields = { + dep: 'dependencies', + dev: 'devDependencies', + peer: 'peerDependencies', +}; + +const devModulePatterns = [ + new minimatch.Minimatch('!src/**'), + new minimatch.Minimatch('src/**/*.test.*'), + new minimatch.Minimatch('src/**/*.stories.*'), + new minimatch.Minimatch('src/**/__testUtils__/**'), + new minimatch.Minimatch('src/**/__mocks__/**'), + new minimatch.Minimatch('src/setupTests.*'), +]; + +function getExpectedDepType( + /** @type {any} */ localPkg, + /** @type {string} */ impPath, + /** @type {string} */ modulePath, +) { + const role = localPkg?.backstage?.role; + // Some package roles have known dependency types + switch (role) { + case 'common-library': + case 'web-library': + case 'frontend-plugin': + case 'frontend-plugin-module': + case 'node-library': + case 'backend-plugin': + case 'backend-plugin-module': + switch (impPath) { + case 'react': + case 'react-dom': + case 'react-router': + case 'react-router-dom': + return 'peer'; + } + break; + case 'cli': + case 'frontend': + case 'backend': + default: + break; + } + + for (const pattern of devModulePatterns) { + if (pattern.match(modulePath)) { + return 'dev'; + } + } + return 'dep'; +} + +/** + * + * @param {import('@manypkg/get-packages').Package['packageJson']} pkg + * @param {string} name + * @param {ReturnType} expectedType + * @returns {{oldDepsField?: string, depsField: string} | undefined} + */ +function findConflict(pkg, name, expectedType) { + const isDep = pkg.dependencies?.[name]; + const isDevDep = pkg.devDependencies?.[name]; + const isPeerDep = pkg.peerDependencies?.[name]; + const depsField = depFields[expectedType]; + + if (expectedType === 'dep' && !isDep && !isPeerDep) { + const oldDepsField = isDevDep ? depFields.dev : undefined; + return { oldDepsField, depsField }; + } else if (expectedType === 'dev' && !isDevDep && !isDep && !isPeerDep) { + return { oldDepsField: undefined, depsField }; + } else if (expectedType === 'peer' && !isPeerDep) { + const oldDepsField = isDep + ? depFields.dep + : isDevDep + ? depFields.dev + : undefined; + + return { oldDepsField, depsField }; + } + return undefined; +} + +/** + * @param {string} depsField + */ +function getAddFlagForDepsField(depsField) { + switch (depsField) { + case depFields.dep: + return ''; + case depFields.dev: + return ' --dev'; + case depFields.peer: + return ' --peer'; + default: + return ''; + } +} + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + messages: { + undeclared: + "{{ packageName }} must be declared in {{ depsField }} of {{ packageJsonPath }}, run 'yarn --cwd {{ packagePath }} add{{ addFlag }} {{ packageName }}' from the project root.", + switch: + '{{ packageName }} is declared in {{ oldDepsField }}, but should be moved to {{ depsField }} in {{ packageJsonPath }}.', + }, + docs: { + description: + 'Forbid imports of external packages that have not been declared in the appropriate dependencies field in `package.json`.', + url: 'https://github.com/backstage/backstage/blob/master/packages/eslint-plugin/docs/rules/no-undeclared-imports.md', + }, + }, + create(context) { + const packages = getPackageMap(context.getCwd()); + if (!packages) { + return {}; + } + const filePath = context.getPhysicalFilename + ? context.getPhysicalFilename() + : context.getFilename(); + + const localPkg = packages.byPath(filePath); + if (!localPkg) { + return {}; + } + + return visitImports(context, (node, imp) => { + if (imp.type !== 'external') { + return; + } + // We leave checking of type imports to the repo-tools check + if (imp.kind === 'type') { + return; + } + + const modulePath = path.relative(localPkg.dir, filePath); + const expectedType = getExpectedDepType( + localPkg.packageJson, + imp.packageName, + modulePath, + ); + + const conflict = findConflict( + localPkg.packageJson, + imp.packageName, + expectedType, + ); + + if (conflict) { + try { + const fullImport = imp.path + ? `${imp.packageName}/${imp.path}` + : imp.packageName; + require.resolve(fullImport, { + paths: [localPkg.dir], + }); + } catch { + // If the dependency doesn't resolve then it's likely a type import, ignore + return; + } + + const packagePath = path.relative(packages.root.dir, localPkg.dir); + const packageJsonPath = path.join(packagePath, 'package.json'); + + context.report({ + node, + messageId: conflict.oldDepsField ? 'switch' : 'undeclared', + data: { + ...conflict, + packagePath, + addFlag: getAddFlagForDepsField(conflict.depsField), + packageName: imp.packageName, + packageJsonPath: packageJsonPath, + }, + }); + } + }); + }, +}; diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/package.json new file mode 100644 index 0000000000..c35178c46f --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/package.json @@ -0,0 +1,6 @@ +{ + "name": "root", + "workspaces": [ + "packages/*" + ] +} diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json new file mode 100644 index 0000000000..efef27047b --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/bar/package.json @@ -0,0 +1,17 @@ +{ + "name": "@internal/bar", + "exports": { + ".": "./src/index.ts", + "./BarPage": "./src/components/Bar.tsx", + "./package.json": "./package.json" + }, + "backstage": { + "role": "frontend-plugin" + }, + "dependencies": { + "react-router": "*" + }, + "devDependencies": { + "react-router-dom": "*" + } +} diff --git a/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json new file mode 100644 index 0000000000..563ec35120 --- /dev/null +++ b/packages/eslint-plugin/src/__fixtures__/monorepo/packages/foo/package.json @@ -0,0 +1,16 @@ +{ + "name": "@internal/foo", + "dependencies": { + "@internal/bar": "1.0.0" + }, + "devDependencies": { + "lodash": "*" + }, + "peerDependencies": { + "react": "*" + }, + "files": [ + "dist", + "type-utils" + ] +} diff --git a/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts b/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts new file mode 100644 index 0000000000..349b71319f --- /dev/null +++ b/packages/eslint-plugin/src/no-forbidden-package-imports.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import path from 'path'; +import rule from '../rules/no-forbidden-package-imports'; + +const RULE = 'no-forbidden-package-imports'; +const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo'); + +const ERR = (name: string, path: string) => ({ + message: `${name} does not export ${path}`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import '@internal/foo'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/type-utils'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/type-utils/anything'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/foo/package.json'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar/package.json'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + { + code: `import '@internal/bar/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + }, + ], + invalid: [ + { + code: `import '@internal/foo/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'FooPage')], + }, + { + code: `import '@internal/foo/dist'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'dist')], + }, + { + code: `import '@internal/foo/dist/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'dist/FooPage')], + }, + { + code: `import '@internal/foo/src/FooPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'src/FooPage')], + }, + { + code: `import '@internal/foo/src'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/foo', 'src')], + }, + { + code: `import '@internal/bar/OtherBarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'OtherBarPage')], + }, + { + code: `import '@internal/bar/dist'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'dist')], + }, + { + code: `import '@internal/bar/dist/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'dist/BarPage')], + }, + { + code: `import '@internal/bar/src/BarPage'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'src/BarPage')], + }, + { + code: `import '@internal/bar/src'`, + filename: path.join(FIXTURE, 'index.ts'), + errors: [ERR('@internal/bar', 'src')], + }, + ], +}); diff --git a/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts b/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts new file mode 100644 index 0000000000..6888d258cc --- /dev/null +++ b/packages/eslint-plugin/src/no-relative-monorepo-imports.test.ts @@ -0,0 +1,68 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import path from 'path'; +import rule from '../rules/no-relative-monorepo-imports'; + +const RULE = 'no-relative-monorepo-imports'; +const FIXTURE = path.resolve(__dirname, '__fixtures__/monorepo'); + +const ERR_OUTSIDE = (path: string) => ({ + message: `Import of ${path} is outside of any known monorepo package`, +}); +const ERR_FORBIDDEN = (newImp: string) => ({ + message: `Relative imports of monorepo packages are forbidden, use '${newImp}' instead`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import { version } from '@internal/foo'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + }, + { + code: `import { version } from '@internal/foo/src'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + }, + ], + invalid: [ + { + code: `import { version } from '../../foo'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_FORBIDDEN('@internal/foo')], + }, + { + code: `import { version } from '../../foo/src'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_FORBIDDEN('@internal/foo/src')], + }, + { + code: `import { version } from '../../../package.json'`, + filename: path.join(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ERR_OUTSIDE(path.join(FIXTURE, 'package.json'))], + }, + ], +}); diff --git a/packages/eslint-plugin/src/no-undeclared-imports.test.ts b/packages/eslint-plugin/src/no-undeclared-imports.test.ts new file mode 100644 index 0000000000..1b5cbee853 --- /dev/null +++ b/packages/eslint-plugin/src/no-undeclared-imports.test.ts @@ -0,0 +1,240 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { RuleTester } from 'eslint'; +import { join as joinPath } from 'path'; +import rule from '../rules/no-undeclared-imports'; + +const RULE = 'no-undeclared-imports'; +const FIXTURE = joinPath(__dirname, '__fixtures__/monorepo'); + +const ERR_UNDECLARED = ( + name: string, + field: string, + path: string, + flag?: string, +) => ({ + message: `${name} must be declared in ${field} of ${joinPath( + path, + 'package.json', + )}, run 'yarn --cwd ${path} add${ + flag ? ` ${flag}` : '' + } ${name}' from the project root.`, +}); +const ERR_SWITCHED = ( + name: string, + old: string, + field: string, + path: string, +) => ({ + message: `${name} is declared in ${old}, but should be moved to ${field} in ${joinPath( + path, + 'package.json', + )}.`, +}); + +process.chdir(FIXTURE); + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + ecmaVersion: 2021, + }, +}); + +ruleTester.run(RULE, rule, { + valid: [ + { + code: `import '@internal/foo'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import '@internal/bar'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import 'react'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + }, + { + code: `import '@internal/foo'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import '@internal/bar'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + code: `import 'react'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + }, + { + // We're only able to validate literals + code: `require('lod' + 'ash')`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + }, + ], + invalid: [ + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'lodash', + 'devDependencies', + 'dependencies', + joinPath('packages', 'foo'), + ), + ], + }, + { + code: `import 'react-router'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'react-router', + 'dependencies', + 'peerDependencies', + joinPath('packages', 'bar'), + ), + ], + }, + { + code: `import 'react-router-dom'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_SWITCHED( + 'react-router-dom', + 'devDependencies', + 'peerDependencies', + joinPath('packages', 'bar'), + ), + ], + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import { debounce } from 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import * as _ from 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import _ from 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import('lodash')`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `require('lodash')`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED('lodash', 'dependencies', joinPath('packages', 'bar')), + ], + }, + { + code: `import 'lodash'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.test.ts'), + errors: [ + ERR_UNDECLARED( + 'lodash', + 'devDependencies', + joinPath('packages', 'bar'), + '--dev', + ), + ], + }, + { + code: `import 'react'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.ts'), + errors: [ + ERR_UNDECLARED( + 'react', + 'peerDependencies', + joinPath('packages', 'bar'), + '--peer', + ), + ], + }, + { + code: `import 'react'`, + filename: joinPath(FIXTURE, 'packages/bar/src/index.test.ts'), + errors: [ + ERR_UNDECLARED( + 'react', + 'peerDependencies', + joinPath('packages', 'bar'), + '--peer', + ), + ], + }, + { + code: `import 'react-dom'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.ts'), + errors: [ + ERR_UNDECLARED( + 'react-dom', + 'dependencies', + joinPath('packages', 'foo'), + ), + ], + }, + { + code: `import 'react-dom'`, + filename: joinPath(FIXTURE, 'packages/foo/src/index.test.ts'), + errors: [ + ERR_UNDECLARED( + 'react-dom', + 'devDependencies', + joinPath('packages', 'foo'), + '--dev', + ), + ], + }, + ], +}); diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md index 5e880265f8..8813458f86 100644 --- a/packages/integration-react/CHANGELOG.md +++ b/packages/integration-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/integration-react +## 1.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + ## 1.1.10-next.0 ### Patch Changes diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json index 8fdc8d4841..b287f88f87 100644 --- a/packages/integration-react/package.json +++ b/packages/integration-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/integration-react", "description": "Frontend package for managing integrations towards external systems", - "version": "1.1.10-next.0", + "version": "1.1.10-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md index 5969521581..2875d54fef 100644 --- a/packages/repo-tools/CHANGELOG.md +++ b/packages/repo-tools/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/repo-tools +## 0.1.2-next.0 + +### Patch Changes + +- ff63acf30a: Packages without a declared `backstage.role` are now ignored. +- Updated dependencies + - @backstage/cli-common@0.1.11 + - @backstage/errors@1.1.4 + ## 0.1.1 ### Patch Changes diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json index dd70575433..5ea26060c4 100644 --- a/packages/repo-tools/package.json +++ b/packages/repo-tools/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/repo-tools", "description": "CLI for Backstage repo tooling ", - "version": "0.1.1", + "version": "0.1.2-next.0", "publishConfig": { "access": "public" }, diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts index b037f40be6..39df70c21a 100644 --- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts +++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts @@ -1107,7 +1107,7 @@ export async function categorizePackageDirs(packageDirs: any[]) { }); const role = pkgJson?.backstage?.role; if (!role) { - throw new Error(`No backstage.role in ${dir}/package.json`); + return; // Ignore packages without roles } if (role === 'cli') { cliPackageDirs.push(dir); diff --git a/packages/repo-tools/src/commands/type-deps/type-deps.ts b/packages/repo-tools/src/commands/type-deps/type-deps.ts index d4dd28aa14..4f8e5e5529 100644 --- a/packages/repo-tools/src/commands/type-deps/type-deps.ts +++ b/packages/repo-tools/src/commands/type-deps/type-deps.ts @@ -17,7 +17,7 @@ import fs from 'fs'; import { resolve as resolvePath } from 'path'; // Cba polluting root package.json, we'll have this -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import chalk from 'chalk'; import { getPackages, Package } from '@manypkg/get-packages'; diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md index f82207ff06..55d39a6ee5 100644 --- a/packages/techdocs-cli-embedded-app/CHANGELOG.md +++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md @@ -1,5 +1,24 @@ # techdocs-cli-embedded-app +## 0.2.79-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/app-defaults@1.1.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + ## 0.2.79-next.1 ### Patch Changes diff --git a/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json b/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json index 2b3a458b95..a467608916 100644 --- a/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json +++ b/packages/techdocs-cli-embedded-app/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json index 68d443db99..acee2e8cb3 100644 --- a/packages/techdocs-cli-embedded-app/package.json +++ b/packages/techdocs-cli-embedded-app/package.json @@ -1,6 +1,6 @@ { "name": "techdocs-cli-embedded-app", - "version": "0.2.79-next.1", + "version": "0.2.79-next.2", "private": true, "backstage": { "role": "frontend" diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md index 8e0bc02dca..54bf7d2e8c 100644 --- a/packages/techdocs-cli/CHANGELOG.md +++ b/packages/techdocs-cli/CHANGELOG.md @@ -1,5 +1,16 @@ # @techdocs/cli +## 1.3.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/cli-common@0.1.11 + - @backstage/config@1.0.6 + - @backstage/plugin-techdocs-node@1.4.6-next.2 + ## 1.3.2-next.1 ### Patch Changes diff --git a/packages/techdocs-cli/cli-report.md b/packages/techdocs-cli/cli-report.md index 861dad4605..846ccc88ca 100644 --- a/packages/techdocs-cli/cli-report.md +++ b/packages/techdocs-cli/cli-report.md @@ -33,6 +33,7 @@ Options: --no-docker --techdocs-ref --etag + --site-name -v --verbose --omitTechdocsCoreMkdocsPlugin --legacyCopyReadmeMdToIndexMd @@ -98,6 +99,7 @@ Options: --docker-entrypoint --docker-option --no-docker + --site-name --mkdocs-port -v --verbose --preview-app-bundle-path @@ -115,6 +117,7 @@ Options: --docker-entrypoint --docker-option --no-docker + --site-name -p, --port -v --verbose -h, --help diff --git a/packages/techdocs-cli/cypress.config.js b/packages/techdocs-cli/cypress.config.js new file mode 100644 index 0000000000..abaa324d76 --- /dev/null +++ b/packages/techdocs-cli/cypress.config.js @@ -0,0 +1,30 @@ +/* + * Copyright 2022 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 { defineConfig } from 'cypress'; +import { initPlugin } from '@frsource/cypress-plugin-visual-regression-diff/plugins'; + +module.exports = defineConfig({ + e2e: { + setupNodeEvents(on, config) { + initPlugin(on, config); + }, + + excludeSpecPattern: ['**/__snapshots__/*', '**/__image_snapshots__/*'], + }, + viewportWidth: 1920, + viewportHeight: 1080, + includeShadowDom: true, +}); diff --git a/packages/techdocs-cli/cypress.json b/packages/techdocs-cli/cypress.json deleted file mode 100644 index b6f3b96019..0000000000 --- a/packages/techdocs-cli/cypress.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "env": { - "mkDocsBaseUrl": "http://localhost:8000", - "backstageBaseUrl": "http://localhost:3000", - "cypress-plugin-snapshots": { - "autoCleanUp": false, - "imageConfig": { - "resizeDevicePixelRatio": true, - "threshold": 0.01 - } - } - }, - "viewportWidth": 1920, - "viewportHeight": 1080, - "includeShadowDom": true, - "ignoreTestFiles": ["**/__snapshots__/*", "**/__image_snapshots__/*"] -} diff --git a/packages/techdocs-cli/cypress/.eslintrc.json b/packages/techdocs-cli/cypress/.eslintrc.json index 2481ac5715..06965a82da 100644 --- a/packages/techdocs-cli/cypress/.eslintrc.json +++ b/packages/techdocs-cli/cypress/.eslintrc.json @@ -7,15 +7,6 @@ { "assertFunctionNames": ["expect", "cy.contains", "cy.document"] } - ], - "import/no-extraneous-dependencies": [ - "error", - { - "devDependencies": true, - "optionalDependencies": true, - "peerDependencies": true, - "bundledDependencies": true - } ] } } diff --git a/packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png b/packages/techdocs-cli/cypress/e2e/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png similarity index 100% rename from packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png rename to packages/techdocs-cli/cypress/e2e/__image_snapshots__/TechDocs Live Preview - Backstage Serve toMatchImageSnapshot - Backstage TechDocs Page #0.png diff --git a/packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png b/packages/techdocs-cli/cypress/e2e/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png similarity index 100% rename from packages/techdocs-cli/cypress/integration/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png rename to packages/techdocs-cli/cypress/e2e/__image_snapshots__/TechDocs Live Preview - MkDocs Serve toMatchImageSnapshot - MkDocs Page #0.png diff --git a/packages/techdocs-cli/cypress/integration/backstage_serve.js b/packages/techdocs-cli/cypress/e2e/backstage_serve.cy.js similarity index 94% rename from packages/techdocs-cli/cypress/integration/backstage_serve.js rename to packages/techdocs-cli/cypress/e2e/backstage_serve.cy.js index 15fc03f8f4..0267c8bfe6 100644 --- a/packages/techdocs-cli/cypress/integration/backstage_serve.js +++ b/packages/techdocs-cli/cypress/e2e/backstage_serve.cy.js @@ -48,11 +48,11 @@ describe('TechDocs Live Preview - Backstage Serve', () => { cy.get('.md-footer').should('have.length', 1); }); - it('toMatchImageSnapshot - Backstage TechDocs Page', () => { + it('matchImage - Backstage TechDocs Page', () => { cy.visit( `${Cypress.env('backstageBaseUrl')}/docs/default/component/local`, ).then(() => { - cy.document().toMatchImageSnapshot(); + cy.document().matchImage(); }); }); }); diff --git a/packages/techdocs-cli/cypress/integration/mkdocs_serve.js b/packages/techdocs-cli/cypress/e2e/mkdocs_serve.cy.js similarity index 88% rename from packages/techdocs-cli/cypress/integration/mkdocs_serve.js rename to packages/techdocs-cli/cypress/e2e/mkdocs_serve.cy.js index 1ace7298ce..d5417e615f 100644 --- a/packages/techdocs-cli/cypress/integration/mkdocs_serve.js +++ b/packages/techdocs-cli/cypress/e2e/mkdocs_serve.cy.js @@ -16,7 +16,7 @@ /// describe('TechDocs Live Preview - MkDocs Serve', () => { it('successfully serves documentation', () => { - cy.visit(Cypress.env('mkDocsBaseUrl')); + cy.visit(Cypress.env('mkdocsBaseUrl')); cy.contains('hello mock docs'); }); @@ -40,9 +40,9 @@ describe('TechDocs Live Preview - MkDocs Serve', () => { cy.get('.md-footer').should('have.length', 1); }); - it('toMatchImageSnapshot - MkDocs Page', () => { - cy.visit(Cypress.env('mkDocsBaseUrl')).then(() => { - cy.document().toMatchImageSnapshot(); + it('matchImage - MkDocs Page', () => { + cy.visit(Cypress.env('mkdocsBaseUrl')).then(() => { + cy.document().matchImage(); }); }); }); diff --git a/packages/techdocs-cli/cypress/plugins/index.js b/packages/techdocs-cli/cypress/plugins/index.js deleted file mode 100644 index 92faabb6ad..0000000000 --- a/packages/techdocs-cli/cypress/plugins/index.js +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2022 The Backstage Authors - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// *********************************************************** -// This example plugins/index.js can be used to load plugins -// -// You can change the location of this file or turn off loading -// the plugins file with the 'pluginsFile' configuration option. -// -// You can read more here: -// https://on.cypress.io/plugins-guide -// *********************************************************** - -// This function is called when a project is opened or re-opened (e.g. due to -// the project's config changing) - -const { initPlugin } = require('cypress-plugin-snapshots/plugin'); - -/** - * @type {Cypress.PluginConfig} - */ -// eslint-disable-next-line no-unused-vars -module.exports = (on, config) => { - // `on` is used to hook into various events Cypress emits - // `config` is the resolved Cypress config - initPlugin(on, config); - return config; -}; diff --git a/packages/techdocs-cli/cypress/support/index.js b/packages/techdocs-cli/cypress/support/e2e.js similarity index 94% rename from packages/techdocs-cli/cypress/support/index.js rename to packages/techdocs-cli/cypress/support/e2e.js index a1a5b10ea4..f06a08e9a5 100644 --- a/packages/techdocs-cli/cypress/support/index.js +++ b/packages/techdocs-cli/cypress/support/e2e.js @@ -30,7 +30,7 @@ // Import commands.js using ES2015 syntax: import './commands'; -import 'cypress-plugin-snapshots/commands'; +import '@frsource/cypress-plugin-visual-regression-diff/commands'; // Alternatively you can use CommonJS syntax: // require('./commands') diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json index 4e6bde27c2..092bd687bc 100644 --- a/packages/techdocs-cli/package.json +++ b/packages/techdocs-cli/package.json @@ -1,7 +1,7 @@ { "name": "@techdocs/cli", "description": "Utility CLI for managing TechDocs sites in Backstage.", - "version": "1.3.2-next.1", + "version": "1.3.2-next.2", "publishConfig": { "access": "public" }, @@ -37,6 +37,7 @@ }, "devDependencies": { "@backstage/cli": "workspace:^", + "@frsource/cypress-plugin-visual-regression-diff": "^3.2.8", "@types/commander": "^2.12.2", "@types/fs-extra": "^9.0.6", "@types/http-proxy": "^1.17.4", @@ -44,7 +45,6 @@ "@types/serve-handler": "^6.1.0", "@types/webpack-env": "^1.15.3", "cypress": "^10.0.0", - "cypress-plugin-snapshots": "^1.4.4", "find-process": "^1.4.5", "nodemon": "^2.0.2", "techdocs-cli-embedded-app": "link:../techdocs-cli-embedded-app", diff --git a/packages/techdocs-cli/src/commands/generate/generate.ts b/packages/techdocs-cli/src/commands/generate/generate.ts index 874b904d86..b8ad466bb3 100644 --- a/packages/techdocs-cli/src/commands/generate/generate.ts +++ b/packages/techdocs-cli/src/commands/generate/generate.ts @@ -21,6 +21,7 @@ import Docker from 'dockerode'; import { TechdocsGenerator, ParsedLocationAnnotation, + getMkdocsYml, } from '@backstage/plugin-techdocs-node'; import { ContainerRunner, @@ -54,6 +55,10 @@ export default async function generate(opts: OptionValues) { await fs.ensureDir(outputDir); + const { path: mkdocsYmlPath, configIsTemporary } = await getMkdocsYml( + sourceDir, + ); + const config = new ConfigReader({ techdocs: { generator: { @@ -106,7 +111,14 @@ export default async function generate(opts: OptionValues) { logger, etag: opts.etag, ...(process.env.LOG_LEVEL === 'debug' ? { logStream: stdout } : {}), + siteOptions: { name: opts.siteName }, }); + if (configIsTemporary) { + process.on('exit', async () => { + fs.rmSync(mkdocsYmlPath, {}); + }); + } + logger.info('Done!'); } diff --git a/packages/techdocs-cli/src/commands/index.ts b/packages/techdocs-cli/src/commands/index.ts index caa7d8efbd..102e6d87de 100644 --- a/packages/techdocs-cli/src/commands/index.ts +++ b/packages/techdocs-cli/src/commands/index.ts @@ -54,6 +54,11 @@ export function registerCommands(program: Command) { '--etag ', 'A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json.', ) + .option( + '--site-name', + 'Name for site when using default MkDocs config', + 'Documentation Site', + ) .option('-v --verbose', 'Enable verbose output.', false) .option( '--omitTechdocsCoreMkdocsPlugin', @@ -224,6 +229,11 @@ export function registerCommands(program: Command) { '--no-docker', 'Do not use Docker, run `mkdocs serve` in current user environment.', ) + .option( + '--site-name', + 'Name for site when using default MkDocs config', + 'Documentation Site', + ) .option('-p, --port ', 'Port to serve documentation locally', '8000') .option('-v --verbose', 'Enable verbose output.', false) .action(lazy(() => import('./serve/mkdocs').then(m => m.default))); @@ -250,6 +260,11 @@ export function registerCommands(program: Command) { '--no-docker', 'Do not use Docker, use MkDocs executable in current user environment.', ) + .option( + '--site-name', + 'Name for site when using default MkDocs config', + 'Documentation Site', + ) .option('--mkdocs-port ', 'Port for MkDocs server to use', '8000') .option('-v --verbose', 'Enable verbose output.', false) .option( diff --git a/packages/techdocs-cli/src/commands/serve/mkdocs.ts b/packages/techdocs-cli/src/commands/serve/mkdocs.ts index 9cc0aef3be..c061d4474f 100644 --- a/packages/techdocs-cli/src/commands/serve/mkdocs.ts +++ b/packages/techdocs-cli/src/commands/serve/mkdocs.ts @@ -19,6 +19,8 @@ import openBrowser from 'react-dev-utils/openBrowser'; import { createLogger } from '../../lib/utility'; import { runMkdocsServer } from '../../lib/mkdocsServer'; import { LogFunc, waitForSignal } from '../../lib/run'; +import { getMkdocsYml } from '@backstage/plugin-techdocs-node'; +import fs from 'fs-extra'; export default async function serveMkdocs(opts: OptionValues) { const logger = createLogger({ verbose: opts.verbose }); @@ -26,6 +28,12 @@ export default async function serveMkdocs(opts: OptionValues) { const dockerAddr = `http://0.0.0.0:${opts.port}`; const localAddr = `http://127.0.0.1:${opts.port}`; const expectedDevAddr = opts.docker ? dockerAddr : localAddr; + + const { path: mkdocsYmlPath, configIsTemporary } = await getMkdocsYml( + './', + opts.siteName, + ); + // We want to open browser only once based on a log. let boolOpenBrowserTriggered = false; @@ -70,4 +78,10 @@ export default async function serveMkdocs(opts: OptionValues) { // Keep waiting for user to cancel the process await waitForSignal([childProcess]); + + if (configIsTemporary) { + process.on('exit', async () => { + fs.rmSync(mkdocsYmlPath, {}); + }); + } } diff --git a/packages/techdocs-cli/src/commands/serve/serve.ts b/packages/techdocs-cli/src/commands/serve/serve.ts index d46c70678a..9e2e521546 100644 --- a/packages/techdocs-cli/src/commands/serve/serve.ts +++ b/packages/techdocs-cli/src/commands/serve/serve.ts @@ -22,6 +22,8 @@ import HTTPServer from '../../lib/httpServer'; import { runMkdocsServer } from '../../lib/mkdocsServer'; import { LogFunc, waitForSignal } from '../../lib/run'; import { createLogger } from '../../lib/utility'; +import { getMkdocsYml } from '@backstage/plugin-techdocs-node'; +import fs from 'fs-extra'; function findPreviewBundlePath(): string { try { @@ -64,6 +66,11 @@ export default async function serve(opts: OptionValues) { ? mkdocsDockerAddr : mkdocsLocalAddr; + const { path: mkdocsYmlPath, configIsTemporary } = await getMkdocsYml( + './', + opts.siteName, + ); + let mkdocsServerHasStarted = false; const mkdocsLogFunc: LogFunc = data => { // Sometimes the lines contain an unnecessary extra new line @@ -120,7 +127,7 @@ export default async function serve(opts: OptionValues) { const httpServer = new HTTPServer( previewAppPath, port, - opts.mkdocsPort, + mkdocsExpectedDevAddr, opts.verbose, ); @@ -140,4 +147,10 @@ export default async function serve(opts: OptionValues) { }); await waitForSignal([mkdocsChildProcess]); + + if (configIsTemporary) { + process.on('exit', async () => { + fs.rmSync(mkdocsYmlPath, {}); + }); + } } diff --git a/packages/techdocs-cli/src/lib/PublisherConfig.ts b/packages/techdocs-cli/src/lib/PublisherConfig.ts index 0d1c8fc8a9..0ef6b66eaf 100644 --- a/packages/techdocs-cli/src/lib/PublisherConfig.ts +++ b/packages/techdocs-cli/src/lib/PublisherConfig.ts @@ -17,7 +17,7 @@ import { ConfigReader } from '@backstage/config'; import { OptionValues } from 'commander'; -type Publisher = keyof typeof PublisherConfig['configFactories']; +type Publisher = keyof (typeof PublisherConfig)['configFactories']; type PublisherConfiguration = { [p in Publisher]?: any; } & { @@ -73,7 +73,7 @@ export class PublisherConfig { */ private static isKnownPublisher( type: string, - ): type is keyof typeof PublisherConfig['configFactories'] { + ): type is keyof (typeof PublisherConfig)['configFactories'] { return PublisherConfig.configFactories.hasOwnProperty(type); } diff --git a/packages/techdocs-cli/src/lib/httpServer.ts b/packages/techdocs-cli/src/lib/httpServer.ts index c4bd5b823a..08987f3f1d 100644 --- a/packages/techdocs-cli/src/lib/httpServer.ts +++ b/packages/techdocs-cli/src/lib/httpServer.ts @@ -23,26 +23,26 @@ export default class HTTPServer { private readonly proxyEndpoint: string; private readonly backstageBundleDir: string; private readonly backstagePort: number; - private readonly mkdocsPort: number; + private readonly mkdocsTargetAddress: string; private readonly verbose: boolean; constructor( backstageBundleDir: string, backstagePort: number, - mkdocsPort: number, + mkdocsTargetAddress: string, verbose: boolean, ) { this.proxyEndpoint = '/api/techdocs/'; this.backstageBundleDir = backstageBundleDir; this.backstagePort = backstagePort; - this.mkdocsPort = mkdocsPort; + this.mkdocsTargetAddress = mkdocsTargetAddress; this.verbose = verbose; } // Create a Proxy for mkdocs server private createProxy() { const proxy = httpProxy.createProxyServer({ - target: `http://localhost:${this.mkdocsPort}`, + target: this.mkdocsTargetAddress, }); return (request: http.IncomingMessage): [httpProxy, string] => { diff --git a/packages/test-utils/src/testUtils/TestApiProvider.tsx b/packages/test-utils/src/testUtils/TestApiProvider.tsx index 51edf77cba..35fa302415 100644 --- a/packages/test-utils/src/testUtils/TestApiProvider.tsx +++ b/packages/test-utils/src/testUtils/TestApiProvider.tsx @@ -119,7 +119,7 @@ export class TestApiRegistry implements ApiHolder { * implements a subset of the `IdentityApi`. * * @public - **/ + */ export const TestApiProvider = ( props: TestApiProviderProps, ) => { diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/baseTheme.ts index 5e222c0c3e..53f3a3ef2b 100644 --- a/packages/theme/src/baseTheme.ts +++ b/packages/theme/src/baseTheme.ts @@ -159,7 +159,7 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides { head: { wordBreak: 'break-word', overflow: 'hidden', - color: 'rgb(179, 179, 179)', + color: theme.palette.textSubtle, fontWeight: 'normal', lineHeight: '1', }, diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md index c791947589..11f11a19b9 100644 --- a/plugins/adr-backend/CHANGELOG.md +++ b/plugins/adr-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-adr-backend +## 0.2.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-adr-common@0.2.6-next.1 + - @backstage/plugin-search-common@1.2.1 + ## 0.2.7-next.1 ### Patch Changes diff --git a/plugins/adr-backend/README.md b/plugins/adr-backend/README.md index b751a3b56d..29fead7c84 100644 --- a/plugins/adr-backend/README.md +++ b/plugins/adr-backend/README.md @@ -19,9 +19,8 @@ Here's how to get the backend up and running: 1. First we need to add the `@backstage/plugin-adr-backend` package to your backend: ```sh -# From the Backstage root directory -cd packages/backend -yarn add @backstage/plugin-adr-backend +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-adr-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/adr.ts`, and add the diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json index 0c74df5cf2..33679a00dc 100644 --- a/plugins/adr-backend/package.json +++ b/plugins/adr-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr-backend", - "version": "0.2.7-next.1", + "version": "0.2.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr-backend/src/search/createMadrParser.ts b/plugins/adr-backend/src/search/createMadrParser.ts index 6b77ec2fa7..1bc3c852e9 100644 --- a/plugins/adr-backend/src/search/createMadrParser.ts +++ b/plugins/adr-backend/src/search/createMadrParser.ts @@ -14,12 +14,11 @@ * limitations under the License. */ -import { DateTime } from 'luxon'; -import { marked } from 'marked'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { MADR_DATE_FORMAT } from '@backstage/plugin-adr-common'; import { AdrParser } from './types'; +import { madrParser } from './madrParser'; const applyArgsToFormat = ( format: string, @@ -65,43 +64,12 @@ export const createMadrParser = ( const dateFormat = options.dateFormat ?? MADR_DATE_FORMAT; return async ({ entity, content, path }) => { - const tokens = marked.lexer(content); - if (!tokens.length) { - throw new Error('ADR has no content'); - } - - // First h1 header should contain ADR title - const adrTitle = ( - tokens.find( - t => t.type === 'heading' && t.depth === 1, - ) as marked.Tokens.Heading - )?.text; - - // First list should contain status & date metadata (if defined) - const listTokens = ( - tokens.find(t => t.type === 'list') as marked.Tokens.List - )?.items; - const adrStatus = listTokens - .find(t => /^status:/i.test(t.text)) - ?.text.replace(/^status:/i, '') - .trim() - .toLocaleLowerCase('en-US'); - const adrDateTime = DateTime.fromFormat( - listTokens - .find(t => /^date:/i.test(t.text)) - ?.text.replace(/^date:/i, '') - .trim() ?? '', - dateFormat, - ); - const adrDate = adrDateTime.isValid - ? adrDateTime.toFormat(MADR_DATE_FORMAT) - : undefined; - + const madr = madrParser(content, dateFormat); return { - title: adrTitle ?? path.replace(/\.md$/, ''), + title: madr.title ?? path.replace(/\.md$/, ''), text: content, - status: adrStatus, - date: adrDate, + status: madr.status, + date: madr.date, entityRef: stringifyEntityRef(entity), entityTitle: entity.metadata.title, location: applyArgsToFormat(locationTemplate, { diff --git a/plugins/adr-backend/src/search/madrParser.ts b/plugins/adr-backend/src/search/madrParser.ts new file mode 100644 index 0000000000..30672028fe --- /dev/null +++ b/plugins/adr-backend/src/search/madrParser.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DateTime } from 'luxon'; +import { marked } from 'marked'; +import { MADR_DATE_FORMAT } from '@backstage/plugin-adr-common'; + +export const madrParser = (content: string, dateFormat = MADR_DATE_FORMAT) => { + const tokens = marked.lexer(content); + if (!tokens.length) { + throw new Error('ADR has no content'); + } + + // First h1 header should contain ADR title + const adrTitle = ( + tokens.find( + t => t.type === 'heading' && t.depth === 1, + ) as marked.Tokens.Heading + )?.text; + + // First list should contain status & date metadata (if defined) + const listTokens = (tokens.find(t => t.type === 'list') as marked.Tokens.List) + ?.items; + const adrStatus = listTokens + ?.find(t => /^status:/i.test(t.text)) + ?.text.replace(/^status:/i, '') + .trim() + .toLocaleLowerCase('en-US'); + + const adrDateTime = DateTime.fromFormat( + listTokens + ?.find(t => /^date:/i.test(t.text)) + ?.text.replace(/^date:/i, '') + .trim() ?? '', + dateFormat, + ); + const adrDate = adrDateTime?.isValid + ? adrDateTime.toFormat(MADR_DATE_FORMAT) + : undefined; + + return { + title: adrTitle, + status: adrStatus, + date: adrDate, + }; +}; diff --git a/plugins/adr-backend/src/service/router.test.ts b/plugins/adr-backend/src/service/router.test.ts index fbbd6e8532..6836343882 100644 --- a/plugins/adr-backend/src/service/router.test.ts +++ b/plugins/adr-backend/src/service/router.test.ts @@ -107,6 +107,8 @@ class MockCacheClient implements CacheClient { async delete(key: string) { delete this.itemRegistry[key]; } + + withOptions = () => this; } describe('createRouter', () => { @@ -156,8 +158,8 @@ describe('createRouter', () => { data: [ { type: 'file', - name: 'testFile001.txt', - path: 'folder/testFile001.txt', + name: 'testFile002.txt', + path: 'testFile002.txt', }, { type: 'file', @@ -166,8 +168,8 @@ describe('createRouter', () => { }, { type: 'file', - name: 'testFile002.txt', - path: 'testFile002.txt', + name: 'testFile001.txt', + path: 'folder/testFile001.txt', }, ], }; diff --git a/plugins/adr-backend/src/service/router.ts b/plugins/adr-backend/src/service/router.ts index e6603ab346..ed02a76e13 100644 --- a/plugins/adr-backend/src/service/router.ts +++ b/plugins/adr-backend/src/service/router.ts @@ -19,6 +19,7 @@ import { NotModifiedError, stringifyError } from '@backstage/errors'; import { Logger } from 'winston'; import express from 'express'; import Router from 'express-promise-router'; +import { madrParser } from '../search/madrParser'; /** @public */ export type AdrRouterOptions = { @@ -59,13 +60,20 @@ export async function createRouter( etag: cachedTree?.etag, }); const files = await treeGetResponse.files(); - const data = files.map(file => { - return { - type: 'file', - name: file.path.substring(file.path.lastIndexOf('/') + 1), - path: file.path, - }; - }); + const data = await Promise.all( + files + .map(async file => { + const fileContent = await file.content(); + const adrInfo = madrParser(fileContent.toString()); + return { + type: 'file', + name: file.path.substring(file.path.lastIndexOf('/') + 1), + path: file.path, + ...adrInfo, + }; + }) + .reverse(), + ); await cacheClient.set(urlToProcess, { data, diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md index 4ee9e2fd8e..7ed77717a1 100644 --- a/plugins/adr-common/CHANGELOG.md +++ b/plugins/adr-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-adr-common +## 0.2.6-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/integration@1.4.2 + - @backstage/plugin-search-common@1.2.1 + ## 0.2.6-next.0 ### Patch Changes diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json index a34cf5497c..7bae7294bb 100644 --- a/plugins/adr-common/package.json +++ b/plugins/adr-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-adr-common", "description": "Common functionalities for the adr plugin", - "version": "0.2.6-next.0", + "version": "0.2.6-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md index 6f9a6b4be6..cea1b59deb 100644 --- a/plugins/adr/CHANGELOG.md +++ b/plugins/adr/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-adr +## 0.4.0-next.2 + +### Minor Changes + +- ec34b535c0: The `AdrSearchResultListItem` component is now a search result extension. This means that when rendered as a child of components that render search extensions, the `result`, `rank`, and `highlight` properties are optional. See the [documentation](https://backstage.io/docs/features/search/how-to-guides#how-to-render-search-results-using-extensions) for more details. + +### Patch Changes + +- ec34b535c0: Support displaying an icon on `AdrSearchResultListItem` +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-adr-common@0.2.6-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + ## 0.3.1-next.1 ### Patch Changes diff --git a/plugins/adr/README.md b/plugins/adr/README.md index b9cc5d84ba..a669762dad 100644 --- a/plugins/adr/README.md +++ b/plugins/adr/README.md @@ -4,6 +4,8 @@ Welcome to the ADR plugin! This plugin allows you to browse ADRs associated with your entities as well as a way to discover ADRs across others entities via Backstage Search. Use this to learn from the past experience of other projects to guide your own architecture decisions. +![ADR tab](./docs/adr-tab.png) + ## Setup 1. Install this plugin: diff --git a/plugins/adr/api-report.md b/plugins/adr/api-report.md index d6480cd090..1d07edc4ce 100644 --- a/plugins/adr/api-report.md +++ b/plugins/adr/api-report.md @@ -11,6 +11,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstagePlugin } from '@backstage/core-plugin-api'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { isAdrAvailable } from '@backstage/plugin-adr-common'; +import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; @@ -51,6 +52,9 @@ export type AdrFileInfo = { type: string; path: string; name: string; + title?: string; + status?: string; + date?: string; }; // @public @@ -82,12 +86,18 @@ export type AdrReadResult = { }; // @public -export function AdrSearchResultListItem(props: { +export const AdrSearchResultListItem: ( + props: AdrSearchResultListItemProps, +) => JSX.Element | null; + +// @public (undocumented) +export type AdrSearchResultListItemProps = { lineClamp?: number; highlight?: ResultHighlight; + icon?: ReactNode; rank?: number; - result: AdrDocument; -}): JSX.Element; + result?: AdrDocument; +}; // @public export const EntityAdrContent: (props: { diff --git a/plugins/adr/docs/adr-tab.png b/plugins/adr/docs/adr-tab.png new file mode 100644 index 0000000000..d9dd728221 Binary files /dev/null and b/plugins/adr/docs/adr-tab.png differ diff --git a/plugins/adr/package.json b/plugins/adr/package.json index 9d915b7af7..085729f86f 100644 --- a/plugins/adr/package.json +++ b/plugins/adr/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-adr", - "version": "0.3.1-next.1", + "version": "0.4.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,6 +39,7 @@ "remark-gfm": "^3.0.1" }, "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", "react": "^16.13.1 || ^17.0.0", "react-router-dom": "6.0.0-beta.0 || ^6.3.0" }, diff --git a/plugins/adr/src/api/types.ts b/plugins/adr/src/api/types.ts index e4632e171a..0af35cc6df 100644 --- a/plugins/adr/src/api/types.ts +++ b/plugins/adr/src/api/types.ts @@ -30,6 +30,15 @@ export type AdrFileInfo = { /** The name of the ADR file. */ name: string; + + /** The title of the ADR. */ + title?: string; + + /** The status of the ADR. */ + status?: string; + + /** The date of the ADR. */ + date?: string; }; /** diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx index d1db286943..eb5b4aed21 100644 --- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx +++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx @@ -35,6 +35,7 @@ import { } from '@backstage/plugin-adr-common'; import { useEntity } from '@backstage/plugin-catalog-react'; import { + Chip, Grid, List, ListItem, @@ -67,7 +68,9 @@ export const EntityAdrContent = (props: { const classes = useStyles(); const { entity } = useEntity(); const rootLink = useRouteRef(rootRouteRef); - const [adrList, setAdrList] = useState([]); + const [adrList, setAdrList] = useState< + { name: string; title?: string; status?: string; date?: string }[] + >([]); const [searchParams, setSearchParams] = useSearchParams(); const scmIntegrations = useApi(scmIntegrationsApiRef); const adrApi = useApi(adrApiRef); @@ -79,10 +82,10 @@ export const EntityAdrContent = (props: { }, [entity, scmIntegrations]); const selectedAdr = - adrList.find(adr => adr === searchParams.get('record')) ?? ''; + adrList.find(adr => adr.name === searchParams.get('record'))?.name ?? ''; useEffect(() => { if (adrList.length && !selectedAdr) { - searchParams.set('record', adrList[0]); + searchParams.set('record', adrList[0].name); setSearchParams(searchParams, { replace: true }); } }); @@ -100,11 +103,22 @@ export const EntityAdrContent = (props: { ? filePathFilterFn(item.name) : madrFilePathFilter(item.name)), ) - .map(({ name }: { name: string }) => name); + .map(({ name, title, status, date }) => ({ name, title, status, date })); setAdrList(adrs); }, [filePathFilterFn, value]); + const getChipColor = (status: string) => { + switch (status) { + case 'accepted': + return 'primary'; + case 'rejected' || 'deprecated': + return 'secondary'; + default: + return 'default'; + } + }; + return ( @@ -133,13 +147,24 @@ export const EntityAdrContent = (props: { key={idx} button component={Link} - to={`${rootLink()}?record=${adr}`} - selected={selectedAdr === adr} + to={`${rootLink()}?record=${adr.name}`} + selected={selectedAdr === adr.name} > + {adr.status && ( + + )} ))} diff --git a/plugins/adr/src/index.ts b/plugins/adr/src/index.ts index 2ec76b0fd6..f24b3ac531 100644 --- a/plugins/adr/src/index.ts +++ b/plugins/adr/src/index.ts @@ -30,5 +30,5 @@ export type { } from './api'; export { isAdrAvailable } from '@backstage/plugin-adr-common'; export * from './components/AdrReader'; -export { adrPlugin, EntityAdrContent } from './plugin'; -export * from './search'; +export { adrPlugin, AdrSearchResultListItem, EntityAdrContent } from './plugin'; +export type { AdrSearchResultListItemProps } from './search'; diff --git a/plugins/adr/src/plugin.ts b/plugins/adr/src/plugin.ts index f23df826cd..05e4adec4a 100644 --- a/plugins/adr/src/plugin.ts +++ b/plugins/adr/src/plugin.ts @@ -21,7 +21,9 @@ import { createRoutableExtension, discoveryApiRef, } from '@backstage/core-plugin-api'; +import { createSearchResultListItemExtension } from '@backstage/plugin-search-react'; import { rootRouteRef } from './routes'; +import { AdrSearchResultListItemProps } from './search'; /** * The Backstage plugin that holds ADR specific components @@ -57,3 +59,21 @@ export const EntityAdrContent = adrPlugin.provide( mountPoint: rootRouteRef, }), ); + +/** + * React extension used to render results on Search page or modal + * + * @public + */ +export const AdrSearchResultListItem: ( + props: AdrSearchResultListItemProps, +) => JSX.Element | null = adrPlugin.provide( + createSearchResultListItemExtension({ + name: 'AdrSearchResultListItem', + component: () => + import('./search/AdrSearchResultListItem').then( + m => m.AdrSearchResultListItem, + ), + predicate: result => result.type === 'adr', + }), +); diff --git a/plugins/adr/src/search/AdrSearchResultListItem.tsx b/plugins/adr/src/search/AdrSearchResultListItem.tsx index 3a11b5cf9a..118333a439 100644 --- a/plugins/adr/src/search/AdrSearchResultListItem.tsx +++ b/plugins/adr/src/search/AdrSearchResultListItem.tsx @@ -14,19 +14,19 @@ * limitations under the License. */ -import React from 'react'; +import React, { ReactNode } from 'react'; import { Box, Chip, Divider, ListItem, + ListItemIcon, ListItemText, makeStyles, } from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import { parseEntityRef } from '@backstage/catalog-model'; import { Link } from '@backstage/core-components'; -import { useAnalytics } from '@backstage/core-plugin-api'; import { AdrDocument } from '@backstage/plugin-adr-common'; import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; import { ResultHighlight } from '@backstage/plugin-search-common'; @@ -43,35 +43,36 @@ const useStyles = makeStyles({ }, }); +/** + * @public + */ +export type AdrSearchResultListItemProps = { + lineClamp?: number; + highlight?: ResultHighlight; + icon?: ReactNode; + rank?: number; + result?: AdrDocument; +}; + /** * A component to display an ADR search result. * @public */ -export function AdrSearchResultListItem(props: { - lineClamp?: number; - highlight?: ResultHighlight; - rank?: number; - result: AdrDocument; -}) { - const { lineClamp = 5, highlight, rank, result } = props; +export function AdrSearchResultListItem(props: AdrSearchResultListItemProps) { + const { lineClamp = 5, highlight, icon, result } = props; const classes = useStyles(); - const analytics = useAnalytics(); - const handleClick = () => { - analytics.captureEvent('discover', result.title, { - attributes: { to: result.location }, - value: rank, - }); - }; + if (!result) return null; return ( <> + {icon && {icon}} + {highlight?.fields.title ? ( { { title: 'Link', field: 'dagUrl', - render: (row: Partial) => ( - - - - - - ), + render: (row: Partial) => + !row.dagUrl ? null : ( + + + + + + ), width: '5%', disableClick: true, hidden: hiddenColumns.some(field => field === 'dagUrl'), diff --git a/plugins/api-docs-module-protoc-gen-doc/README.md b/plugins/api-docs-module-protoc-gen-doc/README.md index 4d90cd74c2..a921fafc0b 100644 --- a/plugins/api-docs-module-protoc-gen-doc/README.md +++ b/plugins/api-docs-module-protoc-gen-doc/README.md @@ -6,8 +6,9 @@ This package contains ApiDefinitionWidgets for the following projects: ## Setup -``` -yarn add @backstage/plugin-api-docs-module-protoc-gen-doc +```sh +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-api-docs-module-protoc-gen-doc ``` ## Add the GrpcDocsApiWidget to your apis diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index b002e4c373..39796b3231 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-api-docs +## 0.9.0-next.2 + +### Minor Changes + +- 9ed53218a5: Adds a new tRPC API definition widget which users can utilize to integrate their tRPC API definitions into Backstage. + +### Patch Changes + +- 6856ab98f4: Updated dependency `@asyncapi/react-component` to `1.0.0-next.47`. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.8.15-next.1 ### Patch Changes diff --git a/plugins/api-docs/api-report.md b/plugins/api-docs/api-report.md index 0872d97355..a4f61872f2 100644 --- a/plugins/api-docs/api-report.md +++ b/plugins/api-docs/api-report.md @@ -173,4 +173,14 @@ export const ProvidedApisCard: (props: { export const ProvidingComponentsCard: (props: { variant?: InfoCardVariants; }) => JSX.Element; + +// @public (undocumented) +export const TrpcApiDefinitionWidget: ( + props: TrpcApiDefinitionWidgetProps, +) => JSX.Element; + +// @public (undocumented) +export type TrpcApiDefinitionWidgetProps = { + definition: string; +}; ``` diff --git a/plugins/api-docs/dev/index.tsx b/plugins/api-docs/dev/index.tsx index 9975e91e98..995aa9c2fb 100644 --- a/plugins/api-docs/dev/index.tsx +++ b/plugins/api-docs/dev/index.tsx @@ -32,6 +32,7 @@ import graphqlApiEntity from './graphql-example-api.yaml'; import invalidLanguageApiEntity from './invalid-language-example-api.yaml'; import openapiApiEntity from './openapi-example-api.yaml'; import otherApiEntity from './other-example-api.yaml'; +import trpcApiEntity from './trpc-example-api.yaml'; const mockEntities = [ openapiApiEntity, @@ -39,6 +40,7 @@ const mockEntities = [ graphqlApiEntity, invalidLanguageApiEntity, otherApiEntity, + trpcApiEntity, ] as unknown as Entity[]; createDevApp() @@ -137,4 +139,17 @@ createDevApp() ), }) + .addPage({ + title: 'tRPC', + element: ( + +
    + + + + + + + ), + }) .render(); diff --git a/plugins/api-docs/dev/trpc-example-api.yaml b/plugins/api-docs/dev/trpc-example-api.yaml new file mode 100644 index 0000000000..f5807537b3 --- /dev/null +++ b/plugins/api-docs/dev/trpc-example-api.yaml @@ -0,0 +1,25 @@ +apiVersion: backstage.io/v1alpha1 +kind: API +metadata: + name: hello-world-trpc + description: Hello World example for tRPC +spec: + type: trpc + lifecycle: experimental + owner: team-c + definition: | + import { z } from 'zod'; + import { publicProcedure, router } from '../trpc'; + + export const apiRouter = router({ + version: publicProcedure.query(() => { + return { version: '0.42.0' }; + }), + hello: publicProcedure + .input(z.object({ username: z.string().nullish() }).nullish()) + .query(({ input, ctx }) => { + return { + text: `hello ${input?.username ?? ctx.user?.name ?? 'world'}`, + }; + }), + }); diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index e6dd3d6e1f..ee3e88fe11 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-api-docs", "description": "A Backstage plugin that helps represent API entities in the frontend", - "version": "0.8.15-next.1", + "version": "0.9.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,7 +32,7 @@ "clean": "backstage-cli package clean" }, "dependencies": { - "@asyncapi/react-component": "1.0.0-next.44", + "@asyncapi/react-component": "1.0.0-next.47", "@backstage/catalog-model": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionWidget.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionWidget.tsx index 03a56eaa78..8d7d7a7729 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionWidget.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionWidget.tsx @@ -18,6 +18,7 @@ import { AsyncApiDefinitionWidget } from '../AsyncApiDefinitionWidget'; import { GraphQlDefinitionWidget } from '../GraphQlDefinitionWidget'; import { OpenApiDefinitionWidget } from '../OpenApiDefinitionWidget'; import { GrpcApiDefinitionWidget } from '../GrpcApiDefinitionWidget'; +import { TrpcApiDefinitionWidget } from '../TrpcDefinitionWidget'; /** @public */ export type ApiDefinitionWidget = { @@ -61,5 +62,12 @@ export function defaultDefinitionWidgets(): ApiDefinitionWidget[] { ), }, + { + type: 'trpc', + title: 'tRPC', + component: definition => ( + + ), + }, ]; } diff --git a/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.test.tsx b/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.test.tsx new file mode 100644 index 0000000000..d7e6951e2e --- /dev/null +++ b/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.test.tsx @@ -0,0 +1,32 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; +import { TrpcApiDefinitionWidget } from './TrpcApiDefinitionWidget'; + +describe('', () => { + it('renders plain text', async () => { + const { getAllByText } = await renderInTestApp( + , + ); + + expect( + getAllByText((_text, element) => element?.textContent === 'Hello World') + .length, + ).toBeGreaterThan(0); + }); +}); diff --git a/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.tsx b/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.tsx new file mode 100644 index 0000000000..af73335dbd --- /dev/null +++ b/plugins/api-docs/src/components/TrpcDefinitionWidget/TrpcApiDefinitionWidget.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { CodeSnippet } from '@backstage/core-components'; +import { useTheme } from '@material-ui/core/styles'; +import { BackstageTheme } from '@backstage/theme'; + +/** @public */ +export type TrpcApiDefinitionWidgetProps = { + definition: string; +}; + +/** @public */ +export const TrpcApiDefinitionWidget = ( + props: TrpcApiDefinitionWidgetProps, +) => { + const { definition } = props; + const theme = useTheme(); + return ( + + ); +}; diff --git a/plugins/api-docs/src/components/TrpcDefinitionWidget/index.ts b/plugins/api-docs/src/components/TrpcDefinitionWidget/index.ts new file mode 100644 index 0000000000..604adb06f3 --- /dev/null +++ b/plugins/api-docs/src/components/TrpcDefinitionWidget/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { + TrpcApiDefinitionWidget, + type TrpcApiDefinitionWidgetProps, +} from './TrpcApiDefinitionWidget'; diff --git a/plugins/api-docs/src/components/index.ts b/plugins/api-docs/src/components/index.ts index ea40076af4..7e7b206c54 100644 --- a/plugins/api-docs/src/components/index.ts +++ b/plugins/api-docs/src/components/index.ts @@ -22,3 +22,4 @@ export * from './ComponentsCards'; export * from './GraphQlDefinitionWidget'; export * from './OpenApiDefinitionWidget'; export * from './PlainApiDefinitionWidget'; +export * from './TrpcDefinitionWidget'; diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md index df9c9d8270..8903d504a9 100644 --- a/plugins/apollo-explorer/CHANGELOG.md +++ b/plugins/apollo-explorer/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-apollo-explorer +## 0.1.8-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.1.8-next.0 ### Patch Changes diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json index 713ad945ad..7917f5a417 100644 --- a/plugins/apollo-explorer/package.json +++ b/plugins/apollo-explorer/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-apollo-explorer", - "version": "0.1.8-next.0", + "version": "0.1.8-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index a8a9ca7edf..617d74217b 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-app-backend +## 0.3.42-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/config-loader@1.1.8 + - @backstage/types@1.0.2 + ## 0.3.42-next.1 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index d434749619..ff9cd8fe5f 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-app-backend", "description": "A Backstage backend plugin that serves the Backstage frontend app", - "version": "0.3.42-next.1", + "version": "0.3.42-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/app-backend/src/service/appPlugin.ts b/plugins/app-backend/src/service/appPlugin.ts index b12a8d6877..a29f8fd5a8 100644 --- a/plugins/app-backend/src/service/appPlugin.ts +++ b/plugins/app-backend/src/service/appPlugin.ts @@ -71,7 +71,7 @@ export type AppPluginOptions = { * @alpha */ export const appPlugin = createBackendPlugin((options: AppPluginOptions) => ({ - id: 'app', + pluginId: 'app', register(env) { env.registerInit({ deps: { diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md index a1bee43423..f76848cb94 100644 --- a/plugins/auth-backend/CHANGELOG.md +++ b/plugins/auth-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-auth-backend +## 0.17.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + ## 0.17.5-next.1 ### Patch Changes diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index 88cb33863e..63aa060875 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -7,7 +7,7 @@ import { BackstageIdentityResponse } from '@backstage/plugin-auth-node'; import { BackstageSignInResult } from '@backstage/plugin-auth-node'; -import { CacheClient } from '@backstage/backend-plugin-api'; +import { CacheService } from '@backstage/backend-plugin-api'; import { CatalogApi } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; import { Entity } from '@backstage/catalog-model'; @@ -132,6 +132,19 @@ export type BitbucketPassportProfile = Profile & { }; }; +// @public (undocumented) +export type BitbucketServerOAuthResult = { + fullProfile: Profile; + params: { + scope: string; + access_token?: string; + token_type?: string; + expires_in?: number; + }; + accessToken: string; + refreshToken?: string; +}; + // @public export class CatalogIdentityClient { constructor(options: { catalogApi: CatalogApi; tokenManager: TokenManager }); @@ -480,13 +493,30 @@ export const providers: Readonly<{ userIdMatchingUserEntityAnnotation(): SignInResolver; }>; }>; + bitbucketServer: Readonly<{ + create: ( + options?: + | { + authHandler?: AuthHandler | undefined; + signIn?: + | { + resolver: SignInResolver; + } + | undefined; + } + | undefined, + ) => AuthProviderFactory; + resolvers: Readonly<{ + emailMatchingUserEntityProfileEmail: () => SignInResolver; + }>; + }>; cfAccess: Readonly<{ create: (options: { authHandler?: AuthHandler | undefined; signIn: { resolver: SignInResolver; }; - cache?: CacheClient | undefined; + cache?: CacheService | undefined; }) => AuthProviderFactory; resolvers: Readonly<{ emailMatchingUserEntityProfileEmail: () => SignInResolver; diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index a95465dbad..7a855e62a8 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.17.5-next.1", + "version": "0.17.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/auth-backend/src/providers/bitbucketServer/index.ts b/plugins/auth-backend/src/providers/bitbucketServer/index.ts new file mode 100644 index 0000000000..cef12cd50d --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucketServer/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { bitbucketServer } from './provider'; +export type { BitbucketServerOAuthResult } from './provider'; diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts new file mode 100644 index 0000000000..fc48cdbe8b --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.test.ts @@ -0,0 +1,390 @@ +/* + * 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 * as helpers from '../../lib/passport/PassportStrategyHelper'; +import { makeProfileInfo } from '../../lib/passport'; +import { AuthResolverContext } from '../types'; +import { + bitbucketServer, + BitbucketServerAuthProvider, + BitbucketServerOAuthResult, +} from './provider'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; + +jest.mock('../../lib/passport/PassportStrategyHelper', () => { + return { + ...jest.requireActual('../../lib/passport/PassportStrategyHelper'), + executeFrameHandlerStrategy: jest.fn(), + executeRefreshTokenStrategy: jest.fn(), + executeFetchUserProfileStrategy: jest.fn(), + }; +}); + +const mockFrameHandler = jest.spyOn( + helpers, + 'executeFrameHandlerStrategy', +) as unknown as jest.MockedFunction< + () => Promise<{ + result: BitbucketServerOAuthResult; + privateInfo: { refreshToken?: string }; + }> +>; + +const passportProfile = { + id: '123', + username: 'john.doe', + provider: 'bitubcketServer', + displayName: 'John Doe', + emails: [{ value: 'john@doe.com' }], + photos: [{ value: 'https://bitbucket.org/user/123/avatar' }], +}; + +const mockHost = 'bitbucket.org'; +const mockBaseUrl = `https://${mockHost}`; + +const whoAmIHandler = (options?: { fail?: boolean; value?: string }) => + rest.get( + `${mockBaseUrl}/plugins/servlet/applinks/whoami`, + (_req, res, ctx) => { + if (options?.fail) { + res.networkError('error'); + } + return res( + ctx.status(200), + ctx.set('X-Ausername', options?.value ?? passportProfile.username), + ); + }, + ); + +const getUserHandler = (options?: { + fail?: boolean; + status?: number; + avatarUrl?: string; + noDisplayName?: boolean; + noUserName?: boolean; +}) => + rest.get( + `${mockBaseUrl}/rest/api/latest/users/${passportProfile.username}`, + (_req, res, ctx) => { + if (options?.fail) { + res.networkError('error'); + } + return res( + ctx.status(options?.status ?? 200), + ctx.json({ + name: options?.noUserName ? undefined : 'john.doe', + emailAddress: 'john@doe.com', + id: 123, + displayName: options?.noDisplayName ? undefined : 'John Doe', + active: true, + slug: 'john.doe', + type: 'NORMAL', + links: { + self: [ + { + href: 'https://bitbucket.org/users/john.doe', + }, + ], + }, + avatarUrl: options?.avatarUrl ?? '/user/123/avatar', + }), + ); + }, + ); + +describe('BitbucketServerAuthProvider', () => { + const provider = new BitbucketServerAuthProvider({ + resolverContext: { + signInWithCatalogUser: jest.fn(info => { + return { + token: `token-for-user:${info.filter['spec.profile.email']}`, + }; + }), + } as unknown as AuthResolverContext, + signInResolver: + bitbucketServer.resolvers.emailMatchingUserEntityProfileEmail(), + authHandler: async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }), + callbackUrl: 'mock', + clientId: 'mock', + clientSecret: 'mock', + host: mockHost, + authorizationUrl: 'mock', + tokenUrl: 'mock', + }); + + describe('when transforming to type OAuthResponse', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + it('should map to a valid response', async () => { + server.use(whoAmIHandler(), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + + const expected = { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'John Doe', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }; + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); + }); + + it('should throw if whoami fails', async () => { + server.use(whoAmIHandler({ fail: true }), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + `Failed to retrieve the username of the logged in user`, + ); + }); + + it('should throw if whoami returns an invalid response', async () => { + server.use(whoAmIHandler({ value: '' }), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + `Failed to retrieve the username of the logged in user`, + ); + }); + + it('should throw if get user fails', async () => { + server.use(whoAmIHandler(), getUserHandler({ fail: true })); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + `Failed to retrieve the user '${passportProfile.username}'`, + ); + }); + + it('should throw if get user is not ok', async () => { + server.use(whoAmIHandler(), getUserHandler({ status: 500 })); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + + await expect(provider.handler({} as any)).rejects.toThrow( + `Failed to retrieve the user '${passportProfile.username}'`, + ); + }); + + it('should not set an avatar url if not given', async () => { + server.use(whoAmIHandler(), getUserHandler({ avatarUrl: '' })); + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + + const expected = { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'John Doe', + }, + }; + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); + }); + + it('should fallback to the username if no displayName is given', async () => { + server.use(whoAmIHandler(), getUserHandler({ noDisplayName: true })); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + + const expected = { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'john.doe', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }; + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); + }); + + it('should fallback to the user id if no name is given', async () => { + server.use( + whoAmIHandler(), + getUserHandler({ noDisplayName: true, noUserName: true }), + ); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + + const expected = { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: '123', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }; + + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: {}, + }); + const { response } = await provider.handler({} as any); + expect(response).toEqual(expected); + }); + }); + + describe('when authenticating', () => { + const server = setupServer(); + setupRequestMockHandlers(server); + + it('should forward the refresh token', async () => { + server.use(whoAmIHandler(), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: { refreshToken: 'refresh-token' }, + }); + + const response = await provider.handler({} as any); + + const expected = { + response: { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'John Doe', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }, + refreshToken: 'refresh-token', + }; + + expect(response).toEqual(expected); + }); + + it('should forward a new refresh token on refresh', async () => { + server.use(whoAmIHandler(), getUserHandler()); + + const accessToken = '19xasczxcm9n7gacn9jdgm19me'; + const params = { scope: 'REPO_READ' }; + const mockRefreshToken = jest.spyOn( + helpers, + 'executeRefreshTokenStrategy', + ) as unknown as jest.MockedFunction<() => Promise<{}>>; + mockRefreshToken.mockResolvedValueOnce({ + accessToken, + refreshToken: 'dont-forget-to-send-refresh', + params, + }); + mockFrameHandler.mockResolvedValueOnce({ + result: { fullProfile: passportProfile, accessToken, params }, + privateInfo: { refreshToken: 'refresh-token' }, + }); + + const expected = { + response: { + backstageIdentity: { + token: 'token-for-user:john@doe.com', + }, + providerInfo: { + accessToken: '19xasczxcm9n7gacn9jdgm19me', + scope: 'REPO_READ', + }, + profile: { + email: 'john@doe.com', + displayName: 'John Doe', + picture: 'https://bitbucket.org/user/123/avatar', + }, + }, + refreshToken: 'dont-forget-to-send-refresh', + }; + const response = await provider.refresh({ scope: 'REPO_WRITE' } as any); + + expect(response).toEqual(expected); + }); + }); +}); diff --git a/plugins/auth-backend/src/providers/bitbucketServer/provider.ts b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts new file mode 100644 index 0000000000..fb8dd3d5b8 --- /dev/null +++ b/plugins/auth-backend/src/providers/bitbucketServer/provider.ts @@ -0,0 +1,305 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + encodeState, + OAuthAdapter, + OAuthEnvironmentHandler, + OAuthHandlers, + OAuthProviderOptions, + OAuthRefreshRequest, + OAuthResponse, + OAuthStartRequest, +} from '../../lib/oauth'; +import { Strategy as OAuth2Strategy, VerifyCallback } from 'passport-oauth2'; +import { + executeFetchUserProfileStrategy, + executeFrameHandlerStrategy, + executeRedirectStrategy, + executeRefreshTokenStrategy, + makeProfileInfo, +} from '../../lib/passport'; +import { + AuthHandler, + AuthResolverContext, + OAuthStartResponse, + SignInResolver, +} from '../types'; +import express from 'express'; +import { createAuthProviderIntegration } from '../createAuthProviderIntegration'; +import { Profile as PassportProfile } from 'passport'; +import { commonByEmailResolver } from '../resolvers'; +import fetch from 'node-fetch'; + +type PrivateInfo = { + refreshToken: string; +}; + +/** @public */ +export type BitbucketServerOAuthResult = { + fullProfile: PassportProfile; + params: { + scope: string; + access_token?: string; + token_type?: string; + expires_in?: number; + }; + accessToken: string; + refreshToken?: string; +}; + +export type BitbucketServerAuthProviderOptions = OAuthProviderOptions & { + host: string; + authorizationUrl: string; + tokenUrl: string; + authHandler: AuthHandler; + signInResolver?: SignInResolver; + resolverContext: AuthResolverContext; +}; + +export class BitbucketServerAuthProvider implements OAuthHandlers { + private readonly signInResolver?: SignInResolver; + private readonly authHandler: AuthHandler; + private readonly resolverContext: AuthResolverContext; + private readonly strategy: OAuth2Strategy; + private readonly host: string; + + constructor(options: BitbucketServerAuthProviderOptions) { + this.signInResolver = options.signInResolver; + this.authHandler = options.authHandler; + this.resolverContext = options.resolverContext; + this.strategy = new OAuth2Strategy( + { + authorizationURL: options.authorizationUrl, + tokenURL: options.tokenUrl, + clientID: options.clientId, + clientSecret: options.clientSecret, + callbackURL: options.callbackUrl, + }, + ( + accessToken: string, + refreshToken: string, + params: any, + fullProfile: PassportProfile, + done: VerifyCallback, + ) => { + done(undefined, { fullProfile, params, accessToken }, { refreshToken }); + }, + ); + this.host = options.host; + } + + async start(req: OAuthStartRequest): Promise { + return await executeRedirectStrategy(req, this.strategy, { + accessType: 'offline', + prompt: 'consent', + scope: req.scope, + state: encodeState(req.state), + }); + } + + async handler( + req: express.Request, + ): Promise<{ response: OAuthResponse; refreshToken?: string }> { + const { result, privateInfo } = await executeFrameHandlerStrategy< + BitbucketServerOAuthResult, + PrivateInfo + >(req, this.strategy); + + return { + response: await this.handleResult(result), + refreshToken: privateInfo.refreshToken, + }; + } + + async refresh( + req: OAuthRefreshRequest, + ): Promise<{ response: OAuthResponse; refreshToken?: string }> { + const { accessToken, refreshToken, params } = + await executeRefreshTokenStrategy( + this.strategy, + req.refreshToken, + req.scope, + ); + const fullProfile = await executeFetchUserProfileStrategy( + this.strategy, + accessToken, + ); + return { + response: await this.handleResult({ + fullProfile, + params, + accessToken, + }), + refreshToken, + }; + } + + private async handleResult( + result: BitbucketServerOAuthResult, + ): Promise { + // The OAuth2 strategy does not return a user profile -> let's fetch it before calling the auth handler + result.fullProfile = await this.fetchProfile(result); + const { profile } = await this.authHandler(result, this.resolverContext); + + let backstageIdentity = undefined; + if (this.signInResolver) { + backstageIdentity = await this.signInResolver( + { result, profile }, + this.resolverContext, + ); + } + + return { + providerInfo: { + accessToken: result.accessToken, + scope: result.params.scope, + expiresInSeconds: result.params.expires_in, + }, + profile, + backstageIdentity, + }; + } + + private async fetchProfile( + result: BitbucketServerOAuthResult, + ): Promise { + // Get current user name + let whoAmIResponse; + try { + whoAmIResponse = await fetch( + `https://${this.host}/plugins/servlet/applinks/whoami`, + { + headers: { + Authorization: `Bearer ${result.accessToken}`, + }, + }, + ); + } catch (e) { + throw new Error(`Failed to retrieve the username of the logged in user`); + } + + // A response.ok check here would be worthless as the Bitbucket API always returns 200 OK for this call + const username = whoAmIResponse.headers.get('X-Ausername'); + if (!username) { + throw new Error(`Failed to retrieve the username of the logged in user`); + } + + let userResponse; + try { + userResponse = await fetch( + `https://${this.host}/rest/api/latest/users/${username}?avatarSize=256`, + { + headers: { + Authorization: `Bearer ${result.accessToken}`, + }, + }, + ); + } catch (e) { + throw new Error(`Failed to retrieve the user '${username}'`); + } + + if (!userResponse.ok) { + throw new Error(`Failed to retrieve the user '${username}'`); + } + + const user = (await userResponse.json()) as any; + + const passportProfile = { + provider: 'bitbucketServer', + id: user.id.toString(), + displayName: user.displayName, + username: user.name, + emails: [ + { + value: user.emailAddress, + }, + ], + } as PassportProfile; + + if (user.avatarUrl) { + passportProfile.photos = [ + { value: `https://${this.host}${user.avatarUrl}` }, + ]; + } + + return passportProfile; + } +} + +export const bitbucketServer = createAuthProviderIntegration({ + create(options?: { + /** + * The profile transformation function used to verify and convert the auth response + * into the profile that will be presented to the user. + */ + authHandler?: AuthHandler; + + /** + * Configure sign-in for this provider, without it the provider can not be used to sign users in. + */ + signIn?: { + /** + * Maps an auth result to a Backstage identity for the user. + */ + resolver: SignInResolver; + }; + }) { + return ({ providerId, globalConfig, config, resolverContext }) => + OAuthEnvironmentHandler.mapConfig(config, envConfig => { + const clientId = envConfig.getString('clientId'); + const clientSecret = envConfig.getString('clientSecret'); + const host = envConfig.getString('host'); + const customCallbackUrl = envConfig.getOptionalString('callbackUrl'); + const callbackUrl = + customCallbackUrl || + `${globalConfig.baseUrl}/${providerId}/handler/frame`; + const authorizationUrl = `https://${host}/rest/oauth2/latest/authorize`; + const tokenUrl = `https://${host}/rest/oauth2/latest/token`; + + const authHandler: AuthHandler = + options?.authHandler + ? options.authHandler + : async ({ fullProfile }) => ({ + profile: makeProfileInfo(fullProfile), + }); + + const provider = new BitbucketServerAuthProvider({ + callbackUrl, + clientId, + clientSecret, + host, + authorizationUrl, + tokenUrl, + authHandler, + signInResolver: options?.signIn?.resolver, + resolverContext, + }); + + return OAuthAdapter.fromConfig(globalConfig, provider, { + providerId, + callbackUrl, + }); + }); + }, + resolvers: { + /** + * Looks up the user by matching their email to the entity email. + */ + emailMatchingUserEntityProfileEmail: + (): SignInResolver => commonByEmailResolver, + }, +}); diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts index 6653bc38cb..1abca4bdf4 100644 --- a/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts +++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.test.ts @@ -83,6 +83,7 @@ const mockCacheClient = { get: jest.fn(), set: jest.fn(), delete: jest.fn(), + withOptions: jest.fn(), }; jest.mock('jose'); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 67d3a62990..a922065fd8 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -19,6 +19,7 @@ export type { BitbucketOAuthResult, BitbucketPassportProfile, } from './bitbucket'; +export type { BitbucketServerOAuthResult } from './bitbucketServer'; export type { CloudflareAccessClaims, CloudflareAccessGroup, diff --git a/plugins/auth-backend/src/providers/providers.ts b/plugins/auth-backend/src/providers/providers.ts index b2795c25f0..aa630b7481 100644 --- a/plugins/auth-backend/src/providers/providers.ts +++ b/plugins/auth-backend/src/providers/providers.ts @@ -31,6 +31,7 @@ import { okta } from './okta'; import { onelogin } from './onelogin'; import { saml } from './saml'; import { AuthProviderFactory } from './types'; +import { bitbucketServer } from './bitbucketServer'; /** * All built-in auth provider integrations. @@ -42,6 +43,7 @@ export const providers = Object.freeze({ auth0, awsAlb, bitbucket, + bitbucketServer, cfAccess, gcpIap, github, @@ -76,5 +78,6 @@ export const defaultAuthProviderFactories: { onelogin: onelogin.create(), awsalb: awsAlb.create(), bitbucket: bitbucket.create(), + bitbucketServer: bitbucketServer.create(), atlassian: atlassian.create(), }; diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md index d569650cd5..7f9e0f8a33 100644 --- a/plugins/auth-node/CHANGELOG.md +++ b/plugins/auth-node/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-auth-node +## 0.2.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + ## 0.2.11-next.1 ### Patch Changes diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json index 5d0748cdee..fe7adb300d 100644 --- a/plugins/auth-node/package.json +++ b/plugins/auth-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-auth-node", - "version": "0.2.11-next.1", + "version": "0.2.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md index 2c7f39f8b3..7488628b80 100644 --- a/plugins/azure-devops-backend/CHANGELOG.md +++ b/plugins/azure-devops-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-azure-devops-backend +## 0.3.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-devops-common@0.3.0 + ## 0.3.21-next.1 ### Patch Changes diff --git a/plugins/azure-devops-backend/README.md b/plugins/azure-devops-backend/README.md index 301ae5b2b9..ba2a847c0a 100644 --- a/plugins/azure-devops-backend/README.md +++ b/plugins/azure-devops-backend/README.md @@ -30,9 +30,8 @@ Here's how to get the backend up and running: 1. First we need to add the `@backstage/plugin-azure-devops-backend` package to your backend: ```sh - # From the Backstage root directory - cd packages/backend - yarn add @backstage/plugin-azure-devops-backend + # From your Backstage root directory + yarn add --cwd packages/backend @backstage/plugin-azure-devops-backend ``` 2. Then we will create a new file named `packages/backend/src/plugins/azure-devops.ts`, and add the diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json index c62602a8f5..2103acf8fc 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.3.21-next.1", + "version": "0.3.21-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md index e6ab23c37c..cb4be6f908 100644 --- a/plugins/azure-devops/CHANGELOG.md +++ b/plugins/azure-devops/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-azure-devops +## 0.2.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-devops-common@0.3.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.6-next.1 ### Patch Changes diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json index 71ff024078..bfb09aff1d 100644 --- a/plugins/azure-devops/package.json +++ b/plugins/azure-devops/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-devops", - "version": "0.2.6-next.1", + "version": "0.2.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-sites-backend/CHANGELOG.md b/plugins/azure-sites-backend/CHANGELOG.md index 30e29db0ae..e3680abddb 100644 --- a/plugins/azure-sites-backend/CHANGELOG.md +++ b/plugins/azure-sites-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-azure-sites-backend +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-azure-sites-common@0.1.0 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md index 5f19267499..0d44b9a4e4 100644 --- a/plugins/azure-sites-backend/README.md +++ b/plugins/azure-sites-backend/README.md @@ -36,7 +36,7 @@ Here's how to get the backend plugin up and running: 1. First we need to add the `@backstage/plugin-azure-sites-backend` package to your backend: ```sh - # From the Backstage root directory + # From your Backstage root directory yarn add --cwd packages/backend @backstage/plugin-azure-sites-backend ``` diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json index fbc98b0d5e..c053435161 100644 --- a/plugins/azure-sites-backend/package.json +++ b/plugins/azure-sites-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites-backend", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md index 32c55b424c..8c44d24ff3 100644 --- a/plugins/azure-sites/CHANGELOG.md +++ b/plugins/azure-sites/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-azure-sites +## 0.1.4-next.2 + +### Patch Changes + +- 98a00a09d5: Fixed `AzureSites` plugin start and stop issue #15904 + Fixed `AzureSites` UI load issue #15907 +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-azure-sites-common@0.1.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/azure-sites/README.md b/plugins/azure-sites/README.md index f240dab140..57bf84ccc5 100644 --- a/plugins/azure-sites/README.md +++ b/plugins/azure-sites/README.md @@ -44,10 +44,11 @@ azure.com/microsoft-web-sites: func-testapp ### Install the component -1. Install the plugin in the `packages/app` directory +1. Install the plugin ```sh -yarn add @backstage/plugin-azure-sites +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-azure-sites ``` 2. Add widget component to your Backstage instance: diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json index a2032f9292..3cfe1ea9c3 100644 --- a/plugins/azure-sites/package.json +++ b/plugins/azure-sites/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-azure-sites", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts b/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts index fc622e786b..b66a3cbb3f 100644 --- a/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts +++ b/plugins/azure-sites/src/api/AzureSitesApiBackendClient.ts @@ -35,7 +35,7 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { } async stop(request: AzureSiteStartStopRequest): Promise { - const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/${ + const url = `${await this.discoveryApi.getBaseUrl('azure-sites')}/${ request.subscription }/${request.resourceGroup}/${request.name}/stop`; const { token: accessToken } = await this.identityApi.getCredentials(); @@ -48,7 +48,7 @@ export class AzureSitesApiBackendClient implements AzureSitesApi { }); } async start(request: AzureSiteStartStopRequest): Promise { - const url = `${await this.discoveryApi.getBaseUrl('azure-functions')}/${ + const url = `${await this.discoveryApi.getBaseUrl('azure-sites')}/${ request.subscription }/${request.resourceGroup}/${request.name}/start`; const { token: accessToken } = await this.identityApi.getCredentials(); diff --git a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx index a25d186a48..5b67fe844a 100644 --- a/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx +++ b/plugins/azure-sites/src/components/AzureSitesOverviewTableComponent/AzureSitesOverviewTable.tsx @@ -73,9 +73,11 @@ const Kind = ({ value }: { value: Kinds }) => { app: , functionapp: , }; + const kindArray = value.split(','); + const iconValue: Kinds = kindArray.includes('app') ? 'app' : 'functionapp'; return ( - {iconMap[value]} + {iconMap[iconValue]} ); }; @@ -162,9 +164,8 @@ const ActionButtons = ({ )} diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md index c3ed18ac63..ca5bbb859d 100644 --- a/plugins/badges-backend/CHANGELOG.md +++ b/plugins/badges-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-badges-backend +## 0.1.36-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + ## 0.1.36-next.1 ### Patch Changes diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md index 0ecf41ee85..8ca4b561a2 100644 --- a/plugins/badges-backend/api-report.md +++ b/plugins/badges-backend/api-report.md @@ -79,7 +79,7 @@ export type BadgeSpec = { }; // @public (undocumented) -export type BadgeStyle = typeof BADGE_STYLES[number]; +export type BadgeStyle = (typeof BADGE_STYLES)[number]; // @public (undocumented) export const createDefaultBadgeFactories: () => BadgeFactories; diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json index 33e945bfd0..25beed0238 100644 --- a/plugins/badges-backend/package.json +++ b/plugins/badges-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges-backend", "description": "A Backstage backend plugin that generates README badges for your entities", - "version": "0.1.36-next.1", + "version": "0.1.36-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/badges-backend/src/types.ts b/plugins/badges-backend/src/types.ts index eec7063f88..0d778289f9 100644 --- a/plugins/badges-backend/src/types.ts +++ b/plugins/badges-backend/src/types.ts @@ -27,7 +27,7 @@ export const BADGE_STYLES = [ ] as const; /** @public */ -export type BadgeStyle = typeof BADGE_STYLES[number]; +export type BadgeStyle = (typeof BADGE_STYLES)[number]; /** @public */ export interface Badge { diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md index c58757b38d..cefd2f8ccd 100644 --- a/plugins/badges/CHANGELOG.md +++ b/plugins/badges/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-badges +## 0.2.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.39-next.1 ### Patch Changes diff --git a/plugins/badges/package.json b/plugins/badges/package.json index 8e6eb3c88d..4c8edbd09f 100644 --- a/plugins/badges/package.json +++ b/plugins/badges/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-badges", "description": "A Backstage plugin that generates README badges for your entities", - "version": "0.2.39-next.1", + "version": "0.2.39-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md index 6fc165aebc..5a7107312e 100644 --- a/plugins/bazaar-backend/CHANGELOG.md +++ b/plugins/bazaar-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-bazaar-backend +## 0.2.5-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + ## 0.2.5-next.1 ### Patch Changes diff --git a/plugins/bazaar-backend/api-report.md b/plugins/bazaar-backend/api-report.md index 1ee889c04a..6281530851 100644 --- a/plugins/bazaar-backend/api-report.md +++ b/plugins/bazaar-backend/api-report.md @@ -3,12 +3,16 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; import { PluginDatabaseManager } from '@backstage/backend-common'; +// @alpha +export const bazaarPlugin: () => BackendFeature; + // @public (undocumented) export function createRouter(options: RouterOptions): Promise; diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json index 166aca7c44..5fba3fcbb3 100644 --- a/plugins/bazaar-backend/package.json +++ b/plugins/bazaar-backend/package.json @@ -1,20 +1,21 @@ { "name": "@backstage/plugin-bazaar-backend", - "version": "0.2.5-next.1", + "version": "0.2.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "backend-plugin" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -23,7 +24,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", @@ -35,10 +36,12 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^" }, "files": [ "dist", - "migrations/**/*.{js,d.ts}" + "migrations/**/*.{js,d.ts}", + "alpha" ] } diff --git a/plugins/bazaar-backend/src/index.ts b/plugins/bazaar-backend/src/index.ts index ca73cb27ba..dd0cdbb750 100644 --- a/plugins/bazaar-backend/src/index.ts +++ b/plugins/bazaar-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export { bazaarPlugin } from './plugin'; diff --git a/plugins/bazaar-backend/src/plugin.ts b/plugins/bazaar-backend/src/plugin.ts new file mode 100644 index 0000000000..62622717c8 --- /dev/null +++ b/plugins/bazaar-backend/src/plugin.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The Bazaar backend plugin. + * + * @alpha + */ +export const bazaarPlugin = createBackendPlugin({ + pluginId: 'bazaar', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + database: coreServices.database, + identity: coreServices.identity, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ database, config, identity, logger, httpRouter }) { + httpRouter.use( + await createRouter({ + database, + config, + identity, + logger: loggerToWinstonLogger(logger), + }), + ); + }, + }); + }, +}); diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md index a24a4164ce..d3b9cc6a71 100644 --- a/plugins/bazaar/CHANGELOG.md +++ b/plugins/bazaar/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-bazaar +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/cli@0.22.2-next.1 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json index d4d88176d8..7558aaf2ec 100644 --- a/plugins/bazaar/package.json +++ b/plugins/bazaar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-bazaar", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/bitbucket-cloud-common/api-report.md b/plugins/bitbucket-cloud-common/api-report.md index 5182579a4b..95e6f36836 100644 --- a/plugins/bitbucket-cloud-common/api-report.md +++ b/plugins/bitbucket-cloud-common/api-report.md @@ -132,7 +132,7 @@ export namespace Models { readonly Plaintext: 'plaintext'; }; export type BaseCommitSummaryMarkupEnum = - typeof BaseCommitSummaryMarkupEnum[keyof typeof BaseCommitSummaryMarkupEnum]; + (typeof BaseCommitSummaryMarkupEnum)[keyof typeof BaseCommitSummaryMarkupEnum]; export interface Branch { default_merge_strategy?: string; // (undocumented) @@ -150,7 +150,7 @@ export namespace Models { readonly FastForward: 'fast_forward'; }; export type BranchMergeStrategiesEnum = - typeof BranchMergeStrategiesEnum[keyof typeof BranchMergeStrategiesEnum]; + (typeof BranchMergeStrategiesEnum)[keyof typeof BranchMergeStrategiesEnum]; export interface Commit extends BaseCommit { // (undocumented) participants?: Array; @@ -179,7 +179,7 @@ export namespace Models { }; // (undocumented) export type CommitFileAttributesEnum = - typeof CommitFileAttributesEnum[keyof typeof CommitFileAttributesEnum]; + (typeof CommitFileAttributesEnum)[keyof typeof CommitFileAttributesEnum]; export interface Link { // (undocumented) href?: string; @@ -221,7 +221,7 @@ export namespace Models { }; // (undocumented) export type ParticipantRoleEnum = - typeof ParticipantRoleEnum[keyof typeof ParticipantRoleEnum]; + (typeof ParticipantRoleEnum)[keyof typeof ParticipantRoleEnum]; const // (undocumented) ParticipantStateEnum: { readonly Approved: 'approved'; @@ -230,7 +230,7 @@ export namespace Models { }; // (undocumented) export type ParticipantStateEnum = - typeof ParticipantStateEnum[keyof typeof ParticipantStateEnum]; + (typeof ParticipantStateEnum)[keyof typeof ParticipantStateEnum]; export interface Project extends ModelObject { // (undocumented) created_on?: string; @@ -306,7 +306,7 @@ export namespace Models { readonly NoForks: 'no_forks'; }; export type RepositoryForkPolicyEnum = - typeof RepositoryForkPolicyEnum[keyof typeof RepositoryForkPolicyEnum]; + (typeof RepositoryForkPolicyEnum)[keyof typeof RepositoryForkPolicyEnum]; const // (undocumented) RepositoryScmEnum: { readonly Git: 'git'; @@ -336,7 +336,7 @@ export namespace Models { } // (undocumented) export type RepositoryScmEnum = - typeof RepositoryScmEnum[keyof typeof RepositoryScmEnum]; + (typeof RepositoryScmEnum)[keyof typeof RepositoryScmEnum]; // (undocumented) export interface SearchCodeSearchResult { // (undocumented) diff --git a/plugins/bitbucket-cloud-common/src/models/index.ts b/plugins/bitbucket-cloud-common/src/models/index.ts index 9d25bbb873..d868e2926c 100644 --- a/plugins/bitbucket-cloud-common/src/models/index.ts +++ b/plugins/bitbucket-cloud-common/src/models/index.ts @@ -107,7 +107,7 @@ export namespace Models { * @public */ export type BaseCommitSummaryMarkupEnum = - typeof BaseCommitSummaryMarkupEnum[keyof typeof BaseCommitSummaryMarkupEnum]; + (typeof BaseCommitSummaryMarkupEnum)[keyof typeof BaseCommitSummaryMarkupEnum]; /** * A branch object, representing a branch in a repository. @@ -146,7 +146,7 @@ export namespace Models { * @public */ export type BranchMergeStrategiesEnum = - typeof BranchMergeStrategiesEnum[keyof typeof BranchMergeStrategiesEnum]; + (typeof BranchMergeStrategiesEnum)[keyof typeof BranchMergeStrategiesEnum]; /** * A repository commit object. @@ -191,7 +191,7 @@ export namespace Models { * @public */ export type CommitFileAttributesEnum = - typeof CommitFileAttributesEnum[keyof typeof CommitFileAttributesEnum]; + (typeof CommitFileAttributesEnum)[keyof typeof CommitFileAttributesEnum]; /** * A link to a resource related to this object. @@ -280,7 +280,7 @@ export namespace Models { * @public */ export type ParticipantRoleEnum = - typeof ParticipantRoleEnum[keyof typeof ParticipantRoleEnum]; + (typeof ParticipantRoleEnum)[keyof typeof ParticipantRoleEnum]; /** * @public @@ -295,7 +295,7 @@ export namespace Models { * @public */ export type ParticipantStateEnum = - typeof ParticipantStateEnum[keyof typeof ParticipantStateEnum]; + (typeof ParticipantStateEnum)[keyof typeof ParticipantStateEnum]; /** * A Bitbucket project. @@ -423,7 +423,7 @@ export namespace Models { * @public */ export type RepositoryForkPolicyEnum = - typeof RepositoryForkPolicyEnum[keyof typeof RepositoryForkPolicyEnum]; + (typeof RepositoryForkPolicyEnum)[keyof typeof RepositoryForkPolicyEnum]; /** * @public @@ -436,7 +436,7 @@ export namespace Models { * @public */ export type RepositoryScmEnum = - typeof RepositoryScmEnum[keyof typeof RepositoryScmEnum]; + (typeof RepositoryScmEnum)[keyof typeof RepositoryScmEnum]; /** * @public diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md index be81c9f7af..65cef85765 100644 --- a/plugins/bitrise/CHANGELOG.md +++ b/plugins/bitrise/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-bitrise +## 0.1.42-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.42-next.1 ### Patch Changes diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json index 3f064430cc..13cc29fd06 100644 --- a/plugins/bitrise/package.json +++ b/plugins/bitrise/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-bitrise", "description": "A Backstage plugin that integrates towards Bitrise", - "version": "0.1.42-next.1", + "version": "0.1.42-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md index bea397e7b5..d0eeb5702c 100644 --- a/plugins/catalog-backend-module-aws/CHANGELOG.md +++ b/plugins/catalog-backend-module-aws/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-aws +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md index 63b85f223b..1725692cb1 100644 --- a/plugins/catalog-backend-module-aws/api-report.md +++ b/plugins/catalog-backend-module-aws/api-report.md @@ -11,7 +11,7 @@ import { Config } from '@backstage/config'; import { Credentials } from 'aws-sdk'; import { EntityProvider } from '@backstage/plugin-catalog-backend'; import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; -import { LocationSpec } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { Logger } from 'winston'; import { PluginTaskScheduler } from '@backstage/backend-tasks'; import { TaskRunner } from '@backstage/backend-tasks'; diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json index 083ed52fab..0c9d9af8bb 100644 --- a/plugins/catalog-backend-module-aws/package.json +++ b/plugins/catalog-backend-module-aws/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-aws", "description": "A Backstage catalog backend module that helps integrate towards AWS", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts index 87b7f2c487..ecf3b1a961 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts @@ -20,10 +20,12 @@ import aws from 'aws-sdk'; describe('AwsEKSClusterProcessor', () => { AWSMock.setSDKInstance(aws); + describe('readLocation', () => { const processor = new (AwsEKSClusterProcessor as any)({}); const location = { type: 'aws-eks', target: '957140518395/us-west-2' }; const emit = jest.fn(); + it('generates cluster correctly', async () => { const clusters: aws.EKS.Types.ListClustersResponse = { clusters: ['backstage-test'], @@ -40,11 +42,12 @@ describe('AwsEKSClusterProcessor', () => { }, }, }; - AWSMock.mock('EKS', 'listClusters', clusters); + AWSMock.mock('EKS', 'listClusters', clusters); AWSMock.mock('EKS', 'describeCluster', cluster); await processor.readLocation(location, false, emit); + expect(emit).toHaveBeenCalledWith({ type: 'entity', location, diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts index c34b247f4d..b6418ade03 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { CatalogProcessor, CatalogProcessorEmit, - LocationSpec, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { ANNOTATION_KUBERNETES_API_SERVER, ANNOTATION_KUBERNETES_API_SERVER_CA, ANNOTATION_KUBERNETES_AUTH_PROVIDER, -} from '@backstage/catalog-model'; +} from '@backstage/plugin-kubernetes-common'; import { Credentials, EKS } from 'aws-sdk'; import { AWSCredentialFactory } from '../types'; diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts index 1018e49a41..ecc42e69ea 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts @@ -19,9 +19,9 @@ import { Config } from '@backstage/config'; import { CatalogProcessor, CatalogProcessorEmit, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import AWS, { Credentials, Organizations } from 'aws-sdk'; import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations'; import { Logger } from 'winston'; diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts index 84f2289c94..072c3d8384 100644 --- a/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-aws/src/processors/AwsS3DiscoveryProcessor.ts @@ -20,9 +20,9 @@ import { CatalogProcessor, CatalogProcessorEmit, CatalogProcessorParser, - LocationSpec, processingResult, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import limiterFactory from 'p-limit'; /** diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts index b92fcfc022..5e24d4dc13 100644 --- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts +++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts @@ -20,9 +20,9 @@ import { AwsS3Integration, ScmIntegrations } from '@backstage/integration'; import { EntityProvider, EntityProviderConnection, - LocationSpec, locationSpecToLocationEntity, } from '@backstage/plugin-catalog-backend'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; import { AwsCredentials } from '../credentials/AwsCredentials'; import { readAwsS3Configs } from './config'; import { AwsS3Config } from './types'; diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md index c71b691a7a..05e48690d7 100644 --- a/plugins/catalog-backend-module-azure/CHANGELOG.md +++ b/plugins/catalog-backend-module-azure/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-backend-module-azure +## 0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json index da7d1f5aa3..9552fe1e98 100644 --- a/plugins/catalog-backend-module-azure/package.json +++ b/plugins/catalog-backend-module-azure/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-azure", "description": "A Backstage catalog backend module that helps integrate towards Azure", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts index 4852edbb03..8893d5d42d 100644 --- a/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-azure/src/processors/AzureDevOpsDiscoveryProcessor.ts @@ -43,7 +43,7 @@ import { codeSearch } from '../lib'; * target: https://dev.azure.com/org/project/_git/repo * * @public - **/ + */ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; @@ -123,7 +123,7 @@ export class AzureDevOpsDiscoveryProcessor implements CatalogProcessor { /** * parseUrl extracts segments from the Azure DevOps URL. - **/ + */ export function parseUrl(urlString: string): { baseUrl: string; org: string; diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md index 4cd4b6565c..261a192dfa 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-backend-module-bitbucket-cloud +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + - @backstage/plugin-catalog-common@1.0.11-next.1 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json index 1e1f94ac38..78e1e15653 100644 --- a/plugins/catalog-backend-module-bitbucket-cloud/package.json +++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md index ef7755cf0d..b3b825bbc6 100644 --- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-bitbucket-server +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + ## 0.1.7-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json index eba6a276ad..edd1fbf0e6 100644 --- a/plugins/catalog-backend-module-bitbucket-server/package.json +++ b/plugins/catalog-backend-module-bitbucket-server/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket-server", - "version": "0.1.7-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md index 455abd3f2b..b07c7a92bb 100644 --- a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md +++ b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-catalog-backend-module-bitbucket +## 0.2.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-bitbucket-cloud-common@0.2.3 + ## 0.2.9-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-bitbucket/package.json b/plugins/catalog-backend-module-bitbucket/package.json index 646f9af81e..7df64bbc87 100644 --- a/plugins/catalog-backend-module-bitbucket/package.json +++ b/plugins/catalog-backend-module-bitbucket/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-bitbucket", "description": "A Backstage catalog backend module that helps integrate towards Bitbucket", - "version": "0.2.9-next.1", + "version": "0.2.9-next.2", "deprecated": true, "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md index f048780bb7..9efd73aac5 100644 --- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md +++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-catalog-backend-module-gerrit +## 0.1.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + ## 0.1.10-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json index f2327ed216..4e8d389c9c 100644 --- a/plugins/catalog-backend-module-gerrit/package.json +++ b/plugins/catalog-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend-module-gerrit", - "version": "0.1.10-next.1", + "version": "0.1.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md index 430615eabd..089cd237e2 100644 --- a/plugins/catalog-backend-module-github/CHANGELOG.md +++ b/plugins/catalog-backend-module-github/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-github +## 0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + ## 0.2.5-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json index d72d520f6a..36cd96274a 100644 --- a/plugins/catalog-backend-module-github/package.json +++ b/plugins/catalog-backend-module-github/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-github", "description": "A Backstage catalog backend module that helps integrate towards GitHub", - "version": "0.2.5-next.1", + "version": "0.2.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-github/src/lib/annotation.ts b/plugins/catalog-backend-module-github/src/lib/annotation.ts new file mode 100644 index 0000000000..3b2f7b87a8 --- /dev/null +++ b/plugins/catalog-backend-module-github/src/lib/annotation.ts @@ -0,0 +1,37 @@ +/* + * 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. + */ + +/** + * The value of this annotation is the so-called login that identifies a user on +[GitHub](https://github.com) (either the public one, or a private GitHub +Enterprise installation) that is related to this entity. It is on the format +``, and is the same as can be seen in the URL location bar of the +browser when viewing that user. + * + * @public + */ +export const ANNOTATION_GITHUB_USER_LOGIN = 'github.com/user-login'; + +/** + * The value of this annotation is the so-called slug that identifies a team on +[GitHub](https://github.com) (either the public one, or a private GitHub +Enterprise installation) that is related to this entity. It is on the format +`/`, and is the same as can be seen in the URL location bar +of the browser when viewing that team. + * + * @public + */ +export const ANNOTATION_GITHUB_TEAM_SLUG = 'github.com/team-slug'; diff --git a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts index efd3eedd5d..7669fd5b47 100644 --- a/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts +++ b/plugins/catalog-backend-module-github/src/lib/defaultTransformers.ts @@ -16,6 +16,10 @@ import { GroupEntity, UserEntity } from '@backstage/catalog-model'; import { graphql } from '@octokit/graphql'; +import { + ANNOTATION_GITHUB_TEAM_SLUG, + ANNOTATION_GITHUB_USER_LOGIN, +} from './annotation'; import { GithubTeam, GithubUser } from './github'; /** @@ -63,7 +67,7 @@ export const defaultUserTransformer: UserTransformer = async ( metadata: { name: item.login, annotations: { - 'github.com/user-login': item.login, + [ANNOTATION_GITHUB_USER_LOGIN]: item.login, }, }, spec: { @@ -87,7 +91,7 @@ export const defaultUserTransformer: UserTransformer = async ( export const defaultOrganizationTeamTransformer: TeamTransformer = async team => { const annotations: { [annotationName: string]: string } = { - 'github.com/team-slug': team.combinedSlug, + [ANNOTATION_GITHUB_TEAM_SLUG]: team.combinedSlug, }; if (team.editTeamUrl) { diff --git a/plugins/catalog-backend-module-github/src/lib/util.ts b/plugins/catalog-backend-module-github/src/lib/util.ts index d284fdabde..54ee575a6c 100644 --- a/plugins/catalog-backend-module-github/src/lib/util.ts +++ b/plugins/catalog-backend-module-github/src/lib/util.ts @@ -88,3 +88,14 @@ export function satisfiesForkFilter( // if forks are allowed, allow all repos through return true; } + +// Given the github organisation team slug, returns a tuple containing [organisation, team] +export function splitTeamSlug(slug: string): [string, string] { + const parts = slug.split('/'); + if (parts.length !== 2) { + throw new Error( + `Github team slug '${slug}' was not in the expected format /`, + ); + } + return [parts[0], parts[1]]; +} diff --git a/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.ts b/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.ts index 3fec87d0b2..c6733df13b 100644 --- a/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.ts +++ b/plugins/catalog-backend-module-github/src/processors/GithubDiscoveryProcessor.ts @@ -46,7 +46,7 @@ import { getOrganizationRepositories } from '../lib'; * target: https://github.com/backstage/*\/blob/main/catalog-info.yaml * * @public - **/ + */ export class GithubDiscoveryProcessor implements CatalogProcessor { private readonly integrations: ScmIntegrationRegistry; private readonly logger: Logger; diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts index c24cd573e0..2711425998 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts @@ -181,7 +181,10 @@ describe('GithubOrgEntityProvider', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { - name: 'githubuser', + name: 'user-name', + annotations: { + 'github.com/user-login': 'githubuser', + }, }, spec: { memberOf: [], @@ -192,12 +195,13 @@ describe('GithubOrgEntityProvider', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'User', metadata: { - name: 'githubuser', + name: 'user-name', annotations: { 'backstage.io/managed-by-location': 'url:https://github.com/githubuser', 'backstage.io/managed-by-origin-location': 'url:https://github.com/githubuser', + 'github.com/user-login': 'githubuser', }, }, spec: { @@ -211,7 +215,10 @@ describe('GithubOrgEntityProvider', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', metadata: { - name: 'mygroup', + name: 'group-name', + annotations: { + 'github.com/team-slug': 'backstage/mygroup', + }, }, spec: { type: 'team', @@ -223,12 +230,13 @@ describe('GithubOrgEntityProvider', () => { apiVersion: 'backstage.io/v1alpha1', kind: 'Group', metadata: { - name: 'mygroup', + name: 'group-name', annotations: { 'backstage.io/managed-by-location': 'url:https://github.com/orgs/backstage/teams/mygroup', 'backstage.io/managed-by-origin-location': 'url:https://github.com/orgs/backstage/teams/mygroup', + 'github.com/team-slug': 'backstage/mygroup', }, }, spec: { diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts index 3093c2a897..2a888e6f91 100644 --- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts +++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts @@ -65,6 +65,11 @@ import { getOrganizationTeam, getOrganizationTeamsFromUsers, } from '../lib/github'; +import { + ANNOTATION_GITHUB_TEAM_SLUG, + ANNOTATION_GITHUB_USER_LOGIN, +} from '../lib/annotation'; +import { splitTeamSlug } from '../lib/util'; /** * Options for {@link GithubOrgEntityProvider}. @@ -602,10 +607,21 @@ export function withLocations( org: string, entity: Entity, ): Entity { + const login = + entity.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] || + entity.metadata.name; + + let team = entity.metadata.name; + const slug = entity.metadata.annotations?.[ANNOTATION_GITHUB_TEAM_SLUG]; + if (slug) { + const [_, slugTeam] = splitTeamSlug(slug); + team = slugTeam; + } + const location = entity.kind === 'Group' - ? `url:${baseUrl}/orgs/${org}/teams/${entity.metadata.name}` - : `url:${baseUrl}/${entity.metadata.name}`; + ? `url:${baseUrl}/orgs/${org}/teams/${team}` + : `url:${baseUrl}/${login}`; return merge( { metadata: { diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md index 9da2c77fdf..1f5fc26f38 100644 --- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md +++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-catalog-backend-module-gitlab +## 0.1.13-next.2 + +### Patch Changes + +- 49948f644f: The config now consistently uses the term 'instance' to refer to a single GitLab API host. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-gitlab/config.d.ts b/plugins/catalog-backend-module-gitlab/config.d.ts index 4f51867efc..00ad0ef032 100644 --- a/plugins/catalog-backend-module-gitlab/config.d.ts +++ b/plugins/catalog-backend-module-gitlab/config.d.ts @@ -31,7 +31,7 @@ export interface Config { host: string; /** * (Optional) Gitlab's group[/subgroup] where the discovery is done. - * If not defined the whole project will be scanned. + * If not defined the whole instance will be scanned. */ group?: string; /** diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json index 44aadec29b..6668e2f45d 100644 --- a/plugins/catalog-backend-module-gitlab/package.json +++ b/plugins/catalog-backend-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-gitlab", "description": "A Backstage catalog backend module that helps integrate towards GitLab", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md index b52760e185..1953885783 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md +++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md @@ -1,5 +1,24 @@ # @backstage/plugin-catalog-backend-module-incremental-ingestion +## 0.2.0-next.2 + +### Patch Changes + +- 407dc01fc9: Removing extra imports for `run` script as `TestBackend` auto loads the default factories +- 77c41b6924: Updated README to include newer API options for incremental entity providers +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + ## 0.2.0-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-incremental-ingestion/README.md b/plugins/catalog-backend-module-incremental-ingestion/README.md index aa23b8dfc1..7fe36eeec7 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/README.md +++ b/plugins/catalog-backend-module-incremental-ingestion/README.md @@ -41,7 +41,7 @@ The Incremental Entity Provider backend is designed for data sources that provid ## Installation -1. Install `@backstage/plugin-catalog-backend-module-incremental-ingestion` with `yarn workspace backend add @backstage/plugin-catalog-backend-module-incremental-ingestion` +1. Install `@backstage/plugin-catalog-backend-module-incremental-ingestion` with `yarn add --cwd packages/backend @backstage/plugin-catalog-backend-module-incremental-ingestion` from the Backstage root directory. 2. In your catalog.ts, import `IncrementalCatalogBuilder` from `@backstage/plugin-catalog-backend-module-incremental-ingestion` and instantiate it with `await IncrementalCatalogBuilder.create(env, builder)`. You have to pass `builder` into `IncrementalCatalogBuilder.create` function because `IncrementalCatalogBuilder` will convert an `IncrementalEntityProvider` into an `EntityProvider` and call `builder.addEntityProvider`. ```ts diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json index ec50c44311..f6ea81996e 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/package.json +++ b/plugins/catalog-backend-module-incremental-ingestion/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-incremental-ingestion", "description": "An entity provider for streaming large asset sources into the catalog", - "version": "0.2.0-next.1", + "version": "0.2.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,7 +36,6 @@ "@backstage/backend-common": "workspace:^", "@backstage/backend-plugin-api": "workspace:^", "@backstage/backend-tasks": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", @@ -56,6 +55,7 @@ }, "devDependencies": { "@backstage/backend-app-api": "workspace:^", + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@backstage/plugin-catalog-backend": "workspace:^" }, diff --git a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts index 08d95f4417..e9cc7dbb2a 100644 --- a/plugins/catalog-backend-module-incremental-ingestion/src/run.ts +++ b/plugins/catalog-backend-module-incremental-ingestion/src/run.ts @@ -13,20 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -// eslint-disable-next-line import/no-extraneous-dependencies -import { - databaseFactory, - discoveryFactory, - httpRouterFactory, - lifecycleFactory, - loggerFactory, - permissionsFactory, - rootLoggerFactory, - schedulerFactory, - tokenManagerFactory, - urlReaderFactory, -} from '@backstage/backend-app-api'; import { coreServices } from '@backstage/backend-plugin-api'; import { startTestBackend } from '@backstage/backend-test-utils'; import { ConfigReader } from '@backstage/config'; @@ -61,19 +47,7 @@ async function main() { }; await startTestBackend({ - services: [ - [coreServices.config, new ConfigReader(config)], - databaseFactory(), - discoveryFactory(), - httpRouterFactory(), - lifecycleFactory(), - loggerFactory(), - permissionsFactory(), - rootLoggerFactory(), - schedulerFactory(), - tokenManagerFactory(), - urlReaderFactory(), - ], + services: [[coreServices.config, new ConfigReader(config)]], extensionPoints: [], features: [ catalogPlugin(), diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md index f64a4460ea..50245d20ae 100644 --- a/plugins/catalog-backend-module-ldap/CHANGELOG.md +++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-backend-module-ldap +## 0.5.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + ## 0.5.9-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json index aed68ebdf9..6a5e1426f7 100644 --- a/plugins/catalog-backend-module-ldap/package.json +++ b/plugins/catalog-backend-module-ldap/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-ldap", "description": "A Backstage catalog backend module that helps integrate towards LDAP", - "version": "0.5.9-next.1", + "version": "0.5.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md index e53d159d1d..0ee67c7a3c 100644 --- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md +++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-catalog-backend-module-msgraph +## 0.5.0-next.2 + +### Minor Changes + +- fb568e2683: Improve performance when loading users via group membership. + Users data is now loaded from a paged query, rather than having to make an extra call per user to load each user's profiles. + + Note, there are still additional per user calls made to load user avatars + +### Patch Changes + +- 28f9883440: Fixed a bug reading the `user.select` field expected from the `app-config.yaml` configuration +- c5b119ad9c: Increased default page size to 999 (from 100) to reduce the number of calls made to the Microsoft Graph API. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + ## 0.4.8-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index a5abdc17c6..09770614b4 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -70,7 +70,11 @@ export const MICROSOFT_GRAPH_USER_ID_ANNOTATION = 'graph.microsoft.com/user-id'; export class MicrosoftGraphClient { constructor(baseUrl: string, tokenCredential: TokenCredential); static create(config: MicrosoftGraphProviderConfig): MicrosoftGraphClient; - getGroupMembers(groupId: string): AsyncIterable; + getGroupMembers( + groupId: string, + query?: ODataQuery, + queryMode?: 'basic' | 'advanced', + ): AsyncIterable; // (undocumented) getGroupPhoto(groupId: string, sizeId?: string): Promise; getGroupPhotoWithSizeLimit( @@ -81,6 +85,11 @@ export class MicrosoftGraphClient { query?: ODataQuery, queryMode?: 'basic' | 'advanced', ): AsyncIterable; + getGroupUserMembers( + groupId: string, + query?: ODataQuery, + queryMode?: 'basic' | 'advanced', + ): AsyncIterable; getOrganization(tenantId: string): Promise; // (undocumented) getUserPhoto(userId: string, sizeId?: string): Promise; @@ -88,10 +97,6 @@ export class MicrosoftGraphClient { userId: string, maxSize: number, ): Promise; - getUserProfile( - userId: string, - query?: ODataQuery, - ): Promise; getUsers( query?: ODataQuery, queryMode?: 'basic' | 'advanced', @@ -231,6 +236,7 @@ export type ODataQuery = { expand?: string; select?: string[]; count?: boolean; + top?: number; }; // @public diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json index 7be2678bc6..7ae0af0bea 100644 --- a/plugins/catalog-backend-module-msgraph/package.json +++ b/plugins/catalog-backend-module-msgraph/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-msgraph", "description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph", - "version": "0.4.8-next.1", + "version": "0.5.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts index fdaa2a6a43..f7d98f12c6 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.test.ts @@ -70,7 +70,7 @@ describe('MicrosoftGraphClient', () => { expect(await response.json()).toEqual({ value: 'example' }); }); - it('should perform api request with filter, select and expand', async () => { + it('should perform api request with filter, select, expand and top', async () => { worker.use( rest.get('https://example.com/users', (req, res, ctx) => res(ctx.status(200), ctx.json({ queryString: req.url.search })), @@ -81,12 +81,13 @@ describe('MicrosoftGraphClient', () => { filter: 'test eq true', expand: 'children', select: ['id', 'children'], + top: 471, }); expect(response.status).toBe(200); expect(await response.json()).toEqual({ queryString: - '?$filter=test%20eq%20true&$select=id,children&$expand=children', + '?$filter=test%20eq%20true&$select=id,children&$expand=children&$top=471', }); }); @@ -134,33 +135,6 @@ describe('MicrosoftGraphClient', () => { expect(values).toEqual(['first', 'second']); }); - it('should load user profile', async () => { - worker.use( - rest.get('https://example.com/users/user-id', (_, res, ctx) => - res( - ctx.status(200), - ctx.json({ - surname: 'Example', - }), - ), - ), - ); - - const userProfile = await client.getUserProfile('user-id'); - - expect(userProfile).toEqual({ surname: 'Example' }); - }); - - it('should throw exception if load user profile fails', async () => { - worker.use( - rest.get('https://example.com/users/user-id', (_, res, ctx) => - res(ctx.status(404)), - ), - ); - - await expect(() => client.getUserProfile('user-id')).rejects.toThrow(); - }); - it('should load user profile photo with max size of 120', async () => { worker.use( rest.get('https://example.com/users/user-id/photos', (_, res, ctx) => @@ -329,6 +303,27 @@ describe('MicrosoftGraphClient', () => { ]); }); + it('should load user group members', async () => { + worker.use( + rest.get( + 'https://example.com/groups/group-id/members/microsoft.graph.user', + (_, res, ctx) => + res( + ctx.status(200), + ctx.json({ + value: [{ id: '12345' }, { id: '67890' }], + }), + ), + ), + ); + + const values = await collectAsyncIterable( + client.getGroupUserMembers('group-id'), + ); + + expect(values).toEqual([{ id: '12345' }, { id: '67890' }]); + }); + it('should load organization', async () => { worker.use( rest.get('https://example.com/organization/tentant-id', (_, res, ctx) => diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts index 321d784543..7a685b258d 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/client.ts @@ -52,6 +52,10 @@ export type ODataQuery = { * Retrieves the total count of matching resources. */ count?: boolean; + /** + * Maximum number of records to receive in one batch. + */ + top?: number; }; /** @@ -188,6 +192,7 @@ export class MicrosoftGraphClient { $select: query?.select?.join(','), $expand: query?.expand, $count: query?.count, + $top: query?.top, }, { addQueryPrefix: true, @@ -229,28 +234,6 @@ export class MicrosoftGraphClient { }); } - /** - * Get {@link https://docs.microsoft.com/en-us/graph/api/resources/user | User} - * from Graph API - * - * @public - * @param userId - The unique identifier for the `User` resource - * @param query - OData Query {@link ODataQuery} - * - */ - async getUserProfile( - userId: string, - query?: ODataQuery, - ): Promise { - const response = await this.requestApi(`users/${userId}`, query); - - if (response.status !== 200) { - await this.handleError('user profile', response); - } - - return await response.json(); - } - /** * Get {@link https://docs.microsoft.com/en-us/graph/api/resources/profilephoto | profilePhoto} * of `User` from Graph API with size limit @@ -343,8 +326,37 @@ export class MicrosoftGraphClient { * @param groupId - The unique identifier for the `Group` resource * */ - async *getGroupMembers(groupId: string): AsyncIterable { - yield* this.requestCollection(`groups/${groupId}/members`); + async *getGroupMembers( + groupId: string, + query?: ODataQuery, + queryMode?: 'basic' | 'advanced', + ): AsyncIterable { + yield* this.requestCollection( + `groups/${groupId}/members`, + query, + queryMode, + ); + } + + /** + * Get a collection of + * {@link https://docs.microsoft.com/en-us/graph/api/resources/user | User} + * belonging to a `Group` from Graph API and return as `AsyncIterable` + * @public + * @param groupId - The unique identifier for the `Group` resource + * @param query - OData Query {@link ODataQuery} + * @param queryMode - Mode to use while querying. Some features are only available at "advanced". + */ + async *getGroupUserMembers( + groupId: string, + query?: ODataQuery, + queryMode?: 'basic' | 'advanced', + ): AsyncIterable { + yield* this.requestCollection( + `groups/${groupId}/members/microsoft.graph.user/`, + query, + queryMode, + ); } /** diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts index 68c174665d..b02894dfd9 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.test.ts @@ -52,6 +52,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com/', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', @@ -69,6 +70,7 @@ describe('readMicrosoftGraphConfig', () => { authority: 'https://login.example.com/', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', @@ -167,6 +169,7 @@ describe('readProviderConfigs', () => { user: { expand: 'manager', filter: 'accountEnabled eq true', + select: ['id', 'displayName', 'department'], }, group: { expand: 'member', @@ -196,6 +199,7 @@ describe('readProviderConfigs', () => { queryMode: 'advanced', userExpand: 'manager', userFilter: 'accountEnabled eq true', + userSelect: ['id', 'displayName', 'department'], groupExpand: 'member', groupSelect: ['id', 'displayName', 'description'], groupFilter: 'securityEnabled eq false', diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts index 506951ff10..cf4ec646a2 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/config.ts @@ -264,6 +264,7 @@ export function readProviderConfig( const userExpand = config.getOptionalString('user.expand'); const userFilter = config.getOptionalString('user.filter'); + const userSelect = config.getOptionalStringArray('user.select'); const groupExpand = config.getOptionalString('group.expand'); const groupFilter = config.getOptionalString('group.filter'); @@ -318,6 +319,7 @@ export function readProviderConfig( tenantId, userExpand, userFilter, + userSelect, groupExpand, groupFilter, groupSearch, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index bafb2ecaad..b1dfc5a148 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -61,9 +61,9 @@ function group(data: Partial): GroupEntity { describe('read microsoft graph', () => { const client: jest.Mocked = { getUsers: jest.fn(), - getUserProfile: jest.fn(), getGroups: jest.fn(), getGroupMembers: jest.fn(), + getGroupUserMembers: jest.fn(), getUserPhotoWithSizeLimit: jest.fn(), getGroupPhotoWithSizeLimit: jest.fn(), getOrganization: jest.fn(), @@ -78,13 +78,6 @@ describe('read microsoft graph', () => { mail: 'user.name@example.com', }; } - async function getExampleUserProfile() { - return { - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }; - } async function* getExampleGroups() { yield { id: 'groupid', @@ -140,6 +133,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toHaveBeenCalledWith( { filter: 'accountEnabled eq true', + top: 999, }, undefined, ); @@ -186,6 +180,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toHaveBeenCalledWith( { filter: 'accountEnabled eq true', + top: 999, }, 'advanced', ); @@ -228,6 +223,7 @@ describe('read microsoft graph', () => { { expand: 'manager', filter: 'accountEnabled eq true', + top: 999, }, undefined, ); @@ -242,9 +238,8 @@ describe('read microsoft graph', () => { describe('readMicrosoftGraphUsersInGroups', () => { it('should read users from Groups', async () => { client.getGroups.mockImplementation(getExampleGroups); - client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getGroupUserMembers.mockImplementation(getExampleUsers); - client.getUserProfile.mockResolvedValue(getExampleUserProfile()); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -278,16 +273,19 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq true', + select: ['id', 'displayName'], + top: 999, }, undefined, ); - expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); - expect(client.getUserProfile).toHaveBeenCalledTimes(1); - expect(client.getUserProfile).toHaveBeenCalledWith('userid', { - expand: undefined, - }); + expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); + expect(client.getGroupUserMembers).toHaveBeenCalledWith( + 'groupid', + { top: 999 }, + undefined, + ); + expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( 'userid', @@ -297,13 +295,8 @@ describe('read microsoft graph', () => { it('should read users from Groups with advanced query mode', async () => { client.getGroups.mockImplementation(getExampleGroups); - client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getGroupUserMembers.mockImplementation(getExampleUsers); - client.getUserProfile.mockResolvedValue({ - id: 'userid', - displayName: 'User Name', - mail: 'user.name@example.com', - }); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -338,16 +331,19 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq true', + select: ['id', 'displayName'], + top: 999, }, 'advanced', ); - expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); - expect(client.getUserProfile).toHaveBeenCalledTimes(1); - expect(client.getUserProfile).toHaveBeenCalledWith('userid', { - expand: undefined, - }); + expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); + expect(client.getGroupUserMembers).toHaveBeenCalledWith( + 'groupid', + { top: 999 }, + 'advanced', + ); + expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( 'userid', @@ -357,8 +353,8 @@ describe('read microsoft graph', () => { it('should read users with userExpand, groupExpand and custom transformer', async () => { client.getGroups.mockImplementation(getExampleGroups); - client.getGroupMembers.mockImplementation(getExampleGroupMembers); - client.getUserProfile.mockResolvedValue(getExampleUserProfile()); + client.getGroupUserMembers.mockImplementation(getExampleUsers); + client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -390,16 +386,22 @@ describe('read microsoft graph', () => { { expand: 'member', filter: 'securityEnabled eq true', + select: ['id', 'displayName'], + top: 999, + }, + undefined, + ); + + expect(client.getGroupUserMembers).toHaveBeenCalledTimes(1); + expect(client.getGroupUserMembers).toHaveBeenCalledWith( + 'groupid', + { + expand: 'manager', + top: 999, }, undefined, ); - expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); - expect(client.getUserProfile).toHaveBeenCalledTimes(1); - expect(client.getUserProfile).toHaveBeenCalledWith('userid', { - expand: 'manager', - }); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledWith( 'userid', @@ -527,11 +529,14 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -603,11 +608,14 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, 'advanced', ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -680,11 +688,14 @@ describe('read microsoft graph', () => { { expand: 'member', filter: 'securityEnabled eq false', + top: 999, }, undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); // TODO: Loading groups photos doesn't work right now as Microsoft Graph // doesn't allows this yet // expect(client.getGroupPhotoWithSizeLimit).toBeCalledTimes(1); @@ -750,11 +761,14 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq true', + top: 999, }, undefined, ); expect(client.getGroupMembers).toHaveBeenCalledTimes(1); - expect(client.getGroupMembers).toHaveBeenCalledWith('groupid'); + expect(client.getGroupMembers).toHaveBeenCalledWith('groupid', { + top: 999, + }); }); }); @@ -887,6 +901,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toHaveBeenCalledWith( { filter: undefined, + top: 999, }, undefined, ); @@ -894,6 +909,7 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, undefined, ); @@ -925,6 +941,7 @@ describe('read microsoft graph', () => { { expand: 'manager', filter: 'accountEnabled eq true', + top: 999, }, undefined, ); @@ -932,6 +949,7 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, undefined, ); @@ -960,6 +978,7 @@ describe('read microsoft graph', () => { expect(client.getUsers).toHaveBeenCalledWith( { select: ['mail'], + top: 999, }, undefined, ); @@ -972,13 +991,13 @@ describe('read microsoft graph', () => { }); client.getUsers.mockImplementation(getExampleUsers); - client.getUserProfile.mockImplementation(getExampleUserProfile); client.getUserPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); client.getGroups.mockImplementation(getExampleGroups); client.getGroupMembers.mockImplementation(getExampleGroupMembers); + client.getGroupUserMembers.mockImplementation(getExampleUsers); client.getGroupPhotoWithSizeLimit.mockResolvedValue( 'data:image/jpeg;base64,...', ); @@ -994,16 +1013,18 @@ describe('read microsoft graph', () => { expect(client.getGroups).toHaveBeenCalledWith( { filter: 'name eq backstage-group', + select: ['id', 'displayName'], + top: 999, }, undefined, ); expect(client.getGroups).toHaveBeenCalledWith( { filter: 'securityEnabled eq false', + top: 999, }, undefined, ); - expect(client.getUserProfile).toHaveBeenCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toHaveBeenCalledTimes(1); }); }); diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 6699af7764..003448a3e8 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -38,6 +38,9 @@ import { defaultOrganizationTransformer, defaultUserTransformer, } from './defaultTransformers'; +import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; + +const PAGE_SIZE = 999; export async function readMicrosoftGraphUsers( client: MicrosoftGraphClient, @@ -52,50 +55,24 @@ export async function readMicrosoftGraphUsers( ): Promise<{ users: UserEntity[]; // With all relations empty }> { - const users: UserEntity[] = []; - const limiter = limiterFactory(10); - - const transformer = options.transformer ?? defaultUserTransformer; - const promises: Promise[] = []; - - for await (const user of client.getUsers( + const users = client.getUsers( { filter: options.userFilter, expand: options.userExpand, select: options.userSelect, + top: PAGE_SIZE, }, options.queryMode, - )) { - // Process all users in parallel, otherwise it can take quite some time - promises.push( - limiter(async () => { - let userPhoto; - try { - userPhoto = await client.getUserPhotoWithSizeLimit( - user.id!, - // We are limiting the photo size, as users with full resolution photos - // can make the Backstage API slow - 120, - ); - } catch (e) { - options.logger.warn(`Unable to load photo for ${user.id}, ${e}`); - } + ); - const entity = await transformer(user, userPhoto); - - if (!entity) { - return; - } - - users.push(entity); - }), - ); - } - - // Wait for all users and photos to be downloaded - await Promise.all(promises); - - return { users }; + return { + users: await transformUsers( + client, + users, + options.logger, + options.transformer, + ), + }; } export async function readMicrosoftGraphUsersInGroups( @@ -113,36 +90,41 @@ export async function readMicrosoftGraphUsersInGroups( ): Promise<{ users: UserEntity[]; // With all relations empty }> { - const users: UserEntity[] = []; - const limiter = limiterFactory(10); - const transformer = options.transformer ?? defaultUserTransformer; const userGroupMemberPromises: Promise[] = []; - const userPromises: Promise[] = []; - - const groupMemberUsers: Set = new Set(); + const userGroupMembers = new Map(); for await (const group of client.getGroups( { expand: options.groupExpand, search: options.userGroupMemberSearch, filter: options.userGroupMemberFilter, + select: ['id', 'displayName'], + top: PAGE_SIZE, }, options.queryMode, )) { // Process all groups in parallel, otherwise it can take quite some time userGroupMemberPromises.push( limiter(async () => { - for await (const member of client.getGroupMembers(group.id!)) { - if (!member.id) { - continue; - } - - if (member['@odata.type'] === '#microsoft.graph.user') { - groupMemberUsers.add(member.id); - } + let groupMemberCount = 0; + for await (const user of client.getGroupUserMembers( + group.id!, + { + expand: options.userExpand, + top: PAGE_SIZE, + }, + options.queryMode, + )) { + userGroupMembers.set(user.id!, user); + groupMemberCount++; } + options.logger.debug('Read users from group', { + groupId: group.id, + groupName: group.displayName, + memberCount: groupMemberCount, + }); }), ); } @@ -150,47 +132,19 @@ export async function readMicrosoftGraphUsersInGroups( // Wait for all group members await Promise.all(userGroupMemberPromises); - options.logger.info(`groupMemberUsers ${groupMemberUsers.size}`); - for (const userId of groupMemberUsers) { - // Process all users in parallel, otherwise it can take quite some time - userPromises.push( - limiter(async () => { - let user; - let userPhoto; - try { - user = await client.getUserProfile(userId, { - expand: options.userExpand, - }); - } catch (e) { - options.logger.warn(`Unable to load user for ${userId}, ${e}`); - } - if (user) { - try { - userPhoto = await client.getUserPhotoWithSizeLimit( - user.id!, - // We are limiting the photo size, as users with full resolution photos - // can make the Backstage API slow - 120, - ); - } catch (e) { - options.logger.warn(`Unable to load userphoto for ${userId}, ${e}`); - } + options.logger.info('Read users from group membership', { + groupCount: userGroupMemberPromises.length, + userCount: userGroupMembers.size, + }); - const entity = await transformer(user, userPhoto); - - if (!entity) { - return; - } - users.push(entity); - } - }), - ); - } - - // Wait for all users and photos to be downloaded - await Promise.all(userPromises); - - return { users }; + return { + users: await transformUsers( + client, + userGroupMembers.values(), + options.logger, + options.transformer, + ), + }; } export async function readMicrosoftGraphOrganization( @@ -248,6 +202,7 @@ export async function readMicrosoftGraphGroups( search: options?.groupSearch, filter: options?.groupFilter, select: options?.groupSelect, + top: PAGE_SIZE, }, options?.queryMode, )) { @@ -269,7 +224,9 @@ export async function readMicrosoftGraphGroups( return; } - for await (const member of client.getGroupMembers(group.id!)) { + for await (const member of client.getGroupMembers(group.id!, { + top: PAGE_SIZE, + })) { if (!member.id) { continue; } @@ -461,6 +418,56 @@ export async function readMicrosoftGraphOrg( return { users, groups }; } +async function transformUsers( + client: MicrosoftGraphClient, + users: Iterable | AsyncIterable, + logger: Logger, + transformer?: UserTransformer, +) { + const limiter = limiterFactory(10); + + const resolvedTransformer = transformer ?? defaultUserTransformer; + const promises: Promise[] = []; + const entities: UserEntity[] = []; + + // Process all users in parallel, otherwise it can take quite some time + for await (const user of users) { + promises.push( + limiter(async () => { + let userPhoto; + try { + userPhoto = await client.getUserPhotoWithSizeLimit( + user.id!, + // We are limiting the photo size, as users with full resolution photos + // can make the Backstage API slow + 120, + ); + } catch (e) { + logger.warn(`Unable to load user photo for`, { + user: user.id, + error: e, + }); + } + + const entity = await resolvedTransformer(user, userPhoto); + + if (entity) { + entities.push(entity); + } + }), + ); + } + + // Wait for all users and photos to be downloaded + await Promise.all(promises); + + logger.debug('Finished transforming users', { + microsoftUserCount: promises.length, + backstageUserCount: entities.length, + }); + return entities; +} + function ensureItem( target: Map>, key: string, diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md index 5dd91ecd63..496221964e 100644 --- a/plugins/catalog-backend-module-openapi/CHANGELOG.md +++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-catalog-backend-module-openapi +## 0.1.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/config@1.0.6 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + ## 0.1.8-next.1 ### Patch Changes diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json index 6ccc4971b0..8f08f4b183 100644 --- a/plugins/catalog-backend-module-openapi/package.json +++ b/plugins/catalog-backend-module-openapi/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-backend-module-openapi", "description": "A Backstage catalog backend module that helps with OpenAPI specifications", - "version": "0.1.8-next.1", + "version": "0.1.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 1ee436c075..329f641c78 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-catalog-backend +## 1.7.2-next.2 + +### Patch Changes + +- e716946103: Updated usage of the lifecycle service. +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + - @backstage/plugin-search-common@1.2.1 + ## 1.7.2-next.1 ### Patch Changes diff --git a/plugins/catalog-backend/openapi.yaml b/plugins/catalog-backend/openapi.yaml new file mode 100644 index 0000000000..ea09f5da10 --- /dev/null +++ b/plugins/catalog-backend/openapi.yaml @@ -0,0 +1,783 @@ +openapi: 3.1.0 + +info: + title: '@backstage/plugin-catalog-backend' + version: '1' + description: The Backstage backend plugin that provides the Backstage catalog + license: + name: Apache-2.0 + contact: {} + +servers: + - url: / + +components: + examples: {} + headers: {} + parameters: {} + requestBodies: {} + responses: {} + schemas: + RefreshOptions: + type: object + properties: + authorizationToken: + type: string + entityRef: + type: string + description: The reference to a single entity that should be refreshed + required: + - entityRef + description: Options for requesting a refresh of entities in the catalog. + JsonObject: + type: object + properties: {} + description: A type representing all allowed JSON object values. + MapStringString: + type: object + properties: {} + additionalProperties: + type: string + description: Construct a type with a set of properties K of type T + EntityLink: + type: object + properties: + type: + type: string + description: An optional value to categorize links into specific groups + icon: + type: string + description: An optional semantic key that represents a visual icon. + title: + type: string + description: An optional descriptive title for the link. + url: + type: string + description: The url to the external site, document, etc. + required: + - url + description: A link to external information that is related to the entity. + EntityMeta: + allOf: + - $ref: '#/components/schemas/JsonObject' + - type: object + properties: + links: + type: array + items: + $ref: '#/components/schemas/EntityLink' + description: A list of external hyperlinks related to the entity. + tags: + type: array + items: + type: string + description: |- + A list of single-valued strings, to for example classify catalog entities in + various ways. + annotations: + $ref: '#/components/schemas/MapStringString' + labels: + $ref: '#/components/schemas/MapStringString' + description: + type: string + description: |- + A short (typically relatively few words, on one line) description of the + entity. + title: + type: string + description: |- + A display name of the entity, to be presented in user interfaces instead + of the `name` property above, when available. + This field is sometimes useful when the `name` is cumbersome or ends up + being perceived as overly technical. The title generally does not have + as stringent format requirements on it, so it may contain special + characters and be more explanatory. Do keep it very short though, and + avoid situations where a title can be confused with the name of another + entity, or where two entities share a title. + Note that this is only for display purposes, and may be ignored by some + parts of the code. Entity references still always make use of the `name` + property, not the title. + namespace: + type: string + description: The namespace that the entity belongs to. + name: + type: string + description: |- + The name of the entity. + Must be unique within the catalog at any given point in time, for any + given namespace + kind pair. This value is part of the technical + identifier of the entity, and as such it will appear in URLs, database + tables, entity references, and similar. It is subject to restrictions + regarding what characters are allowed. + If you want to use a different, more human readable string with fewer + restrictions on it in user interfaces, see the `title` field below. + etag: + type: string + description: |- + An opaque string that changes for each update operation to any part of + the entity, including metadata. + This field can not be set by the user at creation time, and the server + will reject an attempt to do so. The field will be populated in read + operations. The field can (optionally) be specified when performing + update or delete operations, and the server will then reject the + operation if it does not match the current stored value. + uid: + type: string + description: |- + A globally unique ID for the entity. + This field can not be set by the user at creation time, and the server + will reject an attempt to do so. The field will be populated in read + operations. The field can (optionally) be specified when performing + update or delete operations, but the server is free to reject requests + that do so in such a way that it breaks semantics. + required: + - name + description: Metadata fields common to all versions/kinds of entity. + EntityRelation: + type: object + properties: + targetRef: + type: string + description: The entity ref of the target of this relation. + type: + type: string + description: The type of the relation. + required: + - targetRef + - type + description: A relation of a specific type to another entity in the catalog. + Entity: + type: object + properties: + relations: + type: array + items: + $ref: '#/components/schemas/EntityRelation' + description: The relations that this entity has with other entities. + spec: + $ref: '#/components/schemas/JsonObject' + metadata: + $ref: '#/components/schemas/EntityMeta' + kind: + type: string + description: The high level entity type being described. + apiVersion: + type: string + description: |- + The version of specification format for this particular entity that + this is written against. + required: + - metadata + - kind + - apiVersion + description: The parts of the format that's common to all versions/kinds of entity. + EntityAncestryResponse: + type: object + properties: + items: + type: array + items: + type: object + properties: + parentEntityRefs: + items: + type: string + type: array + entity: + $ref: '#/components/schemas/Entity' + required: + - parentEntityRefs + - entity + rootEntityRef: + type: string + required: + - items + - rootEntityRef + EntitiesBatchResponse: + type: object + properties: + items: + type: array + items: + allOf: + - $ref: '#/components/schemas/Entity' + nullable: true + description: |- + The list of entities, in the same order as the refs in the request. Entries + that are null signify that no entity existed with that ref. + required: + - items + EntityFacets: + type: object + properties: + value: + type: string + count: + type: number + description: Construct a type with a set of properties K of type T + EntityFacetsResponse: + type: object + properties: {} + additionalProperties: + $ref: '#/components/schemas/EntityFacets' + required: + - facets + Location: + type: object + properties: + target: + type: string + type: + type: string + id: + type: string + required: + - target + - type + - id + description: Entity location for a specific entity. + LocationSpec: + type: object + properties: + presence: + type: string + enum: + - optional + - required + target: + type: string + type: + type: string + required: + - target + - type + description: Holds the entity location information. + AnalyzeLocationExistingEntity: + type: object + properties: + entity: + $ref: '#/components/schemas/Entity' + isRegistered: + type: boolean + location: + $ref: '#/components/schemas/LocationSpec' + required: + - entity + - isRegistered + - location + description: |- + If the folder pointed to already contained catalog info yaml files, they are + read and emitted like this so that the frontend can inform the user that it + located them and can make sure to register them as well if they weren't + already + RecursivePartial_Entity_: + type: object + properties: + apiVersion: + type: string + description: |- + The version of specification format for this particular entity that + this is written against. + kind: + type: string + description: The high level entity type being described. + metadata: + $ref: '#/components/schemas/EntityMeta' + spec: + $ref: '#/components/schemas/JsonObject' + relations: + type: array + items: + $ref: '#/components/schemas/EntityRelation' + description: The relations that this entity has with other entities. + description: Makes all keys of an entire hierarchy optional. + AnalyzeLocationEntityField: + type: object + properties: + description: + type: string + description: |- + A text to show to the user to inform about the choices made. Like, it could say + "Found a CODEOWNERS file that covers this target, so we suggest leaving this + field empty; which would currently make it owned by X" where X is taken from the + codeowners file. + value: + type: string + nullable: true + state: + type: string + enum: + - analysisSuggestedValue + - analysisSuggestedNoValue + - needsUserInput + description: The outcome of the analysis for this particular field + field: + type: string + description: |- + e.g. "spec.owner"? The frontend needs to know how to "inject" the field into the + entity again if the user wants to change it + required: + - description + - value + - state + - field + AnalyzeLocationGenerateEntity: + type: object + properties: + fields: + type: array + items: + $ref: '#/components/schemas/AnalyzeLocationEntityField' + entity: + $ref: '#/components/schemas/RecursivePartial_Entity_' + required: + - fields + - entity + description: |- + This is some form of representation of what the analyzer could deduce. + We should probably have a chat about how this can best be conveyed to + the frontend. It'll probably contain a (possibly incomplete) entity, plus + enough info for the frontend to know what form data to show to the user + for overriding/completing the info. + AnalyzeLocationResponse: + type: object + properties: + generateEntities: + items: + $ref: '#/components/schemas/AnalyzeLocationGenerateEntity' + type: array + existingEntityFiles: + items: + $ref: '#/components/schemas/AnalyzeLocationExistingEntity' + type: array + required: + - generateEntities + - existingEntityFiles + LocationInput: + type: object + properties: + type: + type: string + target: + type: string + presence: + type: string + enum: + - optional + - required + required: + - type + - target + - presence + SerializedError: + allOf: + - $ref: '#/components/schemas/JsonObject' + - type: object + properties: + code: + type: string + description: A custom code (not necessarily the same as an HTTP response code); may not be present + stack: + type: string + description: A stringified stack trace; may not be present + message: + type: string + description: The message of the exception that was thrown + name: + type: string + description: The name of the exception that was thrown + required: + - message + - name + description: The serialized form of an Error. + securitySchemes: + JWT: + type: http + scheme: bearer + bearerFormat: JWT + +paths: + /refresh: + post: + operationId: RefreshEntity + responses: + '200': + description: Refreshed + security: + # From https://stackoverflow.com/questions/47659324/how-to-specify-an-endpoints-authorization-is-optional-in-openapi-v3 + - {} + - JWT: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/RefreshOptions' + + /entities: + get: + operationId: GetEntities + responses: + '200': + description: '' + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/Entity' + security: + - {} + - JWT: [] + parameters: + - in: query + name: filter + required: false + schema: + type: string + - in: query + name: fields + required: false + schema: + type: string + - in: query + name: offset + required: false + schema: + type: string + - in: query + name: limit + required: false + schema: + type: string + - in: query + name: after + required: false + schema: + type: string + + /entities/by-uid/{uid}: + get: + operationId: GetEntityByUid + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Entity' + security: + - {} + - JWT: [] + parameters: + - in: path + name: uid + required: true + schema: + type: string + delete: + operationId: DeleteEntityByUid + responses: + '204': + description: Deleted successfully. + security: + - {} + - JWT: [] + parameters: + - in: path + name: uid + required: true + schema: + type: string + + /entities/by-name/{kind}/{namespace}/{name}: + get: + operationId: GetEntityByName + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Entity' + security: + - {} + - JWT: [] + parameters: + - in: path + name: kind + required: true + schema: + type: string + - in: path + name: namespace + required: true + schema: + type: string + - in: path + name: name + required: true + schema: + type: string + + /entities/by-name/{kind}/{namespace}/{name}/ancestry: + get: + operationId: GetEntityAncestryByName + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntityAncestryResponse' + security: + - {} + - JWT: [] + parameters: + - in: path + name: kind + required: true + schema: + type: string + - in: path + name: namespace + required: true + schema: + type: string + - in: path + name: name + required: true + schema: + type: string + + /entities/by-refs: + post: + operationId: GetEntitiesByRefs + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntitiesBatchResponse' + security: + - {} + - JWT: [] + parameters: + - in: query + name: fields + required: false + schema: + type: string + requestBody: + required: true + content: + application/json: + schema: + type: array + items: + type: string + + /entity-facets: + get: + operationId: GetEntityFacets + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/EntityFacetsResponse' + security: + - {} + - JWT: [] + parameters: + - in: query + name: facet + required: true + schema: + type: string + - in: query + name: filter + required: false + schema: + type: string + + /locations: + post: + operationId: CreateLocation + responses: + '200': + description: Ok + content: + application/json: + schema: + type: object + properties: + exists: + type: boolean + entities: + items: + $ref: '#/components/schemas/Entity' + type: array + location: + $ref: '#/components/schemas/Location' + required: + - entities + - location + security: + - {} + - JWT: [] + parameters: + - in: query + name: dryRun + required: false + schema: + type: boolean + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + presence: + type: string + enum: + - required + - optional + target: + type: string + type: + type: string + required: + - presence + - target + - type + get: + operationId: GetLocations + responses: + '200': + description: Ok + content: + application/json: + schema: + type: array + items: + type: object + properties: + data: + $ref: '#/components/schemas/Location' + required: + - data + security: + - {} + - JWT: [] + parameters: [] + + /locations/{id}: + get: + operationId: GetLocation + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + security: + - {} + - JWT: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + delete: + operationId: DeleteLocation + responses: + '204': + description: No content + security: + - {} + - JWT: [] + parameters: + - in: path + name: id + required: true + schema: + type: string + + /analyze-location: + post: + operationId: AnalyzeLocation + responses: + '200': + description: Ok + content: + application/json: + schema: + $ref: '#/components/schemas/AnalyzeLocationResponse' + security: + - {} + - JWT: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + catalogFileName: + type: string + location: + $ref: '#/components/schemas/LocationInput' + required: + - catalogFileName + - location + + /validate-entity: + post: + operationId: ValidateEntity + responses: + '200': + description: Ok + content: + application/json: + schema: + anyOf: + - type: object + properties: + errors: + $ref: '#/components/schemas/SerializedError' + required: + - errors + - type: object + properties: + errors: + type: array + items: + $ref: '#/components/schemas/SerializedError' + required: + - errors + security: + - {} + - JWT: [] + parameters: [] + requestBody: + required: true + content: + application/json: + schema: + type: object + properties: + location: + type: string + entity: + $ref: '#/components/schemas/Entity' + required: + - location + - entity diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 2bf0429932..cebd91932b 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": "1.7.2-next.1", + "version": "1.7.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-backend/src/database/DefaultCatalogDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultCatalogDatabase.test.ts index b873f6a985..827ab58ebe 100644 --- a/plugins/catalog-backend/src/database/DefaultCatalogDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultCatalogDatabase.test.ts @@ -105,6 +105,7 @@ describe('DefaultCatalogDatabase', () => { 'location:default/root-2', ]); }, + 60_000, ); }); }); diff --git a/plugins/catalog-backend/src/database/DefaultProviderDatabase.test.ts b/plugins/catalog-backend/src/database/DefaultProviderDatabase.test.ts index 140925b6fd..09bec187fa 100644 --- a/plugins/catalog-backend/src/database/DefaultProviderDatabase.test.ts +++ b/plugins/catalog-backend/src/database/DefaultProviderDatabase.test.ts @@ -745,5 +745,39 @@ describe('DefaultProviderDatabase', () => { }, 60_000, ); + + it.each(databases.eachSupportedId())( + 'should gracefully handle accidental duplicate refresh state references when deletion happens during a full sync, %p', + async databaseId => { + const fakeLogger = { debug: jest.fn() }; + const { knex, db } = await createDatabase( + databaseId, + fakeLogger as any, + ); + + await createLocations(knex, ['component:default/a']); + + await insertRefRow(knex, { + source_key: 'a', + target_entity_ref: 'component:default/a', + }); + await insertRefRow(knex, { + source_key: 'a', + target_entity_ref: 'component:default/a', + }); + + await db.transaction(async tx => { + await db.replaceUnprocessedEntities(tx, { + type: 'full', + sourceKey: 'a', + items: [], + }); + }); + + const state = await knex('refresh_state').select(); + expect(state).toEqual([]); + }, + 60_000, + ); }); }); diff --git a/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.test.ts b/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.test.ts new file mode 100644 index 0000000000..8ab94df64a --- /dev/null +++ b/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.test.ts @@ -0,0 +1,278 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Knex } from 'knex'; +import * as uuid from 'uuid'; +import { applyDatabaseMigrations } from '../../migrations'; +import { DbRefreshStateReferencesRow, DbRefreshStateRow } from '../../tables'; +import { deleteWithEagerPruningOfChildren } from './deleteWithEagerPruningOfChildren'; + +describe('deleteWithEagerPruningOfChildren', () => { + const databases = TestDatabases.create({ + ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'], + }); + + async function createDatabase(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + await applyDatabaseMigrations(knex); + return knex; + } + + async function run( + knex: Knex, + options: { entityRefs: string[]; sourceKey: string }, + ): Promise { + let result: number; + await knex.transaction( + async tx => { + // We can't return here, as knex swallows the return type in case the + // transaction is rolled back: + // https://github.com/knex/knex/blob/e37aeaa31c8ef9c1b07d2e4d3ec6607e557d800d/lib/transaction.js#L136 + result = await deleteWithEagerPruningOfChildren({ tx, ...options }); + }, + { + // If we explicitly trigger a rollback, don't fail. + doNotRejectOnRollback: true, + }, + ); + return result!; + } + + async function insertReference( + knex: Knex, + ...refs: DbRefreshStateReferencesRow[] + ) { + return knex('refresh_state_references').insert( + refs, + ); + } + + async function insertEntity(knex: Knex, ...entityRefs: string[]) { + for (const ref of entityRefs) { + await knex('refresh_state').insert({ + entity_id: uuid.v4(), + entity_ref: ref, + unprocessed_entity: '{}', + processed_entity: '{}', + errors: '[]', + next_update_at: '2021-04-01 13:37:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + } + } + + async function remainingEntities(knex: Knex) { + const rows = await knex('refresh_state') + .orderBy('entity_ref') + .select('entity_ref'); + return rows.map(r => r.entity_ref); + } + + it.each(databases.eachSupportedId())( + 'works for the simple path, %p', + async databaseId => { + /* + P1 - E1 - E2 + + P1 - E3 + + P1 - E4 + + P2 - E5 + + Scenario: P1 issues delete for E1 and E3 + + Result: E1, E2, and E3 deleted; E4 and E5 remain + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3', 'E4', 'E5'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_key: 'P1', target_entity_ref: 'E3' }, + { source_key: 'P1', target_entity_ref: 'E4' }, + { source_key: 'P2', target_entity_ref: 'E5' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1', 'E3'] }); + await expect(remainingEntities(knex)).resolves.toEqual(['E4', 'E5']); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'works when there are multiple identical references, %p', + async databaseId => { + /* + P1 + \ + E1 + / + P1 + + P1 - E2 + + Scenario: P1 issues delete for E1 + + Result: E1 deleted; E2 remains + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_key: 'P1', target_entity_ref: 'E2' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1'] }); + await expect(remainingEntities(knex)).resolves.toEqual(['E2']); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'leaves out things that have roots in other source keys, %p', + async databaseId => { + /* + P1 - E1 + \ + E2 + / + P2 - E3 + + Scenario: P1 issues delete for E1 + + Result: E1 deleted; E2 and E3 remain + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_key: 'P2', target_entity_ref: 'E3' }, + { source_entity_ref: 'E3', target_entity_ref: 'E2' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1'] }); + await expect(remainingEntities(knex)).resolves.toEqual(['E2', 'E3']); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'leaves out things that have several different roots for the same source key, %p', + async databaseId => { + /* + P1 - E1 + \ + E2 + / + P1 - E3 + + Scenario: P1 issues delete for E1 + + Result: E1 deleted; E2 and E3 remain + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_key: 'P1', target_entity_ref: 'E3' }, + { source_entity_ref: 'E3', target_entity_ref: 'E2' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1'] }); + await expect(remainingEntities(knex)).resolves.toEqual(['E2', 'E3']); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'handles cycles and diamonds gracefully, %p', + async databaseId => { + /* + P1 - E1 <-> E2 + \ + E4 E6 + / \ / + P1 -- E3 -- E5 + + Scenario: P1 issues delete for E1, then for E3 + + Result: Everything deleted, but in two steps + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3', 'E4', 'E5', 'E6'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_entity_ref: 'E2', target_entity_ref: 'E1' }, + { source_entity_ref: 'E2', target_entity_ref: 'E6' }, + { source_key: 'P1', target_entity_ref: 'E3' }, + { source_entity_ref: 'E3', target_entity_ref: 'E4' }, + { source_entity_ref: 'E4', target_entity_ref: 'E5' }, + { source_entity_ref: 'E3', target_entity_ref: 'E5' }, + { source_entity_ref: 'E5', target_entity_ref: 'E6' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E1'] }); + await expect(remainingEntities(knex)).resolves.toEqual([ + 'E3', + 'E4', + 'E5', + 'E6', + ]); + await run(knex, { sourceKey: 'P1', entityRefs: ['E3'] }); + await expect(remainingEntities(knex)).resolves.toEqual([]); + }, + 60_000, + ); + + it.each(databases.eachSupportedId())( + 'silently ignores attempts to delete things that are not your own and/or are not roots, %p', + async databaseId => { + /* + P1 - E1 - E2 + + P1 - E3 + + P2 - E4 + + Scenario: P1 issues delete for E2, E3, and E4 + + Result: E3 is deleted; E1, E2 and E4 remain + */ + const knex = await createDatabase(databaseId); + await insertEntity(knex, 'E1', 'E2', 'E3', 'E4'); + await insertReference( + knex, + { source_key: 'P1', target_entity_ref: 'E1' }, + { source_entity_ref: 'E1', target_entity_ref: 'E2' }, + { source_key: 'P1', target_entity_ref: 'E3' }, + { source_key: 'P2', target_entity_ref: 'E4' }, + ); + await run(knex, { sourceKey: 'P1', entityRefs: ['E2', 'E3', 'E4'] }); + await expect(remainingEntities(knex)).resolves.toEqual([ + 'E1', + 'E2', + 'E4', + ]); + }, + 60_000, + ); +}); diff --git a/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.ts b/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.ts index 3d642cbf3a..33da37bc55 100644 --- a/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.ts +++ b/plugins/catalog-backend/src/database/operations/provider/deleteWithEagerPruningOfChildren.ts @@ -16,7 +16,7 @@ import { Knex } from 'knex'; import lodash from 'lodash'; -import { DbRefreshStateReferencesRow, DbRefreshStateRow } from '../../tables'; +import { DbRefreshStateReferencesRow } from '../../tables'; /** * Given a number of entity refs originally created by a given entity provider @@ -31,116 +31,157 @@ export async function deleteWithEagerPruningOfChildren(options: { sourceKey: string; }): Promise { const { tx, entityRefs, sourceKey } = options; - let removedCount = 0; - - const rootId = () => - tx.raw( - tx.client.config.client.includes('mysql') - ? 'CAST(NULL as UNSIGNED INT)' - : 'CAST(NULL as INT)', - [], - ); // Split up the operation by (large) chunks, so that we do not hit database // limits for the number of permitted bindings on a precompiled statement + let removedCount = 0; for (const refs of lodash.chunk(entityRefs, 1000)) { - /* - WITH RECURSIVE - -- All the nodes that can be reached downwards from our root - descendants(root_id, entity_ref) AS ( - SELECT id, target_entity_ref - FROM refresh_state_references - WHERE source_key = "R1" AND target_entity_ref = "A" - UNION - SELECT descendants.root_id, target_entity_ref - FROM descendants - JOIN refresh_state_references ON source_entity_ref = descendants.entity_ref - ), - -- All the nodes that can be reached upwards from the descendants - ancestors(root_id, via_entity_ref, to_entity_ref) AS ( - SELECT CAST(NULL as INT), entity_ref, entity_ref - FROM descendants - UNION - SELECT - CASE WHEN source_key IS NOT NULL THEN id ELSE NULL END, - source_entity_ref, - ancestors.to_entity_ref - FROM ancestors - JOIN refresh_state_references ON target_entity_ref = ancestors.via_entity_ref - ) - -- Start out with all of the descendants - SELECT descendants.entity_ref - FROM descendants - -- Expand with all ancestors that point to those, but aren't the current root - LEFT OUTER JOIN ancestors - ON ancestors.to_entity_ref = descendants.entity_ref - AND ancestors.root_id IS NOT NULL - AND ancestors.root_id != descendants.root_id - -- Exclude all lines that had such a foreign ancestor - WHERE ancestors.root_id IS NULL; - */ - removedCount += await tx('refresh_state') - .whereIn('entity_ref', function orphanedEntityRefs(orphans) { - return ( - orphans - // All the nodes that can be reached downwards from our root - .withRecursive('descendants', function descendants(outer) { - return outer - .select({ root_id: 'id', entity_ref: 'target_entity_ref' }) - .from('refresh_state_references') - .where('source_key', sourceKey) - .whereIn('target_entity_ref', refs) - .union(function recursive(inner) { - return inner - .select({ - root_id: 'descendants.root_id', - entity_ref: 'refresh_state_references.target_entity_ref', - }) - .from('descendants') - .join('refresh_state_references', { - 'descendants.entity_ref': - 'refresh_state_references.source_entity_ref', - }); - }); - }) - // All the nodes that can be reached upwards from the descendants - .withRecursive('ancestors', function ancestors(outer) { - return outer - .select({ - root_id: rootId(), - via_entity_ref: 'entity_ref', - to_entity_ref: 'entity_ref', - }) + removedCount += await tx + .delete() + .from('refresh_state') + .whereIn('entity_ref', orphans => + orphans + // First find all nodes that can be reached downwards from the roots + // (deletion targets), including the roots themselves, by traversing + // down the refresh_state_references table. Note that this query + // starts with a condition that source_key = our source key, and + // target_entity_ref is one of the deletion targets. This has two + // effects: it won't match attempts at deleting something that didn't + // originate from us in the first place, and also won't match non-root + // entities (source_key would be null for those). + // + // KeyA - R1 - R2 Legend: + // \ ----------------------------------------- + // R3 Key* Source key + // / R* Entity ref + // KeyA - R4 - R5 lines Individual references; sources to + // / the left and targets to the right + // KeyB --- R6 + // + // The scenario is that KeyA wants to delete R1. + // + // The query starts with the KeyA-R1 reference, and then traverses + // down to also find R2 and R3. It uses union instead of union all, + // because it wants to find the set of unique descendants even if + // the tree has unexpected loops etc. + .withRecursive('descendants', ['entity_ref'], initial => + initial + .select('target_entity_ref') + .from('refresh_state_references') + .where('source_key', '=', sourceKey) + .whereIn('target_entity_ref', refs) + .union(recursive => + recursive + .select('refresh_state_references.target_entity_ref') + .from('descendants') + .join( + 'refresh_state_references', + 'descendants.entity_ref', + 'refresh_state_references.source_entity_ref', + ), + ), + ) + // Then for each descendant, traverse all the way back upwards through + // the refresh_state_references table to get an exhaustive list of all + // references that are part of keeping that particular descendant + // alive. + // + // Continuing the scenario from above, starting from R3, it goes + // upwards to find every pair along every relation line. + // + // Top branch: R2-R3, R1-R2, KeyA-R1 + // Middle branch: R5-R3, R4-R5, KeyA-R4 + // Bottom branch: R6-R5, KeyB-R6 + // + // Note that this all applied to the subject R3. The exact same thing + // will be done starting from each other descendant (R2 and R1). They + // only have one and two references to find, respectively. + // + // This query also uses union instead of union all, to get the set of + // distinct relations even if the tree has unexpected loops etc. + .withRecursive( + 'ancestors', + ['source_key', 'source_entity_ref', 'target_entity_ref', 'subject'], + initial => + initial + .select( + 'refresh_state_references.source_key', + 'refresh_state_references.source_entity_ref', + 'refresh_state_references.target_entity_ref', + 'descendants.entity_ref', + ) .from('descendants') - .union(function recursive(inner) { - return inner - .select({ - root_id: tx.raw( - 'CASE WHEN source_key IS NOT NULL THEN id ELSE NULL END', - [], - ), - via_entity_ref: 'source_entity_ref', - to_entity_ref: 'ancestors.to_entity_ref', - }) + .join( + 'refresh_state_references', + 'refresh_state_references.target_entity_ref', + 'descendants.entity_ref', + ) + .union(recursive => + recursive + .select( + 'refresh_state_references.source_key', + 'refresh_state_references.source_entity_ref', + 'refresh_state_references.target_entity_ref', + 'ancestors.subject', + ) .from('ancestors') - .join('refresh_state_references', { - target_entity_ref: 'ancestors.via_entity_ref', - }); - }); - }) - // Start out with all of the descendants - .select('descendants.entity_ref') - .from('descendants') - // Expand with all ancestors that point to those, but aren't the current root - .leftOuterJoin('ancestors', function keepaliveRoots() { - this.on('ancestors.to_entity_ref', '=', 'descendants.entity_ref'); - this.andOnNotNull('ancestors.root_id'); - this.andOn('ancestors.root_id', '!=', 'descendants.root_id'); - }) - .whereNull('ancestors.root_id') - ); - }) - .delete(); + .join( + 'refresh_state_references', + 'refresh_state_references.target_entity_ref', + 'ancestors.source_entity_ref', + ), + ), + ) + // Finally, from that list of ancestor relations per descendant, pick + // out the ones that are roots (have a source_key). Specifically, find + // ones that seem to be be either (1) from another source, or (2) + // aren't part of the deletion targets. Those are markers that tell us + // that the corresponding descendant should be kept alive and NOT + // subject to eager deletion, because there's "something else" (not + // targeted for deletion) that has references down through the tree to + // it. + // + // Continuing the scenario from above, for R3 we have + // + // KeyA-R1, KeyA-R4, KeyB-R6 + // + // This tells us that R3 should be kept alive for two reasons: it's + // referenced by a node that isn't being deleted (R4), and also by + // another source (KeyB). What about R1 and R2? They both have + // + // KeyA-R1 + // + // So those should be deleted, since they are definitely only being + // kept alive by something that's about to be deleted. + // + // Final shape of the tree: + // + // R3 + // / + // KeyA - R4 - R5 + // / + // KeyB --- R6 + .with('retained', ['entity_ref'], notPartOfDeletion => + notPartOfDeletion + .select('subject') + .from('ancestors') + .whereNotNull('ancestors.source_key') + .where(foreignKeyOrRef => + foreignKeyOrRef + .where('ancestors.source_key', '!=', sourceKey) + .orWhereNotIn('ancestors.target_entity_ref', refs), + ), + ) + // Return all descendants minus the retained ones + .select('descendants.entity_ref') + .from('descendants') + .leftOuterJoin( + 'retained', + 'retained.entity_ref', + 'descendants.entity_ref', + ) + .whereNull('retained.entity_ref'), + ); // Delete the references that originate only from this entity provider. Note // that there may be more than one entity provider making a "claim" for a diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts index 34e89fad29..592b1dda21 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.test.ts @@ -35,6 +35,10 @@ const entity = { }; const location: Record = { + v: { + type: 'url', + target: 'https://github.com/b/c/blob/master/.folder/v.yaml', + }, w: { type: 'url', target: 'https://github.com/b/c/blob/master/w.yaml', @@ -263,4 +267,20 @@ describe('DefaultCatalogRulesEnforcer', () => { expect(enforcer.isAllowed(entity.component, location.z)).toBe(false); }); }); + + it('should allow locations with a hidden folder', () => { + const enforcer = DefaultCatalogRulesEnforcer.fromConfig( + new ConfigReader({ + catalog: { + rules: [ + { + allow: ['Component'], + locations: [{ type: 'url', pattern: 'https://github.com/b/**' }], + }, + ], + }, + }), + ); + expect(enforcer.isAllowed(entity.component, location.v)).toBe(true); + }); }); diff --git a/plugins/catalog-backend/src/ingestion/CatalogRules.ts b/plugins/catalog-backend/src/ingestion/CatalogRules.ts index 677649d4b2..21b2d05a46 100644 --- a/plugins/catalog-backend/src/ingestion/CatalogRules.ts +++ b/plugins/catalog-backend/src/ingestion/CatalogRules.ts @@ -186,7 +186,10 @@ export class DefaultCatalogRulesEnforcer implements CatalogRulesEnforcer { } if ( matcher.pattern && - !minimatch(location?.target, matcher.pattern, { nocase: true }) + !minimatch(location?.target, matcher.pattern, { + nocase: true, + dot: true, + }) ) { continue; } diff --git a/plugins/catalog-backend/src/integration.test.ts b/plugins/catalog-backend/src/integration.test.ts index 4ba046834b..f0f916a78d 100644 --- a/plugins/catalog-backend/src/integration.test.ts +++ b/plugins/catalog-backend/src/integration.test.ts @@ -52,9 +52,14 @@ import { } from '@backstage/plugin-catalog-node'; import { RefreshStateItem } from './database/types'; import { DefaultProviderDatabase } from './database/DefaultProviderDatabase'; +import { InputError } from '@backstage/errors'; const voidLogger = getVoidLogger(); +type ProgressTrackerWithErrorReports = ProgressTracker & { + reportError(unprocessedEntity: Entity, errors: Error[]): void; +}; + class TestProvider implements EntityProvider { #connection?: EntityProviderConnection; @@ -74,10 +79,10 @@ class TestProvider implements EntityProvider { } } -class ProxyProgressTracker implements ProgressTracker { - #inner: ProgressTracker; +class ProxyProgressTracker implements ProgressTrackerWithErrorReports { + #inner: ProgressTrackerWithErrorReports; - constructor(inner: ProgressTracker) { + constructor(inner: ProgressTrackerWithErrorReports) { this.#inner = inner; } @@ -85,12 +90,16 @@ class ProxyProgressTracker implements ProgressTracker { return this.#inner.processStart(item, voidLogger); } - setTracker(tracker: ProgressTracker) { + setTracker(tracker: ProgressTrackerWithErrorReports) { this.#inner = tracker; } + + reportError(unprocessedEntity: Entity, errors: Error[]): void { + this.#inner.reportError(unprocessedEntity, errors); + } } -class NoopProgressTracker implements ProgressTracker { +class NoopProgressTracker implements ProgressTrackerWithErrorReports { static emptyTracking = { markFailed() {}, markProcessorsCompleted() {}, @@ -102,18 +111,20 @@ class NoopProgressTracker implements ProgressTracker { processStart() { return NoopProgressTracker.emptyTracking; } + + reportError() {} } -class WaitingProgressTracker implements ProgressTracker { - #resolve: (errors: Record) => void; - #promise: Promise>; +class WaitingProgressTracker implements ProgressTrackerWithErrorReports { + #resolve: (errors: Record) => void; + #promise: Promise>; #counts = new Map(); - #errors = new Map(); + #errors = new Map(); #inFlight = new Array>(); constructor(private readonly entityRefs?: Set) { - let resolve: (errors: Record) => void; - this.#promise = new Promise>(_resolve => { + let resolve: (errors: Record) => void; + this.#promise = new Promise>(_resolve => { resolve = _resolve; }); this.#resolve = resolve!; @@ -143,7 +154,7 @@ class WaitingProgressTracker implements ProgressTracker { }; return { markFailed: (error: Error) => { - this.#errors.set(item.entityRef, error); + this.#errors.set(item.entityRef, [error]); onDone(); resolve(); }, @@ -154,7 +165,6 @@ class WaitingProgressTracker implements ProgressTracker { resolve(); }, markSuccessfulWithErrors: () => { - this.#errors.delete(item.entityRef); onDone(); resolve(); }, @@ -165,7 +175,11 @@ class WaitingProgressTracker implements ProgressTracker { }; } - async wait(): Promise> { + reportError(unprocessedEntity: Entity, errors: Error[]): void { + this.#errors.set(stringifyEntityRef(unprocessedEntity), errors); + } + + async wait(): Promise> { return this.#promise; } @@ -191,10 +205,6 @@ class TestHarness { location: LocationSpec, emit: CatalogProcessorEmit, ): Promise; - onProcessingError?(event: { - unprocessedEntity: Entity; - errors: Error[]; - }): void; }) { const config = new ConfigReader( options?.config ?? { @@ -271,13 +281,7 @@ class TestHarness { () => createHash('sha1'), 50, event => { - if (options?.onProcessingError) { - options.onProcessingError(event); - } else { - throw new Error( - `Catalog processing error, ${event.errors.join(', ')}`, - ); - } + proxyProgressTracker.reportError(event.unprocessedEntity, event.errors); }, proxyProgressTracker, ); @@ -388,7 +392,13 @@ describe('Catalog Backend Integration', () => { triggerError = true; - await expect(harness.process()).resolves.toEqual({}); + await expect(harness.process()).resolves.toEqual({ + 'component:default/test': [ + new InputError( + 'Processor Object threw an error while preprocessing; caused by Error: NOPE', + ), + ], + }); await expect(harness.getOutputEntities()).resolves.toEqual({ 'component:default/test': { @@ -685,4 +695,69 @@ describe('Catalog Backend Integration', () => { .annotations!['backstage.io/orphan'], ).toBeUndefined(); }); + + it('should reject insecure URLs', async () => { + const harness = await TestHarness.create(); + + await harness.setInputEntities([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'backstage.io/managed-by-location': 'url:.', + 'backstage.io/managed-by-origin-location': 'url:.', + 'backstage.io/view-url': ' javascript:bad()', + 'backstage.io/edit-url': ' javascript:alert()', + }, + }, + }, + ]); + + await expect(harness.process()).resolves.toEqual({}); + + await expect(harness.getOutputEntities()).resolves.toEqual({ + 'component:default/test': { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: expect.objectContaining({ + name: 'test', + annotations: expect.objectContaining({ + 'backstage.io/view-url': + 'https://backstage.io/annotation-rejected-for-security-reasons', + 'backstage.io/edit-url': + 'https://backstage.io/annotation-rejected-for-security-reasons', + }), + }), + relations: [], + }, + }); + }); + + it('should reject insecure location URLs', async () => { + const harness = await TestHarness.create(); + + await harness.setInputEntities([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + annotations: { + 'backstage.io/managed-by-location': 'url:javascript:bad()', + 'backstage.io/managed-by-origin-location': 'url:javascript:alert()', + }, + }, + }, + ]); + + await expect(harness.process()).resolves.toEqual({ + 'component:default/test': [ + new TypeError( + "Invalid location ref 'url:javascript:bad()', target is a javascript: URL", + ), + ], + }); + }); }); diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts index 5b34c98ca6..b2630f0f73 100644 --- a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.test.ts @@ -25,7 +25,7 @@ describe('createPropertyRule', () => { it('formats the rule description correctly', () => { expect(description).toBe( - 'Allow entities which have the specified metadata subfield.', + 'Allow entities with the specified metadata subfield', ); }); diff --git a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts index 19a4423846..fd51a1a060 100644 --- a/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts +++ b/plugins/catalog-backend/src/permissions/rules/createPropertyRule.ts @@ -22,7 +22,7 @@ import { z } from 'zod'; export const createPropertyRule = (propertyType: 'metadata' | 'spec') => createCatalogPermissionRule({ name: `HAS_${propertyType.toUpperCase()}`, - description: `Allow entities which have the specified ${propertyType} subfield.`, + description: `Allow entities with the specified ${propertyType} subfield`, resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ key: z diff --git a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts index c24620f41c..ae9b1c3963 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasAnnotation.ts @@ -28,8 +28,7 @@ import { createCatalogPermissionRule } from './util'; */ export const hasAnnotation = createCatalogPermissionRule({ name: 'HAS_ANNOTATION', - description: - 'Allow entities which are annotated with the specified annotation', + description: 'Allow entities with the specified annotation', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ annotation: z.string().describe('Name of the annotation to match on'), diff --git a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts index 376534ca6f..16c5917585 100644 --- a/plugins/catalog-backend/src/permissions/rules/hasLabel.ts +++ b/plugins/catalog-backend/src/permissions/rules/hasLabel.ts @@ -25,10 +25,10 @@ import { createCatalogPermissionRule } from './util'; */ export const hasLabel = createCatalogPermissionRule({ name: 'HAS_LABEL', - description: 'Allow entities which have the specified label metadata.', + description: 'Allow entities with the specified label', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ - label: z.string().describe('Name of the label to match one'), + label: z.string().describe('Name of the label to match on'), }), apply: (resource, { label }) => !!resource.metadata.labels?.hasOwnProperty(label), diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts index eee7a790b2..94aaa63567 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityKind.ts @@ -25,7 +25,7 @@ import { createCatalogPermissionRule } from './util'; */ export const isEntityKind = createCatalogPermissionRule({ name: 'IS_ENTITY_KIND', - description: 'Allow entities with the specified kind', + description: 'Allow entities matching a specified kind', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ kinds: z diff --git a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts index c3bfff5c2e..d0cc1d9280 100644 --- a/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts +++ b/plugins/catalog-backend/src/permissions/rules/isEntityOwner.ts @@ -27,7 +27,7 @@ import { createCatalogPermissionRule } from './util'; */ export const isEntityOwner = createCatalogPermissionRule({ name: 'IS_ENTITY_OWNER', - description: 'Allow entities owned by the current user', + description: 'Allow entities owned by a specified claim', resourceType: RESOURCE_TYPE_CATALOG_ENTITY, paramsSchema: z.object({ claims: z diff --git a/plugins/catalog-backend/src/service/CatalogPlugin.ts b/plugins/catalog-backend/src/service/CatalogPlugin.ts index effed7f8f1..5abb630668 100644 --- a/plugins/catalog-backend/src/service/CatalogPlugin.ts +++ b/plugins/catalog-backend/src/service/CatalogPlugin.ts @@ -56,7 +56,7 @@ class CatalogExtensionPointImpl implements CatalogProcessingExtensionPoint { * @alpha */ export const catalogPlugin = createBackendPlugin({ - id: 'catalog', + pluginId: 'catalog', register(env) { const processingExtensions = new CatalogExtensionPointImpl(); // plugins depending on this API will be initialized before this plugins init method is executed. @@ -97,11 +97,7 @@ export const catalogPlugin = createBackendPlugin({ const { processingEngine, router } = await builder.build(); await processingEngine.start(); - lifecycle.addShutdownHook({ - fn: async () => { - await processingEngine.stop(); - }, - }); + lifecycle.addShutdownHook(() => processingEngine.stop()); httpRouter.use(router); }, }); diff --git a/plugins/catalog-backend/src/stitching/Stitcher.ts b/plugins/catalog-backend/src/stitching/Stitcher.ts index 4742c81933..bdf9f8791a 100644 --- a/plugins/catalog-backend/src/stitching/Stitcher.ts +++ b/plugins/catalog-backend/src/stitching/Stitcher.ts @@ -17,6 +17,8 @@ import { ENTITY_STATUS_CATALOG_PROCESSING_TYPE } from '@backstage/catalog-client'; import { AlphaEntity, + ANNOTATION_EDIT_URL, + ANNOTATION_VIEW_URL, EntityRelation, EntityStatusItem, } from '@backstage/catalog-model'; @@ -32,6 +34,11 @@ import { import { buildEntitySearch } from './buildEntitySearch'; import { BATCH_SIZE, generateStableHash } from './util'; +// See https://github.com/facebook/react/blob/f0cf832e1d0c8544c36aa8b310960885a11a847c/packages/react-dom-bindings/src/shared/sanitizeURL.js +const scriptProtocolPattern = + // eslint-disable-next-line no-control-regex + /^[\u0000-\u001F ]*j[\r\n\t]*a[\r\n\t]*v[\r\n\t]*a[\r\n\t]*s[\r\n\t]*c[\r\n\t]*r[\r\n\t]*i[\r\n\t]*p[\r\n\t]*t[\r\n\t]*\:/i; + /** * Performs the act of stitching - to take all of the various outputs from the * ingestion process, and stitching them together into the final entity JSON @@ -166,6 +173,14 @@ export class Stitcher { })); } } + // We opt to do this check here as we otherwise can't guarantee that it will be run after all processors + for (const annotation of [ANNOTATION_VIEW_URL, ANNOTATION_EDIT_URL]) { + const value = entity.metadata.annotations?.[annotation]; + if (typeof value === 'string' && scriptProtocolPattern.test(value)) { + entity.metadata.annotations![annotation] = + 'https://backstage.io/annotation-rejected-for-security-reasons'; + } + } // TODO: entityRef is lower case and should be uppercase in the final // result diff --git a/plugins/catalog-common/CHANGELOG.md b/plugins/catalog-common/CHANGELOG.md index 7ea2f35c98..b5dc4e29e8 100644 --- a/plugins/catalog-common/CHANGELOG.md +++ b/plugins/catalog-common/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-common +## 1.0.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + ## 1.0.11-next.0 ### Patch Changes diff --git a/plugins/catalog-common/package.json b/plugins/catalog-common/package.json index f5c91f3697..2ba16c3d2a 100644 --- a/plugins/catalog-common/package.json +++ b/plugins/catalog-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-common", "description": "Common functionalities for the catalog plugin", - "version": "1.0.11-next.0", + "version": "1.0.11-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md index 13b1364d66..f8ff5a85b8 100644 --- a/plugins/catalog-customized/CHANGELOG.md +++ b/plugins/catalog-customized/CHANGELOG.md @@ -1,5 +1,13 @@ # @internal/plugin-catalog-customized +## 0.0.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.0.7-next.1 ### Patch Changes diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json index e72331539d..9f1e310508 100644 --- a/plugins/catalog-customized/package.json +++ b/plugins/catalog-customized/package.json @@ -1,7 +1,7 @@ { "name": "@internal/plugin-catalog-customized", "description": "The internal Backstage Customizable plugin for browsing the Backstage catalog", - "version": "0.0.7-next.1", + "version": "0.0.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md index dcdbe96b91..d45fa8f234 100644 --- a/plugins/catalog-graph/CHANGELOG.md +++ b/plugins/catalog-graph/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-graph +## 0.2.27-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.27-next.1 ### Patch Changes diff --git a/plugins/catalog-graph/README.md b/plugins/catalog-graph/README.md index 33dee92935..f723523716 100644 --- a/plugins/catalog-graph/README.md +++ b/plugins/catalog-graph/README.md @@ -24,9 +24,10 @@ The plugin comes with these features: To use the catalog graph plugin, you have to add some things to your Backstage app: -1. Add a dependency to your `packages/app/package.json`, run: +1. Add a dependency to your `packages/app/package.json`: ```sh - yarn add @backstage/plugin-catalog-graph + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-catalog-graph ``` 2. Add the `CatalogGraphPage` to your `packages/app/src/App.tsx`: diff --git a/plugins/catalog-graph/api-report.md b/plugins/catalog-graph/api-report.md index 95f497fe95..f7444c5f1b 100644 --- a/plugins/catalog-graph/api-report.md +++ b/plugins/catalog-graph/api-report.md @@ -65,19 +65,13 @@ export enum Direction { } // @public -export const EntityCatalogGraphCard: (props: { - variant?: InfoCardVariants | undefined; - relationPairs?: RelationPairs | undefined; - maxDepth?: number | undefined; - unidirectional?: boolean | undefined; - mergeRelations?: boolean | undefined; - kinds?: string[] | undefined; - relations?: string[] | undefined; - direction?: Direction | undefined; - height?: number | undefined; - title?: string | undefined; - zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; -}) => JSX.Element; +export const EntityCatalogGraphCard: ( + props: Partial & { + variant?: InfoCardVariants | undefined; + height?: number | undefined; + title?: string | undefined; + }, +) => JSX.Element; // @public export type EntityEdge = DependencyGraphTypes.DependencyEdge; @@ -103,26 +97,27 @@ export type EntityNodeData = { }; // @public -export const EntityRelationsGraph: (props: { +export const EntityRelationsGraph: ( + props: EntityRelationsGraphProps, +) => JSX.Element; + +// @public (undocumented) +export type EntityRelationsGraphProps = { rootEntityNames: CompoundEntityRef | CompoundEntityRef[]; - maxDepth?: number | undefined; - unidirectional?: boolean | undefined; - mergeRelations?: boolean | undefined; - kinds?: string[] | undefined; - relations?: string[] | undefined; - direction?: Direction | undefined; - onNodeClick?: - | ((value: EntityNode, event: MouseEvent_2) => void) - | undefined; - relationPairs?: RelationPairs | undefined; - className?: string | undefined; - zoom?: 'disabled' | 'enabled' | 'enable-on-click' | undefined; - renderNode?: DependencyGraphTypes.RenderNodeFunction | undefined; - renderLabel?: - | DependencyGraphTypes.RenderLabelFunction - | undefined; - curve?: 'curveStepBefore' | 'curveMonotoneX' | undefined; -}) => JSX.Element; + maxDepth?: number; + unidirectional?: boolean; + mergeRelations?: boolean; + kinds?: string[]; + relations?: string[]; + direction?: Direction; + onNodeClick?: (value: EntityNode, event: MouseEvent_2) => void; + relationPairs?: RelationPairs; + className?: string; + zoom?: 'enabled' | 'disabled' | 'enable-on-click'; + renderNode?: DependencyGraphTypes.RenderNodeFunction; + renderLabel?: DependencyGraphTypes.RenderLabelFunction; + curve?: 'curveStepBefore' | 'curveMonotoneX'; +}; // @public export type RelationPairs = [string, string][]; diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json index 9ff0c8e92a..aee919241c 100644 --- a/plugins/catalog-graph/package.json +++ b/plugins/catalog-graph/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-graph", - "version": "0.2.27-next.1", + "version": "0.2.27-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index b9a15eb569..ae4ad850a4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -36,8 +36,8 @@ import { Direction, EntityNode, EntityRelationsGraph, - RelationPairs, } from '../EntityRelationsGraph'; +import { EntityRelationsGraphProps } from '../EntityRelationsGraph/EntityRelationsGraph'; const useStyles = makeStyles( { @@ -55,29 +55,26 @@ const useStyles = makeStyles( { name: 'PluginCatalogGraphCatalogGraphCard' }, ); -export const CatalogGraphCard = (props: { - variant?: InfoCardVariants; - relationPairs?: RelationPairs; - maxDepth?: number; - unidirectional?: boolean; - mergeRelations?: boolean; - kinds?: string[]; - relations?: string[]; - direction?: Direction; - height?: number; - title?: string; - zoom?: 'enabled' | 'disabled' | 'enable-on-click'; -}) => { +export const CatalogGraphCard = ( + props: Partial & { + variant?: InfoCardVariants; + height?: number; + title?: string; + }, +) => { const { variant = 'gridItem', relationPairs = ALL_RELATION_PAIRS, maxDepth = 1, unidirectional = true, mergeRelations = true, + direction = Direction.LEFT_RIGHT, kinds, relations, - direction = Direction.LEFT_RIGHT, height, + className, + rootEntityNames, + onNodeClick, title = 'Relations', zoom = 'enable-on-click', } = props; @@ -90,7 +87,7 @@ export const CatalogGraphCard = (props: { const classes = useStyles({ height }); const analytics = useAnalytics(); - const onNodeClick = useCallback( + const defaultOnNodeClick = useCallback( (node: EntityNode, _: MouseEvent) => { const nodeEntityName = parseEntityRef(node.id); const path = catalogEntityRoute({ @@ -113,8 +110,8 @@ export const CatalogGraphCard = (props: { rootEntityRefs: [stringifyEntityRef(entity)], unidirectional, mergeRelations, - kinds, - relations, + selectedKinds: kinds, + selectedRelations: relations, direction, }, { arrayFormat: 'brackets', addQueryPrefix: true }, @@ -133,15 +130,14 @@ export const CatalogGraphCard = (props: { }} > diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx index 67aa230ce2..2451f4913a 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityRelationsGraph.tsx @@ -67,11 +67,9 @@ const useStyles = makeStyles( ); /** - * Core building block for custom entity relations diagrams. - * * @public */ -export const EntityRelationsGraph = (props: { +export type EntityRelationsGraphProps = { rootEntityNames: CompoundEntityRef | CompoundEntityRef[]; maxDepth?: number; unidirectional?: boolean; @@ -86,7 +84,14 @@ export const EntityRelationsGraph = (props: { renderNode?: DependencyGraphTypes.RenderNodeFunction; renderLabel?: DependencyGraphTypes.RenderLabelFunction; curve?: 'curveStepBefore' | 'curveMonotoneX'; -}) => { +}; + +/** + * Core building block for custom entity relations diagrams. + * + * @public + */ +export const EntityRelationsGraph = (props: EntityRelationsGraphProps) => { const { rootEntityNames, maxDepth = 2, diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts index b86c8f05ae..06227babe3 100644 --- a/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts +++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/index.ts @@ -14,6 +14,7 @@ * limitations under the License. */ export { EntityRelationsGraph } from './EntityRelationsGraph'; +export type { EntityRelationsGraphProps } from './EntityRelationsGraph'; export { ALL_RELATION_PAIRS } from './relations'; export type { RelationPairs } from './relations'; export { Direction } from './types'; diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md index 5d4581212a..ee49cb0c3e 100644 --- a/plugins/catalog-graphql/CHANGELOG.md +++ b/plugins/catalog-graphql/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-catalog-graphql +## 0.3.18-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + ## 0.3.18-next.0 ### Patch Changes diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index 2f68052900..e66e0e0db2 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-graphql", "description": "An experimental Backstage catalog GraphQL module", - "version": "0.3.18-next.0", + "version": "0.3.18-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index 3f0711c721..9c74c13c88 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-catalog-import +## 0.9.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.9.5-next.1 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 36351746e5..a128aedfd8 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-import", "description": "A Backstage plugin the helps you import entities into your catalog", - "version": "0.9.5-next.1", + "version": "0.9.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md index b01d88ae26..8d5667e4a6 100644 --- a/plugins/catalog-node/CHANGELOG.md +++ b/plugins/catalog-node/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-catalog-node +## 1.3.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + ## 1.3.3-next.1 ### Patch Changes diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json index 7874e36072..70071dcaea 100644 --- a/plugins/catalog-node/package.json +++ b/plugins/catalog-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog-node", "description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend", - "version": "1.3.3-next.1", + "version": "1.3.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md index 7f657f418f..662b57550a 100644 --- a/plugins/catalog-react/CHANGELOG.md +++ b/plugins/catalog-react/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-catalog-react +## 1.3.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + ## 1.3.0-next.1 ### Minor Changes diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 705c1b544a..c3e6cdda46 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": "1.3.0-next.1", + "version": "1.3.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 588bfd40cd..204df92577 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-catalog +## 1.8.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + ## 1.8.0-next.1 ### Minor Changes diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md index 901120bd22..6d746f6a7c 100644 --- a/plugins/catalog/api-report.md +++ b/plugins/catalog/api-report.md @@ -19,6 +19,7 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; import { StarredEntitiesApi } from '@backstage/plugin-catalog-react'; import { StorageApi } from '@backstage/core-plugin-api'; import { StyleRules } from '@material-ui/core/styles/withStyles'; @@ -108,7 +109,7 @@ export const catalogPlugin: BackstagePlugin< // @public (undocumented) export const CatalogSearchResultListItem: ( - props: CatalogSearchResultListItemProps, + props: SearchResultListItemExtensionProps, ) => JSX.Element | null; // @public diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index 333d621d40..c5e2c675c9 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-catalog", "description": "The Backstage plugin for browsing the Backstage catalog", - "version": "1.8.0-next.1", + "version": "1.8.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx index cf60138d99..a277584d2f 100644 --- a/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx +++ b/plugins/catalog/src/components/CatalogSearchResultListItem/CatalogSearchResultListItem.tsx @@ -18,8 +18,6 @@ import React, { ReactNode } from 'react'; import { Box, Chip, - Divider, - ListItem, ListItemIcon, ListItemText, makeStyles, @@ -31,16 +29,20 @@ import { } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '@backstage/plugin-search-react'; -const useStyles = makeStyles({ - flexContainer: { - flexWrap: 'wrap', +const useStyles = makeStyles( + { + item: {}, + flexContainer: { + flexWrap: 'wrap', + }, + itemText: { + width: '100%', + wordBreak: 'break-all', + marginBottom: '1rem', + }, }, - itemText: { - width: '100%', - wordBreak: 'break-all', - marginBottom: '1rem', - }, -}); + { name: 'CatalogSearchResultListItem' }, +); /** * Props for {@link CatalogSearchResultListItem}. @@ -66,49 +68,44 @@ export function CatalogSearchResultListItem( if (!result) return null; return ( - <> - - {props.icon && {props.icon}} -
    - - {highlight?.fields.title ? ( - - ) : ( - result.title - )} - - } - secondary={ - highlight?.fields.text ? ( +
    + {props.icon && {props.icon}} +
    + + {highlight?.fields.title ? ( ) : ( - result.text - ) - } - /> - - {result.kind && ( - - )} - {result.lifecycle && ( - - )} - -
    - - - + result.title + )} + + } + secondary={ + highlight?.fields.text ? ( + + ) : ( + result.text + ) + } + /> + + {result.kind && } + {result.lifecycle && ( + + )} + +
    +
    ); } diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 935e002375..18080bb7b0 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -16,6 +16,7 @@ import { ANNOTATION_EDIT_URL, ANNOTATION_VIEW_URL, + Entity, RELATION_OWNED_BY, RELATION_PART_OF, } from '@backstage/catalog-model'; @@ -62,6 +63,16 @@ const YellowStar = withStyles({ }, })(Star); +const refCompare = (a: Entity, b: Entity) => { + const toRef = (entity: Entity) => + entity.metadata.title || + humanizeEntityRef(entity, { + defaultKind: 'Component', + }); + + return toRef(a).localeCompare(toRef(b)); +}; + /** @public */ export const CatalogTable = (props: CatalogTableProps) => { const { columns, actions, tableOptions, subtitle, emptyContent } = props; @@ -177,7 +188,7 @@ export const CatalogTable = (props: CatalogTableProps) => { }, ]; - const rows = entities.map(entity => { + const rows = entities.sort(refCompare).map(entity => { const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, { kind: 'system', }); diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 12c22eb537..ea6ddcaff2 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -31,7 +31,10 @@ import { fetchApiRef, storageApiRef, } from '@backstage/core-plugin-api'; -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react'; +import { + createSearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '@backstage/plugin-search-react'; import { DefaultStarredEntitiesApi } from './apis'; import { AboutCardProps } from './components/AboutCard'; import { DefaultCatalogPageProps } from './components/CatalogPage'; @@ -254,7 +257,7 @@ export const RelatedEntitiesCard: ( /** @public */ export const CatalogSearchResultListItem: ( - props: CatalogSearchResultListItemProps, + props: SearchResultListItemExtensionProps, ) => JSX.Element | null = catalogPlugin.provide( createSearchResultListItemExtension({ name: 'CatalogSearchResultListItem', diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md index 811df4adcb..6516506197 100644 --- a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md +++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cicd-statistics-module-gitlab +## 0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-cicd-statistics@0.1.17-next.2 + ## 0.1.11-next.1 ### Patch Changes diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json index 32c33e207a..2c79050e98 100644 --- a/plugins/cicd-statistics-module-gitlab/package.json +++ b/plugins/cicd-statistics-module-gitlab/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics-module-gitlab", "description": "CI/CD Statistics plugin module; Gitlab CICD", - "version": "0.1.11-next.1", + "version": "0.1.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md index a09c2b3339..5dcd054002 100644 --- a/plugins/cicd-statistics/CHANGELOG.md +++ b/plugins/cicd-statistics/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-cicd-statistics +## 0.1.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.17-next.1 ### Patch Changes diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json index 09ff03a4c7..795d8a6c8f 100644 --- a/plugins/cicd-statistics/package.json +++ b/plugins/cicd-statistics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cicd-statistics", "description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)", - "version": "0.1.17-next.1", + "version": "0.1.17-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index c3b50931e8..701b6f5a86 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-circleci +## 0.3.15-next.2 + +### Patch Changes + +- 1616e06047: Making workflow a link +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.3.15-next.1 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 6e370c5def..b72b344eeb 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-circleci", "description": "A Backstage plugin that integrates towards Circle CI", - "version": "0.3.15-next.1", + "version": "0.3.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx index adc8a569bb..22f69ac30f 100644 --- a/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/circleci/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -212,6 +212,14 @@ const generatedColumns: TableColumn[] = [ { title: 'Workflow', field: 'workflow.name', + highlight: true, + render: (row: Partial) => ( + + {row?.workflow?.name} + + ), }, { title: 'Actions', diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index e69f59d9bf..2b2c262778 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-cloudbuild +## 0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.3.15-next.1 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index f80944b683..c1ac873f30 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cloudbuild", "description": "A Backstage plugin that integrates towards Google Cloud Build", - "version": "0.3.15-next.1", + "version": "0.3.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md index 84c51e3f43..5264e35a96 100644 --- a/plugins/code-climate/CHANGELOG.md +++ b/plugins/code-climate/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-code-climate +## 0.1.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.15-next.1 ### Patch Changes diff --git a/plugins/code-climate/dev/index.tsx b/plugins/code-climate/dev/index.tsx index d5fc4c28e3..060679337e 100644 --- a/plugins/code-climate/dev/index.tsx +++ b/plugins/code-climate/dev/index.tsx @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import { createDevApp, EntityGridItem } from '@backstage/dev-utils'; import { Grid } from '@material-ui/core'; import React from 'react'; diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json index 9783ee7583..9146f3e118 100644 --- a/plugins/code-climate/package.json +++ b/plugins/code-climate/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-code-climate", - "version": "0.1.15-next.1", + "version": "0.1.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/code-climate/src/setupTests.ts b/plugins/code-climate/src/setupTests.ts index 43d9e09713..eb4654b869 100644 --- a/plugins/code-climate/src/setupTests.ts +++ b/plugins/code-climate/src/setupTests.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ import '@testing-library/jest-dom'; import 'cross-fetch/polyfill'; diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md index 4d6a69c494..a5e6acae94 100644 --- a/plugins/code-coverage-backend/CHANGELOG.md +++ b/plugins/code-coverage-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-code-coverage-backend +## 0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/code-coverage-backend/README.md b/plugins/code-coverage-backend/README.md index ae6021b07c..802e538125 100644 --- a/plugins/code-coverage-backend/README.md +++ b/plugins/code-coverage-backend/README.md @@ -4,7 +4,10 @@ This is the backend part of the `code-coverage` plugin. It takes care of process ## Installation -`yarn workspace backend add @backstage/plugin-code-coverage-backend` +```sh +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-code-coverage-backend +``` First create a `codecoverage.ts` file here: `packages/backend/src/plugins`. Now add the following as its content: diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json index 1d3ae89cc3..f9bcefbd5d 100644 --- a/plugins/code-coverage-backend/package.json +++ b/plugins/code-coverage-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage-backend", "description": "A Backstage backend plugin that helps you keep track of your code coverage", - "version": "0.2.8-next.1", + "version": "0.2.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md index f787ac85aa..5064067470 100644 --- a/plugins/code-coverage/CHANGELOG.md +++ b/plugins/code-coverage/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-code-coverage +## 0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/code-coverage/README.md b/plugins/code-coverage/README.md index ce793a78d3..d4daa6ec00 100644 --- a/plugins/code-coverage/README.md +++ b/plugins/code-coverage/README.md @@ -4,7 +4,10 @@ This is the frontend part of the code-coverage plugin. It displays code coverage ## Installation -`yarn workspace app add @backstage/plugin-code-coverage` +```sh +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-code-coverage +``` Finally you need to import and render the code coverage entity, in `packages/app/src/components/catalog/EntityPage.tsx` add the following: diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json index d336d9324f..fd15272e38 100644 --- a/plugins/code-coverage/package.json +++ b/plugins/code-coverage/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-code-coverage", "description": "A Backstage plugin that helps you keep track of your code coverage", - "version": "0.2.8-next.1", + "version": "0.2.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md index a89f49dc62..d07630cadf 100644 --- a/plugins/codescene/CHANGELOG.md +++ b/plugins/codescene/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-codescene +## 0.1.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + ## 0.1.10-next.0 ### Patch Changes diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json index 6f763d529c..4249e11064 100644 --- a/plugins/codescene/package.json +++ b/plugins/codescene/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-codescene", - "version": "0.1.10-next.0", + "version": "0.1.10-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md index a8b4640584..21f079f591 100644 --- a/plugins/config-schema/CHANGELOG.md +++ b/plugins/config-schema/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-config-schema +## 0.1.38-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + ## 0.1.38-next.0 ### Patch Changes diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json index f7bb416f22..95221632e6 100644 --- a/plugins/config-schema/package.json +++ b/plugins/config-schema/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-config-schema", "description": "A Backstage plugin that lets you browse the configuration schema of your app", - "version": "0.1.38-next.0", + "version": "0.1.38-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 7c69958dde..05d55f3bfe 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-cost-insights +## 0.12.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-cost-insights-common@0.1.1 + ## 0.12.4-next.1 ### Patch Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 6e2bb01ef9..02a86793b0 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-cost-insights", "description": "A Backstage plugin that helps you keep track of your cloud spend", - "version": "0.12.4-next.1", + "version": "0.12.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx index 0a583aae0d..55d5668189 100644 --- a/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx +++ b/plugins/cost-insights/src/components/CostInsightsLayout/CostInsightsLayout.tsx @@ -37,6 +37,7 @@ type CostInsightsLayoutProps = { }; export const CostInsightsLayout = ({ + title = 'Cost Insights', groups, children, }: PropsWithChildren) => { @@ -46,7 +47,7 @@ export const CostInsightsLayout = ({
    diff --git a/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx index 9dda1c65e3..69bf8885e7 100644 --- a/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx +++ b/plugins/cost-insights/src/components/EntityCosts/EntityCosts.tsx @@ -120,7 +120,7 @@ export const EntityCostsCard = () => { return {error.message}; } - if (!dailyCost) { + if (!dailyCost || !dailyCost.aggregation.length) { return No daily costs; } diff --git a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts index 50c3bb1470..046196f26b 100644 --- a/plugins/cost-insights/src/example/templates/CostInsightsClient.ts +++ b/plugins/cost-insights/src/example/templates/CostInsightsClient.ts @@ -22,7 +22,7 @@ // IMPORTANT: Remove the lines below to enable type checking and linting // @ts-nocheck -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ import { CostInsightsApi, diff --git a/plugins/dynatrace/CHANGELOG.md b/plugins/dynatrace/CHANGELOG.md index 60be881c47..e403cf500c 100644 --- a/plugins/dynatrace/CHANGELOG.md +++ b/plugins/dynatrace/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-dynatrace +## 2.0.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 2.0.0-next.1 ### Patch Changes diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json index c7656c52de..4cf4a84c33 100644 --- a/plugins/dynatrace/package.json +++ b/plugins/dynatrace/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-dynatrace", - "version": "2.0.0-next.1", + "version": "2.0.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/entity-feedback-backend/.eslintrc.js b/plugins/entity-feedback-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-feedback-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-feedback-backend/README.md b/plugins/entity-feedback-backend/README.md new file mode 100644 index 0000000000..ef180dbae5 --- /dev/null +++ b/plugins/entity-feedback-backend/README.md @@ -0,0 +1,50 @@ +# Entity Feedback Backend + +Welcome to the entity-feedback backend plugin! + +## Installation + +### Install the package + +```bash +# From your Backstage root directory +yarn add --cwd packages/backend @backstage/plugin-entity-feedback-backend +``` + +### Adding the plugin to your `packages/backend` + +You'll need to add the plugin to the router in your `backend` package. You can do this by creating a file called `packages/backend/src/plugins/entityFeedback.ts` + +```tsx +import { createRouter } from '@backstage/plugin-entity-feedback-backend'; +import { Router } from 'express'; +import { PluginEnvironment } from '../types'; + +export default function createPlugin(env: PluginEnvironment): Promise { + return createRouter({ + database: env.database, + discovery: env.discovery, + identity: env.identity, + logger: env.logger, + }); +} +``` + +With the `entityFeedback.ts` router setup in place, add the router to `packages/backend/src/index.ts`: + +```diff ++import entityFeedback from './plugins/entityFeedback'; + +async function main() { + ... + const createEnv = makeCreateEnv(config); + + const catalogEnv = useHotMemoize(module, () => createEnv('catalog')); ++ const entityFeedbackEnv = useHotMemoize(module, () => createEnv('entityFeedback')); + + const apiRouter = Router(); ++ apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv)); + ... + apiRouter.use(notFoundHandler()); + +``` diff --git a/plugins/entity-feedback-backend/api-report.md b/plugins/entity-feedback-backend/api-report.md new file mode 100644 index 0000000000..3909ac3056 --- /dev/null +++ b/plugins/entity-feedback-backend/api-report.md @@ -0,0 +1,26 @@ +## API Report File for "@backstage/plugin-entity-feedback-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import express from 'express'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import { Logger } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; + +// @public (undocumented) +export function createRouter(options: RouterOptions): Promise; + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + discovery: PluginEndpointDiscovery; + // (undocumented) + identity: IdentityApi; + // (undocumented) + logger: Logger; +} +``` diff --git a/plugins/entity-feedback-backend/migrations/20230109124329_init.js b/plugins/entity-feedback-backend/migrations/20230109124329_init.js new file mode 100644 index 0000000000..42a48d264c --- /dev/null +++ b/plugins/entity-feedback-backend/migrations/20230109124329_init.js @@ -0,0 +1,69 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param { import("knex").Knex } knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('ratings', table => { + table.comment('Ratings table'); + table + .string('entity_ref') + .notNullable() + .comment('The ref of the entity being rated'); + table + .string('user_ref') + .notNullable() + .comment('The user applying the rating'); + table.string('rating').notNullable().comment('The rating value'); + table + .timestamp('timestamp') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('When the rating was recorded'); + }); + + await knex.schema.createTable('responses', table => { + table.comment('Responses table'); + table + .string('entity_ref') + .notNullable() + .comment('The ref of the applicable entity'); + table.string('user_ref').notNullable().comment('The user responding'); + table.text('response').comment('The serialized response'); + table.text('comments').comment('Additional user comments'); + table + .boolean('consent') + .defaultTo(true) + .notNullable() + .comment('Whether user (if recorded) consents to being contacted'); + table + .timestamp('timestamp') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('When the response was recorded'); + }); +}; + +/** + * @param { import("knex").Knex } knex + */ +exports.down = async function down(knex) { + await knex.schema.dropTable('ratings'); + await knex.schema.dropTable('responses'); +}; diff --git a/plugins/entity-feedback-backend/package.json b/plugins/entity-feedback-backend/package.json new file mode 100644 index 0000000000..7fbc7b9f7e --- /dev/null +++ b/plugins/entity-feedback-backend/package.json @@ -0,0 +1,50 @@ +{ + "name": "@backstage/plugin-entity-feedback-backend", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-entity-feedback-common": "workspace:^", + "@types/express": "*", + "express": "^4.18.1", + "express-promise-router": "^4.1.0", + "knex": "^2.0.0", + "node-fetch": "^2.6.7", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.12", + "msw": "^0.49.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/plugins/entity-feedback-backend/src/index.ts b/plugins/entity-feedback-backend/src/index.ts new file mode 100644 index 0000000000..b3c4139eed --- /dev/null +++ b/plugins/entity-feedback-backend/src/index.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Entity Feedback backend plugin + * + * @packageDocumentation + */ +export * from './service/router'; diff --git a/plugins/entity-feedback-backend/src/run.ts b/plugins/entity-feedback-backend/src/run.ts new file mode 100644 index 0000000000..7344f24c17 --- /dev/null +++ b/plugins/entity-feedback-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/entity-feedback-backend/src/service/DatabaseHandler.test.ts b/plugins/entity-feedback-backend/src/service/DatabaseHandler.test.ts new file mode 100644 index 0000000000..c6cde95b3c --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/DatabaseHandler.test.ts @@ -0,0 +1,310 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; +import { Knex } from 'knex'; + +import { DatabaseHandler } from './DatabaseHandler'; + +describe('DatabaseHandler', () => { + const databases = TestDatabases.create(); + + async function createDatabaseHandler(databaseId: TestDatabaseId) { + const knex = await databases.init(databaseId); + return { + knex, + dbHandler: await DatabaseHandler.create({ database: knex }), + }; + } + + describe.each(databases.eachSupportedId())( + '%p', + databaseId => { + let knex: Knex; + let dbHandler: DatabaseHandler; + + beforeEach(async () => { + ({ knex, dbHandler } = await createDatabaseHandler(databaseId)); + }, 30000); + + afterEach(async () => { + // Clean up after each test + await knex('ratings').del(); + await knex('responses').del(); + }, 30000); + + it('getAllRatedEntities', async () => { + await knex('ratings').insert([ + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'DISLIKE', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'LIKE', + }, + { + entity_ref: 'component:default/library', + user_ref: 'user:default/bar', + rating: 'LIKE', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/foo', + rating: 'LIKE', + }, + ]); + + const entities = await dbHandler.getAllRatedEntities(); + expect(entities.length).toEqual(3); + expect(entities).toEqual( + expect.arrayContaining([ + 'component:default/service', + 'component:default/library', + 'component:default/website', + ]), + ); + }); + + it('getRatingsAggregates', async () => { + await knex('ratings').insert([ + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'DISLIKE', + timestamp: '2004-10-19 10:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'LIKE', + timestamp: '2004-11-19 08:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/bar', + rating: 'LIKE', + timestamp: '2004-11-20 08:23:54', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/foo', + rating: 'LIKE', + timestamp: '2004-11-20 08:23:54', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/test', + rating: 'DISLIKE', + timestamp: '2004-11-20 08:23:54', + }, + ]); + + let ratings = await dbHandler.getRatingsAggregates([ + 'component:default/service', + 'component:default/website', + ]); + expect(ratings.length).toEqual(3); + expect(ratings).toEqual( + expect.arrayContaining([ + { + entityRef: 'component:default/service', + rating: 'LIKE', + count: 2, + }, + { + entityRef: 'component:default/website', + rating: 'DISLIKE', + count: 1, + }, + { + entityRef: 'component:default/website', + rating: 'LIKE', + count: 1, + }, + ]), + ); + + ratings = await dbHandler.getRatingsAggregates([ + 'component:default/website', + ]); + expect(ratings.length).toEqual(2); + expect(ratings).toEqual( + expect.arrayContaining([ + { + entityRef: 'component:default/website', + rating: 'DISLIKE', + count: 1, + }, + { + entityRef: 'component:default/website', + rating: 'LIKE', + count: 1, + }, + ]), + ); + }); + + it('recordRating', async () => { + const newRating = { + userRef: 'user:default/me', + entityRef: 'component:default/service', + rating: 'LIKE', + }; + + await dbHandler.recordRating(newRating); + + const ratings = await knex('ratings').select(); + expect(ratings.length).toEqual(1); + expect(ratings[0]).toEqual( + expect.objectContaining({ + user_ref: newRating.userRef, + entity_ref: newRating.entityRef, + rating: newRating.rating, + timestamp: expect.anything(), + }), + ); + }); + + it('getRatings', async () => { + await knex('ratings').insert([ + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'DISLIKE', + timestamp: '2004-10-19 10:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + rating: 'LIKE', + timestamp: '2004-11-19 08:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/bar', + rating: 'LIKE', + timestamp: '2004-11-20 08:23:54', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/foo', + rating: 'LIKE', + timestamp: '2004-11-20 08:23:54', + }, + ]); + + const ratings = await dbHandler.getRatings('component:default/service'); + expect(ratings.length).toEqual(2); + expect(ratings).toEqual( + expect.arrayContaining([ + { userRef: 'user:default/foo', rating: 'LIKE' }, + { userRef: 'user:default/bar', rating: 'LIKE' }, + ]), + ); + }); + + it('recordResponse', async () => { + const newResponse = { + userRef: 'user:default/me', + entityRef: 'component:default/service', + response: 'blah', + comments: 'here is some feedback', + consent: false, + }; + + await dbHandler.recordResponse(newResponse); + + const responses = await knex('responses').select(); + expect(responses.length).toEqual(1); + expect({ + ...responses[0], + consent: Boolean(responses[0].consent), + }).toEqual( + expect.objectContaining({ + user_ref: newResponse.userRef, + entity_ref: newResponse.entityRef, + response: newResponse.response, + comments: newResponse.comments, + consent: newResponse.consent, + timestamp: expect.anything(), + }), + ); + }); + + it('getResponses', async () => { + await knex('responses').insert([ + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + response: 'blah', + comments: 'here is some feedback', + consent: true, + timestamp: '2004-10-19 10:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/foo', + response: 'asdf', + comments: 'here is new feedback', + consent: false, + timestamp: '2004-11-19 08:23:54', + }, + { + entity_ref: 'component:default/service', + user_ref: 'user:default/bar', + response: 'noop', + comments: 'here is different feedback', + consent: true, + timestamp: '2004-11-20 08:23:54', + }, + { + entity_ref: 'component:default/website', + user_ref: 'user:default/foo', + response: 'cool', + comments: 'no comment', + consent: true, + timestamp: '2004-11-20 08:23:54', + }, + ]); + + const responses = await dbHandler.getResponses( + 'component:default/service', + ); + expect(responses.length).toEqual(2); + expect(responses).toEqual( + expect.arrayContaining([ + { + userRef: 'user:default/foo', + response: 'asdf', + comments: 'here is new feedback', + consent: false, + }, + { + userRef: 'user:default/bar', + response: 'noop', + comments: 'here is different feedback', + consent: true, + }, + ]), + ); + }); + }, + 60000, + ); +}); diff --git a/plugins/entity-feedback-backend/src/service/DatabaseHandler.ts b/plugins/entity-feedback-backend/src/service/DatabaseHandler.ts new file mode 100644 index 0000000000..9b81cd94bb --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/DatabaseHandler.ts @@ -0,0 +1,164 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolvePackagePath } from '@backstage/backend-common'; +import { + FeedbackResponse, + Rating, +} from '@backstage/plugin-entity-feedback-common'; +import { Knex } from 'knex'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-entity-feedback-backend', + 'migrations', +); + +type RatingsAggregatesResult = { + entityRef: string; + rating: string; + count: number; +}[]; + +type Options = { + database: Knex; +}; + +/** + * @public + */ +export class DatabaseHandler { + static async create(options: Options): Promise { + const { database } = options; + + await database.migrate.latest({ + directory: migrationsDir, + }); + + return new DatabaseHandler(options); + } + + private readonly database: Knex; + + private constructor(options: Options) { + this.database = options.database; + } + + async getAllRatedEntities(): Promise { + return (await this.database('ratings').distinct('entity_ref')).map( + ({ entity_ref }) => entity_ref, + ); + } + + async getRatingsAggregates( + entityRefs: string[], + ): Promise { + return ( + await this.database('ratings') + .whereIn('ratings.entity_ref', entityRefs) + .innerJoin( + this.database('ratings') + .as('latest_ratings') + .select('entity_ref', 'user_ref') + .max('timestamp', { as: 'timestamp' }) + .whereIn('entity_ref', entityRefs) + .groupBy('entity_ref', 'user_ref'), + function joinClause() { + this.on('ratings.entity_ref', '=', 'latest_ratings.entity_ref') + .andOn('ratings.user_ref', '=', 'latest_ratings.user_ref') + .andOn('ratings.timestamp', '=', 'latest_ratings.timestamp'); + }, + ) + .select('ratings.entity_ref', 'rating') + .count('ratings.user_ref as count') + .groupBy('ratings.entity_ref', 'rating') + ).map(({ entity_ref, rating, count }) => ({ + entityRef: entity_ref as string, + rating: rating as string, + count: Number(count), + })); + } + + async recordRating(rating: Rating) { + await this.database('ratings').insert({ + entity_ref: rating.entityRef, + rating: rating.rating, + user_ref: rating.userRef, + }); + } + + async getRatings(entityRef: string): Promise[]> { + return ( + await this.database('ratings') + .where('entity_ref', entityRef) + .innerJoin( + this.database('ratings') + .as('latest_ratings') + .select('user_ref') + .max('timestamp', { as: 'timestamp' }) + .where('entity_ref', entityRef) + .groupBy('user_ref'), + function joinClause() { + this.on('ratings.user_ref', '=', 'latest_ratings.user_ref').andOn( + 'ratings.timestamp', + '=', + 'latest_ratings.timestamp', + ); + }, + ) + .select('ratings.user_ref', 'rating') + ).map(rating => ({ userRef: rating.user_ref, rating: rating.rating })); + } + + async recordResponse(response: FeedbackResponse) { + await this.database('responses').insert({ + entity_ref: response.entityRef, + response: response.response, + comments: response.comments, + consent: response.consent, + user_ref: response.userRef, + }); + } + + async getResponses( + entityRef: string, + ): Promise[]> { + return ( + await this.database('responses') + .where('entity_ref', entityRef) + .innerJoin( + this.database('responses') + .as('latest_responses') + .select('user_ref') + .max('timestamp', { as: 'timestamp' }) + .where('entity_ref', entityRef) + .groupBy('user_ref'), + function joinClause() { + this.on( + 'responses.user_ref', + '=', + 'latest_responses.user_ref', + ).andOn('responses.timestamp', '=', 'latest_responses.timestamp'); + }, + ) + .select('responses.user_ref', 'response', 'comments', 'consent') + ).map(response => ({ + userRef: response.user_ref, + response: response.response, + comments: response.comments, + consent: Boolean(response.consent), + })); + } +} diff --git a/plugins/entity-feedback-backend/src/service/router.test.ts b/plugins/entity-feedback-backend/src/service/router.test.ts new file mode 100644 index 0000000000..74fae8248b --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/router.test.ts @@ -0,0 +1,290 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DatabaseManager, + getVoidLogger, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { IdentityApi } from '@backstage/plugin-auth-node'; +import express from 'express'; +import request from 'supertest'; + +import { createRouter } from './router'; + +const sampleOwnedEntities = [ + { + kind: 'component', + metadata: { + namespace: 'default', + name: 'foo', + title: 'Foo Component', + }, + }, + { + kind: 'component', + metadata: { + namespace: 'default', + name: 'bar', + title: 'Bar Component', + }, + }, +]; + +const sampleEntities = [ + ...sampleOwnedEntities, + { + kind: 'user', + metadata: { + namespace: 'default', + name: 'foo', + }, + }, + null, + { + kind: 'user', + metadata: { + namespace: 'default', + name: 'bar', + }, + }, +]; + +const mockGetEntties = jest + .fn() + .mockImplementation(async () => ({ items: sampleOwnedEntities })); + +const mockGetEnttiesByRefs = jest + .fn() + .mockImplementation(async () => ({ items: sampleEntities })); + +jest.mock('@backstage/catalog-client', () => ({ + CatalogClient: jest.fn().mockImplementation(() => ({ + getEntities: mockGetEntties, + getEntitiesByRefs: mockGetEnttiesByRefs, + })), +})); + +jest.mock('@backstage/plugin-auth-node', () => ({ + getBearerTokenFromAuthorizationHeader: () => 'token', +})); + +const mockRatings = [ + { userRef: 'user:default/foo', rating: 'LIKE' }, + { userRef: 'user:default/bar', rating: 'LIKE' }, + { userRef: 'user:default/test', rating: 'DISLIKE' }, +]; + +const mockResponses = [ + { + userRef: 'user:default/foo', + response: 'asdf', + comments: 'here is new feedback', + consent: false, + }, + { + userRef: 'user:default/bar', + response: 'noop', + comments: 'here is different feedback', + consent: true, + }, + { + userRef: 'user:default/test', + response: 'err', + comments: 'no comment', + consent: false, + }, +]; + +const mockDbHandler = { + getAllRatedEntities: jest + .fn() + .mockImplementation(async () => [ + 'component:default/foo', + 'component:default/bar', + 'component:default/test', + ]), + getRatingsAggregates: jest.fn().mockImplementation(async () => [ + { entityRef: 'component:default/foo', rating: 'LIKE', count: 3 }, + { entityRef: 'component:default/foo', rating: 'DISLIKE', count: 1 }, + { entityRef: 'component:default/bar', rating: 'LIKE', count: 5 }, + ]), + recordRating: jest.fn().mockImplementation(async () => {}), + getRatings: jest.fn().mockImplementation(async () => mockRatings), + recordResponse: jest.fn().mockImplementation(async () => {}), + getResponses: jest.fn().mockImplementation(async () => mockResponses), +}; + +jest.mock('./DatabaseHandler', () => ({ + DatabaseHandler: { create: async () => mockDbHandler }, +})); + +describe('createRouter', () => { + let app: express.Express; + + const createDatabase = () => + DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('entity-feedback'); + + const mockIdentityClient = { + getIdentity: jest.fn().mockImplementation(async () => ({ + identity: { userEntityRef: 'user:default/me' }, + })), + } as unknown as IdentityApi; + + const discovery: jest.Mocked = { + getBaseUrl: jest.fn(), + getExternalBaseUrl: jest.fn(), + }; + + beforeEach(async () => { + const router = await createRouter({ + database: createDatabase(), + discovery, + identity: mockIdentityClient, + logger: getVoidLogger(), + }); + + app = express().use(router); + jest.clearAllMocks(); + }); + + describe('GET /ratings', () => { + it('should get ratings for all entities correctly', async () => { + const response = await request(app).get('/ratings').send(); + + expect(mockDbHandler.getAllRatedEntities).toHaveBeenCalled(); + expect(mockDbHandler.getRatingsAggregates).toHaveBeenCalledWith( + sampleEntities + .filter(Boolean) + .map((ent: any) => stringifyEntityRef(ent)), + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo Component', + ratings: { LIKE: 3, DISLIKE: 1 }, + }, + { + entityRef: 'component:default/bar', + entityTitle: 'Bar Component', + ratings: { LIKE: 5 }, + }, + ]); + }); + + it('should get ratings for all owned entities correctly', async () => { + const response = await request(app) + .get('/ratings?ownerRef=group:default/test-team') + .send(); + + expect(mockGetEntties).toHaveBeenCalledWith( + expect.objectContaining({ + filter: { 'relations.ownedBy': 'group:default/test-team' }, + }), + { token: 'token' }, + ); + expect(mockDbHandler.getAllRatedEntities).not.toHaveBeenCalled(); + expect(mockDbHandler.getRatingsAggregates).toHaveBeenCalledWith( + sampleOwnedEntities.map((ent: any) => stringifyEntityRef(ent)), + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual([ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo Component', + ratings: { LIKE: 3, DISLIKE: 1 }, + }, + { + entityRef: 'component:default/bar', + entityTitle: 'Bar Component', + ratings: { LIKE: 5 }, + }, + ]); + }); + }); + + describe('POST /ratings/:entityRef', () => { + it('should record a rating correctly', async () => { + const body = { rating: 'LIKE' }; + const response = await request(app) + .post('/ratings/component%3Adefault%2Fservice') + .send(body); + expect(mockDbHandler.recordRating).toHaveBeenCalledWith({ + entityRef: 'component:default/service', + userRef: 'user:default/me', + ...body, + }); + expect(response.status).toEqual(201); + }); + }); + + describe('GET /ratings/:entityRef', () => { + it('should get ratings for an entity correctly', async () => { + const response = await request(app) + .get('/ratings/component%3Adefault%2Fservice') + .send(); + expect(mockDbHandler.getRatings).toHaveBeenCalledWith( + 'component:default/service', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual( + mockRatings.filter(r => r.userRef !== 'user:default/test'), + ); + }); + }); + + describe('POST /responses/:entityRef', () => { + it('should record a response correctly', async () => { + const body = { response: 'blah', comments: 'feedback', consent: true }; + const response = await request(app) + .post('/responses/component%3Adefault%2Fservice') + .send(body); + expect(mockDbHandler.recordResponse).toHaveBeenCalledWith({ + entityRef: 'component:default/service', + userRef: 'user:default/me', + ...body, + }); + expect(response.status).toEqual(201); + }); + }); + + describe('GET /responses/:entityRef', () => { + it('should get responses for an entity correctly', async () => { + const response = await request(app) + .get('/responses/component%3Adefault%2Fservice') + .send(); + expect(mockDbHandler.getResponses).toHaveBeenCalledWith( + 'component:default/service', + ); + expect(response.status).toEqual(200); + expect(response.body).toEqual( + mockResponses.filter(r => r.userRef !== 'user:default/test'), + ); + }); + }); +}); diff --git a/plugins/entity-feedback-backend/src/service/router.ts b/plugins/entity-feedback-backend/src/service/router.ts new file mode 100644 index 0000000000..e1243cedaf --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/router.ts @@ -0,0 +1,215 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + errorHandler, + PluginDatabaseManager, + PluginEndpointDiscovery, +} from '@backstage/backend-common'; +import { CatalogClient } from '@backstage/catalog-client'; +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { + getBearerTokenFromAuthorizationHeader, + IdentityApi, +} from '@backstage/plugin-auth-node'; +import { EntityRatingsData } from '@backstage/plugin-entity-feedback-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; + +import { DatabaseHandler } from './DatabaseHandler'; + +/** + * @public + */ +export interface RouterOptions { + database: PluginDatabaseManager; + discovery: PluginEndpointDiscovery; + identity: IdentityApi; + logger: Logger; +} + +/** + * @public + */ +export async function createRouter( + options: RouterOptions, +): Promise { + const { database, discovery, identity, logger } = options; + + logger.info('Initializing Entity Feedback backend'); + + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + const db = await database.getClient(); + const dbHandler = await DatabaseHandler.create({ database: db }); + + const router = Router(); + router.use(express.json()); + + router.get('/ratings', async (req, res) => { + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + const requestedEntities: { [ref: string]: Entity } = {}; + if (req.query.ownerRef) { + // Get ratings from all owned entities (also ensures only accessible entities are requested) + ( + await catalogClient.getEntities( + { + filter: { 'relations.ownedBy': req.query.ownerRef as string }, + fields: [ + 'kind', + 'metadata.name', + 'metadata.namespace', + 'metadata.title', + ], + }, + { token }, + ) + ).items.forEach(ent => { + requestedEntities[stringifyEntityRef(ent)] = ent; + }); + } else { + const allRatedEntities = await dbHandler.getAllRatedEntities(); + + // Filter entities to only expose entity refs accessible by current user + ( + await catalogClient.getEntitiesByRefs( + { + entityRefs: allRatedEntities, + fields: [ + 'kind', + 'metadata.namespace', + 'metadata.name', + 'metadata.title', + ], + }, + { token }, + ) + ).items + .filter(Boolean) + .forEach(ent => { + requestedEntities[stringifyEntityRef(ent!)] = ent!; + }); + } + + const entityRatings = await dbHandler.getRatingsAggregates( + Object.keys(requestedEntities), + ); + + // Merge rating aggregates into a condensed per entity structure + const entityRatingsMap = entityRatings.reduce<{ + [ref: string]: EntityRatingsData; + }>((ratingsMap, { entityRef, rating, count }) => { + ratingsMap[entityRef] = ratingsMap[entityRef] ?? { + entityRef, + entityTitle: requestedEntities[entityRef].metadata.title, + ratings: {}, + }; + ratingsMap[entityRef].ratings[rating] = count; + return ratingsMap; + }, {}); + + res.json(Object.values(entityRatingsMap)); + }); + + router.post('/ratings/:entityRef', async (req, res) => { + const user = await identity.getIdentity({ request: req }); + const rating = req.body.rating; + if (!user || !rating) { + res.status(400).end(); + return; + } + + await dbHandler.recordRating({ + entityRef: req.params.entityRef, + rating, + userRef: user.identity.userEntityRef, + }); + + res.status(201).end(); + }); + + router.get('/ratings/:entityRef', async (req, res) => { + const ratings = await dbHandler.getRatings(req.params.entityRef); + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + // Filter ratings via user refs to only expose entity refs accessible by current user + const accessibleEntityRefs = ( + await catalogClient.getEntitiesByRefs( + { + entityRefs: ratings.map(r => r.userRef), + fields: ['kind', 'metadata.namespace', 'metadata.name'], + }, + { token }, + ) + ).items + .filter(Boolean) + .map(ent => stringifyEntityRef(ent!)); + + res.json(ratings.filter(r => accessibleEntityRefs.includes(r.userRef))); + }); + + router.post('/responses/:entityRef', async (req, res) => { + const user = await identity.getIdentity({ request: req }); + const { response, comments, consent } = req.body; + + if (!user) { + res.status(400).end(); + return; + } + + await dbHandler.recordResponse({ + entityRef: req.params.entityRef, + response, + comments, + consent, + userRef: user.identity.userEntityRef, + }); + + res.status(201).end(); + }); + + router.get('/responses/:entityRef', async (req, res) => { + const responses = await dbHandler.getResponses(req.params.entityRef); + + const token = getBearerTokenFromAuthorizationHeader( + req.header('authorization'), + ); + + // Filter responses via user refs to only expose entity refs accessible by current user + const accessibleEntityRefs = ( + await catalogClient.getEntitiesByRefs( + { + entityRefs: responses.map(r => r.userRef), + fields: ['kind', 'metadata.namespace', 'metadata.name'], + }, + { token }, + ) + ).items + .filter(Boolean) + .map(ent => stringifyEntityRef(ent!)); + + res.json(responses.filter(r => accessibleEntityRefs.includes(r.userRef))); + }); + + router.use(errorHandler()); + return router; +} diff --git a/plugins/entity-feedback-backend/src/service/standaloneServer.ts b/plugins/entity-feedback-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..9395c72141 --- /dev/null +++ b/plugins/entity-feedback-backend/src/service/standaloneServer.ts @@ -0,0 +1,80 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createServiceBuilder, + DatabaseManager, + loadBackendConfig, + SingleHostDiscovery, + useHotMemoize, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { DefaultIdentityClient } from '@backstage/plugin-auth-node'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'entity-feedback-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }); + const discovery = SingleHostDiscovery.fromConfig(config); + + const database = useHotMemoize(module, () => { + const manager = DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { client: 'better-sqlite3', connection: ':memory:' }, + }, + }), + ); + return manager.forPlugin('entity-feedback'); + }); + + const identity = DefaultIdentityClient.create({ + discovery, + issuer: await discovery.getExternalBaseUrl('auth'), + }); + + logger.debug('Starting application server...'); + const router = await createRouter({ + database, + discovery, + identity, + logger, + }); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/entity-feedback', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/entity-feedback-backend/src/setupTests.ts b/plugins/entity-feedback-backend/src/setupTests.ts new file mode 100644 index 0000000000..aa70772592 --- /dev/null +++ b/plugins/entity-feedback-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export {}; diff --git a/plugins/entity-feedback-common/.eslintrc.js b/plugins/entity-feedback-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-feedback-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-feedback-common/README.md b/plugins/entity-feedback-common/README.md new file mode 100644 index 0000000000..430d14aeab --- /dev/null +++ b/plugins/entity-feedback-common/README.md @@ -0,0 +1,3 @@ +# Entity Feedback Common + +Common types for the entity-feedback plugin. diff --git a/plugins/entity-feedback-common/api-report.md b/plugins/entity-feedback-common/api-report.md new file mode 100644 index 0000000000..118e6c78f2 --- /dev/null +++ b/plugins/entity-feedback-common/api-report.md @@ -0,0 +1,41 @@ +## API Report File for "@backstage/plugin-entity-feedback-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +export interface EntityRatingsData { + // (undocumented) + entityRef: string; + // (undocumented) + entityTitle?: string; + // (undocumented) + ratings: { + [ratingValue: string]: number; + }; +} + +// @public (undocumented) +export interface FeedbackResponse { + // (undocumented) + comments?: string; + // (undocumented) + consent?: boolean; + // (undocumented) + entityRef: string; + // (undocumented) + response?: string; + // (undocumented) + userRef: string; +} + +// @public (undocumented) +export interface Rating { + // (undocumented) + entityRef: string; + // (undocumented) + rating: string; + // (undocumented) + userRef: string; +} +``` diff --git a/plugins/entity-feedback-common/package.json b/plugins/entity-feedback-common/package.json new file mode 100644 index 0000000000..6316b72943 --- /dev/null +++ b/plugins/entity-feedback-common/package.json @@ -0,0 +1,31 @@ +{ + "name": "@backstage/plugin-entity-feedback-common", + "description": "Common functionalities for the entity-feedback plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/entity-feedback-common/src/index.ts b/plugins/entity-feedback-common/src/index.ts new file mode 100644 index 0000000000..eea289a899 --- /dev/null +++ b/plugins/entity-feedback-common/src/index.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Common functionalities for the entity-feedback plugin. + * + * @packageDocumentation + */ + +/** + * @public + */ +export interface Rating { + entityRef: string; + rating: string; + userRef: string; +} + +/** + * @public + */ +export interface FeedbackResponse { + entityRef: string; + response?: string; + comments?: string; + consent?: boolean; + userRef: string; +} + +/** + * @public + */ +export interface EntityRatingsData { + entityRef: string; + entityTitle?: string; + ratings: { + [ratingValue: string]: number; + }; +} diff --git a/plugins/entity-feedback-common/src/setupTests.ts b/plugins/entity-feedback-common/src/setupTests.ts new file mode 100644 index 0000000000..4b9026cde5 --- /dev/null +++ b/plugins/entity-feedback-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export {}; diff --git a/plugins/entity-feedback/.eslintrc.js b/plugins/entity-feedback/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-feedback/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-feedback/README.md b/plugins/entity-feedback/README.md new file mode 100644 index 0000000000..fdfa4761d6 --- /dev/null +++ b/plugins/entity-feedback/README.md @@ -0,0 +1,136 @@ +# Entity Feedback Plugin + +Welcome to the entity-feedback plugin! + +This plugin allows you give and view feedback on entities available in the Backstage catalog. + +## Features + +### Rate entities + +#### Like/Dislike rating + +![Like dislike rating example](./docs/like-dislike-rating.png) + +#### Starred rating + +![Starred rating example](./docs/starred-rating.png) + +### Request additional feedback when poorly rated + +![Response dialog example](./docs/feedback-response-dialog.png) + +### View entity feedback responses + +![Feedback responses example](./docs/feedback-response-table.png) + +### View aggregated ratings on owned entities + +#### Total likes/dislikes + +![Like dislike table example](./docs/like-dislike-table.png) + +#### Star breakdowns + +![Starred rating table example](./docs/starred-rating-table.png) + +## Setup + +The following sections will help you get the Entity Feedback plugin setup and running + +### Backend + +You need to setup the [Entity Feedback backend plugin](https://github.com/backstage/backstage/tree/master/plugins/entity-feedback-backend) before you move forward with any of these steps if you haven't already + +### Installation + +Install this plugin: + +```bash +# From your Backstage root directory +yarn --cwd packages/app add @backstage/plugin-entity-feedback +``` + +### Entity Pages + +Add rating and feedback components to your `EntityPage.tsx` to hook up UI so that users +can rate your entities and for owners to view feedback/responses. + +To allow users to apply "Like" and "Dislike" ratings add the following to each kind/type of +entity in your `EntityPage.tsx` you want to be rated (if you prefer to use star ratings, replace +`EntityLikeDislikeRatingsCard` with `EntityStarredRatingsCard` and `LikeDislikeButtons` with +`StarredRatingButtons`): + +```diff +import { + ... ++ InfoCard, + ... +} from '@backstage/core-components'; ++import { ++ EntityFeedbackResponseContent, ++ EntityLikeDislikeRatingsCard, ++ LikeDislikeButtons, ++} from '@backstage/plugin-entity-feedback'; + +// Add to each applicable kind/type of entity as desired +const overviewContent = ( + + ... ++ ++ ++ ++ ++ + ... + +); + +... + +// Add to each applicable kind/type of entity as desired +const serviceEntityPage = ( + + ... ++ ++ ++ + ... + +); + +... + +// Add ratings card component to user/group entities to view ratings of owned entities +const userPage = ( + + + + ... ++ ++ ++ + ... + + + + +); + +const groupPage = ( + + + + ... ++ ++ ++ + ... + + + + +); +``` + +Note: For a full example of this you can look at [this EntityPage](../../packages/app/src/components/catalog/EntityPage.tsx) diff --git a/plugins/entity-feedback/api-report.md b/plugins/entity-feedback/api-report.md new file mode 100644 index 0000000000..06b0166b0d --- /dev/null +++ b/plugins/entity-feedback/api-report.md @@ -0,0 +1,184 @@ +## API Report File for "@backstage/plugin-entity-feedback" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +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 { EntityRatingsData } from '@backstage/plugin-entity-feedback-common'; +import { FeedbackResponse } from '@backstage/plugin-entity-feedback-common'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { Rating } from '@backstage/plugin-entity-feedback-common'; +import { ReactNode } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export interface EntityFeedbackApi { + // (undocumented) + getAllRatings(): Promise; + // (undocumented) + getOwnedRatings(ownerRef: string): Promise; + // (undocumented) + getRatings(entityRef: string): Promise[]>; + // (undocumented) + getResponses( + entityRef: string, + ): Promise[]>; + // (undocumented) + recordRating(entityRef: string, rating: string): Promise; + // (undocumented) + recordResponse( + entityRef: string, + response: Omit, + ): Promise; +} + +// @public (undocumented) +export const entityFeedbackApiRef: ApiRef; + +// @public (undocumented) +export class EntityFeedbackClient implements EntityFeedbackApi { + constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }); + // (undocumented) + getAllRatings(): Promise; + // (undocumented) + getOwnedRatings(ownerRef: string): Promise; + // (undocumented) + getRatings(entityRef: string): Promise[]>; + // (undocumented) + getResponses( + entityRef: string, + ): Promise[]>; + // (undocumented) + recordRating(entityRef: string, rating: string): Promise; + // (undocumented) + recordResponse( + entityRef: string, + response: Omit, + ): Promise; +} + +// @public (undocumented) +export const entityFeedbackPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {}, + {} +>; + +// @public (undocumented) +export interface EntityFeedbackResponse { + // (undocumented) + id: string; + // (undocumented) + label: string; +} + +// @public (undocumented) +export const EntityFeedbackResponseContent: () => JSX.Element; + +// @public (undocumented) +export const EntityLikeDislikeRatingsCard: () => JSX.Element; + +// @public (undocumented) +export const EntityStarredRatingsCard: () => JSX.Element; + +// @public (undocumented) +export const FeedbackResponseDialog: ( + props: FeedbackResponseDialogProps, +) => JSX.Element; + +// @public (undocumented) +export interface FeedbackResponseDialogProps { + // (undocumented) + entity: Entity; + // (undocumented) + feedbackDialogResponses?: EntityFeedbackResponse[]; + // (undocumented) + feedbackDialogTitle?: ReactNode; + // (undocumented) + onClose: () => void; + // (undocumented) + open: boolean; +} + +// @public (undocumented) +export const FeedbackResponseTable: ( + props: FeedbackResponseTableProps, +) => JSX.Element; + +// @public (undocumented) +export interface FeedbackResponseTableProps { + // (undocumented) + entityRef: string; + // (undocumented) + title?: string; +} + +// @public (undocumented) +export const LikeDislikeButtons: ( + props: LikeDislikeButtonsProps, +) => JSX.Element; + +// @public (undocumented) +export interface LikeDislikeButtonsProps { + // (undocumented) + feedbackDialogResponses?: EntityFeedbackResponse[]; + // (undocumented) + feedbackDialogTitle?: ReactNode; + // (undocumented) + requestResponse?: boolean; +} + +// @public (undocumented) +export const LikeDislikeRatingsTable: ( + props: LikeDislikeRatingsTableProps, +) => JSX.Element; + +// @public (undocumented) +export interface LikeDislikeRatingsTableProps { + // (undocumented) + allEntities?: boolean; + // (undocumented) + ownerRef?: string; + // (undocumented) + title?: string; +} + +// @public (undocumented) +export const StarredRatingButtons: ( + props: StarredRatingButtonsProps, +) => JSX.Element; + +// @public (undocumented) +export interface StarredRatingButtonsProps { + // (undocumented) + feedbackDialogResponses?: EntityFeedbackResponse[]; + // (undocumented) + feedbackDialogTitle?: ReactNode; + // (undocumented) + requestResponse?: boolean; + // (undocumented) + requestResponseThreshold?: number; +} + +// @public (undocumented) +export const StarredRatingsTable: ( + props: StarredRatingsTableProps, +) => JSX.Element; + +// @public (undocumented) +export interface StarredRatingsTableProps { + // (undocumented) + allEntities?: boolean; + // (undocumented) + ownerRef?: string; + // (undocumented) + title?: string; +} +``` diff --git a/plugins/entity-feedback/docs/feedback-response-dialog.png b/plugins/entity-feedback/docs/feedback-response-dialog.png new file mode 100644 index 0000000000..74270c805a Binary files /dev/null and b/plugins/entity-feedback/docs/feedback-response-dialog.png differ diff --git a/plugins/entity-feedback/docs/feedback-response-table.png b/plugins/entity-feedback/docs/feedback-response-table.png new file mode 100644 index 0000000000..3795ac809d Binary files /dev/null and b/plugins/entity-feedback/docs/feedback-response-table.png differ diff --git a/plugins/entity-feedback/docs/like-dislike-rating.png b/plugins/entity-feedback/docs/like-dislike-rating.png new file mode 100644 index 0000000000..0267218243 Binary files /dev/null and b/plugins/entity-feedback/docs/like-dislike-rating.png differ diff --git a/plugins/entity-feedback/docs/like-dislike-table.png b/plugins/entity-feedback/docs/like-dislike-table.png new file mode 100644 index 0000000000..df38883d25 Binary files /dev/null and b/plugins/entity-feedback/docs/like-dislike-table.png differ diff --git a/plugins/entity-feedback/docs/starred-rating-table.png b/plugins/entity-feedback/docs/starred-rating-table.png new file mode 100644 index 0000000000..e678c5b68a Binary files /dev/null and b/plugins/entity-feedback/docs/starred-rating-table.png differ diff --git a/plugins/entity-feedback/docs/starred-rating.png b/plugins/entity-feedback/docs/starred-rating.png new file mode 100644 index 0000000000..6198f7ae66 Binary files /dev/null and b/plugins/entity-feedback/docs/starred-rating.png differ diff --git a/plugins/entity-feedback/package.json b/plugins/entity-feedback/package.json new file mode 100644 index 0000000000..f8560a9237 --- /dev/null +++ b/plugins/entity-feedback/package.json @@ -0,0 +1,63 @@ +{ + "name": "@backstage/plugin-entity-feedback", + "version": "0.0.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/entity-feedback" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-entity-feedback-common": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "@types/react": "^16.13.1 || ^17.0.0", + "react": "^16.13.1 || ^17.0.0", + "react-router-dom": "6.0.0-beta.0 || ^6.3.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/react-hooks": "^8.0.1", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.49.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/entity-feedback/src/api/EntityFeedbackApi.ts b/plugins/entity-feedback/src/api/EntityFeedbackApi.ts new file mode 100644 index 0000000000..d2f1a46f55 --- /dev/null +++ b/plugins/entity-feedback/src/api/EntityFeedbackApi.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core-plugin-api'; +import { + EntityRatingsData, + FeedbackResponse, + Rating, +} from '@backstage/plugin-entity-feedback-common'; + +/** + * @public + */ +export const entityFeedbackApiRef = createApiRef({ + id: 'plugin.entity-feedback.service', +}); + +/** + * @public + */ +export interface EntityFeedbackApi { + getAllRatings(): Promise; + + getOwnedRatings(ownerRef: string): Promise; + + recordRating(entityRef: string, rating: string): Promise; + + getRatings(entityRef: string): Promise[]>; + + recordResponse( + entityRef: string, + response: Omit, + ): Promise; + + getResponses( + entityRef: string, + ): Promise[]>; +} diff --git a/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts b/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts new file mode 100644 index 0000000000..694fd66b06 --- /dev/null +++ b/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts @@ -0,0 +1,175 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { MockFetchApi, setupRequestMockHandlers } from '@backstage/test-utils'; +import { rest } from 'msw'; +import { setupServer } from 'msw/node'; + +import { EntityFeedbackClient } from './EntityFeedbackClient'; + +const server = setupServer(); + +describe('EntityFeedbackClient', () => { + setupRequestMockHandlers(server); + const mockBaseUrl = 'http://backstage/api/entity-feedback'; + const discoveryApi = { getBaseUrl: async () => mockBaseUrl }; + const fetchApi = new MockFetchApi(); + + let client: EntityFeedbackClient; + beforeEach(() => { + client = new EntityFeedbackClient({ discoveryApi, fetchApi }); + }); + + it('getAllRatings', async () => { + const ratings = [ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo', + ratings: { LIKE: 10 }, + }, + { + entityRef: 'component:default/bar', + entityTitle: 'Bar', + ratings: { DISLIKE: 10 }, + }, + ]; + + server.use( + rest.get(`${mockBaseUrl}/ratings`, (_, res, ctx) => + res(ctx.json(ratings)), + ), + ); + + const response = await client.getAllRatings(); + expect(response).toEqual(ratings); + }); + + it('getOwnedRatings', async () => { + const ratings = [ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo', + ratings: { LIKE: 10 }, + }, + { + entityRef: 'component:default/bar', + entityTitle: 'Bar', + ratings: { DISLIKE: 10 }, + }, + ]; + + server.use( + rest.get( + `${mockBaseUrl}/ratings?ownerRef=${encodeURIComponent( + 'group:default/team', + )}`, + (_, res, ctx) => res(ctx.json(ratings)), + ), + ); + + const response = await client.getOwnedRatings('group:default/team'); + expect(response).toEqual(ratings); + }); + + it('recordRating', async () => { + expect.assertions(1); + + server.use( + rest.post( + `${mockBaseUrl}/ratings/${encodeURIComponent( + 'component:default/service', + )}`, + (req, res) => { + expect(req.body).toEqual({ rating: 'LIKE' }); + return res(); + }, + ), + ); + + await client.recordRating('component:default/service', 'LIKE'); + }); + + it('getRatings', async () => { + const ratings = [ + { userRef: 'user:default/foo', rating: 'LIKE' }, + { userRef: 'user:default/bar', rating: 'LIKE' }, + ]; + + server.use( + rest.get( + `${mockBaseUrl}/ratings/${encodeURIComponent( + 'component:default/service', + )}`, + (_, res, ctx) => res(ctx.json(ratings)), + ), + ); + + const response = await client.getRatings('component:default/service'); + expect(response).toEqual(ratings); + }); + + it('recordResponse', async () => { + expect.assertions(1); + const response = { + response: 'blah', + comments: 'feedback', + consent: false, + }; + + server.use( + rest.post( + `${mockBaseUrl}/responses/${encodeURIComponent( + 'component:default/service', + )}`, + (req, res) => { + expect(req.body).toEqual(response); + return res(); + }, + ), + ); + + await client.recordResponse('component:default/service', response); + }); + + it('getResponses', async () => { + const responses = [ + { + userRef: 'user:default/foo', + response: 'asdf', + comments: 'here is new feedback', + consent: false, + }, + { + userRef: 'user:default/bar', + response: 'noop', + comments: 'here is different feedback', + consent: true, + }, + ]; + + server.use( + rest.get( + `${mockBaseUrl}/responses/${encodeURIComponent( + 'component:default/service', + )}`, + (_, res, ctx) => res(ctx.json(responses)), + ), + ); + + const response = await client.getResponses('component:default/service'); + expect(response).toEqual(responses); + }); +}); diff --git a/plugins/entity-feedback/src/api/EntityFeedbackClient.ts b/plugins/entity-feedback/src/api/EntityFeedbackClient.ts new file mode 100644 index 0000000000..34856473c8 --- /dev/null +++ b/plugins/entity-feedback/src/api/EntityFeedbackClient.ts @@ -0,0 +1,136 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DiscoveryApi, FetchApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { + EntityRatingsData, + FeedbackResponse, + Rating, +} from '@backstage/plugin-entity-feedback-common'; + +import { EntityFeedbackApi } from './EntityFeedbackApi'; + +/** + * @public + */ +export class EntityFeedbackClient implements EntityFeedbackApi { + private readonly discoveryApi: DiscoveryApi; + private readonly fetchApi: FetchApi; + + constructor(options: { discoveryApi: DiscoveryApi; fetchApi: FetchApi }) { + this.discoveryApi = options.discoveryApi; + this.fetchApi = options.fetchApi; + } + + async getAllRatings(): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch(`${baseUrl}/ratings`, { + method: 'GET', + }); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + + return resp.json(); + } + + async getOwnedRatings(ownerRef: string): Promise { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/ratings?ownerRef=${encodeURIComponent(ownerRef)}`, + { + method: 'GET', + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + + return resp.json(); + } + + async recordRating(entityRef: string, rating: string) { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/ratings/${encodeURIComponent(entityRef)}`, + { + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + body: JSON.stringify({ rating }), + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + } + + async getRatings(entityRef: string): Promise[]> { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/ratings/${encodeURIComponent(entityRef)}`, + { + method: 'GET', + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + + return resp.json(); + } + + async recordResponse( + entityRef: string, + response: Omit, + ) { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/responses/${encodeURIComponent(entityRef)}`, + { + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + body: JSON.stringify(response), + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + } + + async getResponses( + entityRef: string, + ): Promise[]> { + const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback'); + const resp = await this.fetchApi.fetch( + `${baseUrl}/responses/${encodeURIComponent(entityRef)}`, + { + method: 'GET', + }, + ); + + if (!resp.ok) { + throw await ResponseError.fromResponse(resp); + } + + return resp.json(); + } +} diff --git a/plugins/entity-feedback/src/api/index.ts b/plugins/entity-feedback/src/api/index.ts new file mode 100644 index 0000000000..32c8624425 --- /dev/null +++ b/plugins/entity-feedback/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './EntityFeedbackClient'; +export * from './EntityFeedbackApi'; diff --git a/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.test.tsx b/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.test.tsx new file mode 100644 index 0000000000..b639b2e727 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.test.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackRatingsTable } from './FeedbackRatingsTable'; + +describe('FeedbackRatingsTable', () => { + const sampleRatings = [ + { + entityRef: 'component:default/foo', + entityTitle: 'Foo Component', + ratings: { 'Rating 1': 3, 'Rating 2': 1 }, + }, + { + entityRef: 'system:default/bar', + entityTitle: 'Bar Component', + ratings: { 'Rating 1': 5 }, + }, + { + entityRef: 'domain:default/hello-world', + entityTitle: 'Hello World', + ratings: { 'Rating 3': 5 }, + }, + ]; + + const feedbackApi: Partial = { + getAllRatings: jest.fn().mockImplementation(async () => sampleRatings), + getOwnedRatings: jest.fn().mockImplementation(async () => sampleRatings), + }; + + const sampleRatingValues = ['Rating 1', 'Rating 2']; + + const render = async (props: any = {}) => + renderInTestApp( + + + , + { + mountedRoutes: { '/catalog/:namespace/:kind/:name': entityRouteRef }, + }, + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders all ratings correctly', async () => { + const rendered = await render({ allEntities: true }); + + expect(feedbackApi.getAllRatings).toHaveBeenCalled(); + expect(feedbackApi.getOwnedRatings).not.toHaveBeenCalled(); + + expect(rendered.getByText('Entity Ratings')).toBeInTheDocument(); + + // Columns + expect(rendered.getByText('Entity')).toBeInTheDocument(); + expect(rendered.getByText('Rating 1')).toBeInTheDocument(); + expect(rendered.getByText('Rating 2')).toBeInTheDocument(); + + // Rows + expect(rendered.getByText('Foo Component')).toBeInTheDocument(); + expect(rendered.getByText('component')).toBeInTheDocument(); + expect(rendered.getByText('3')).toBeInTheDocument(); + expect(rendered.getByText('1')).toBeInTheDocument(); + expect(rendered.getByText('Bar Component')).toBeInTheDocument(); + expect(rendered.getByText('system')).toBeInTheDocument(); + expect(rendered.getByText('5')).toBeInTheDocument(); + + expect(rendered.queryByText('Hello World')).toBeNull(); + }); + + it('renders owned entity ratings correctly', async () => { + const rendered = await render({ + ownerRef: 'group:default/test-team', + title: 'Custom Title', + }); + + expect(feedbackApi.getAllRatings).not.toHaveBeenCalled(); + expect(feedbackApi.getOwnedRatings).toHaveBeenCalledWith( + 'group:default/test-team', + ); + + expect(rendered.getByText('Custom Title')).toBeInTheDocument(); + + // Columns + expect(rendered.getByText('Entity')).toBeInTheDocument(); + expect(rendered.getByText('Rating 1')).toBeInTheDocument(); + expect(rendered.getByText('Rating 2')).toBeInTheDocument(); + + // Rows + expect(rendered.getByText('Foo Component')).toBeInTheDocument(); + expect(rendered.getByText('component')).toBeInTheDocument(); + expect(rendered.getByText('3')).toBeInTheDocument(); + expect(rendered.getByText('1')).toBeInTheDocument(); + expect(rendered.getByText('Bar Component')).toBeInTheDocument(); + expect(rendered.getByText('system')).toBeInTheDocument(); + expect(rendered.getByText('5')).toBeInTheDocument(); + + expect(rendered.queryByText('Hello World')).toBeNull(); + }); +}); diff --git a/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.tsx b/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.tsx new file mode 100644 index 0000000000..48b2018729 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackRatingsTable/FeedbackRatingsTable.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { parseEntityRef } from '@backstage/catalog-model'; +import { ErrorPanel, SubvalueCell, Table } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { EntityRefLink } from '@backstage/plugin-catalog-react'; +import { EntityRatingsData } from '@backstage/plugin-entity-feedback-common'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; + +import { entityFeedbackApiRef } from '../../api'; + +interface FeedbackRatingsTableProps { + allEntities?: boolean; + ownerRef?: string; + ratingValues: string[]; + title?: string; +} + +export const FeedbackRatingsTable = (props: FeedbackRatingsTableProps) => { + const { + allEntities, + ownerRef, + ratingValues, + title = 'Entity Ratings', + } = props; + const feedbackApi = useApi(entityFeedbackApiRef); + + const { + error, + loading, + value: ratings, + } = useAsync(async () => { + if (allEntities) { + return feedbackApi.getAllRatings(); + } + + if (!ownerRef) { + return []; + } + + return feedbackApi.getOwnedRatings(ownerRef); + }, [allEntities, feedbackApi, ownerRef]); + + const columns = [ + { title: 'Title', field: 'entityTitle', hidden: true, searchable: true }, + { + title: 'Entity', + field: 'entityRef', + highlight: true, + customSort: (a: EntityRatingsData, b: EntityRatingsData) => { + const titleA = a.entityTitle ?? parseEntityRef(a.entityRef).name; + const titleB = b.entityTitle ?? parseEntityRef(b.entityRef).name; + return titleA.localeCompare(titleB); + }, + render: (rating: EntityRatingsData) => { + const compoundRef = parseEntityRef(rating.entityRef); + return ( + + } + subvalue={compoundRef.kind} + /> + ); + }, + }, + ...ratingValues.map(ratingVal => ({ + title: ratingVal, + field: `ratings.${ratingVal}`, + })), + ]; + + // Exclude entities that don't have applicable ratings + const ratingsRows = ratings?.filter(r => + Object.keys(r.ratings).some(v => ratingValues.includes(v)), + ); + + if (error) { + return ( + + ); + } + + return ( + + columns={columns} + data={ratingsRows ?? []} + isLoading={loading} + options={{ + emptyRowsWhenPaging: false, + loadingType: 'linear', + pageSize: 20, + pageSizeOptions: [20, 50, 100], + paging: true, + showEmptyDataSourceMessage: !loading, + }} + title={title} + /> + ); +}; diff --git a/plugins/entity-feedback/src/components/FeedbackRatingsTable/index.ts b/plugins/entity-feedback/src/components/FeedbackRatingsTable/index.ts new file mode 100644 index 0000000000..565063b3fa --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackRatingsTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FeedbackRatingsTable'; diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx new file mode 100644 index 0000000000..631738d6f9 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.test.tsx @@ -0,0 +1,118 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { ErrorApi, errorApiRef } from '@backstage/core-plugin-api'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import { getByRole, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackResponseDialog } from './FeedbackResponseDialog'; + +describe('FeedbackResponseDialog', () => { + const testEntity: Partial = { + kind: 'component', + metadata: { name: 'test', namespace: 'default' }, + }; + const errorApi: Partial = { post: jest.fn() }; + const feedbackApi: Partial = { + recordResponse: jest.fn().mockImplementation(() => Promise.resolve()), + }; + + const render = async (props: any = {}) => + renderInTestApp( + + + , + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('allows customization of the dialog title', async () => { + const rendered = await render(); + expect( + rendered.getByText('Please provide feedback on what can be improved'), + ).toBeInTheDocument(); + + const customRendered = await render({ feedbackDialogTitle: 'Test Title' }); + expect(customRendered.getByText('Test Title')).toBeInTheDocument(); + }); + + it('allows customization of the reponse options', async () => { + const rendered = await render(); + expect(rendered.getByText('Incorrect info')).toBeInTheDocument(); + expect(rendered.getByText('Missing info')).toBeInTheDocument(); + expect( + rendered.getByText('Other (please specify below)'), + ).toBeInTheDocument(); + + const customResponses = [ + { id: 'foo', label: 'Foo option' }, + { id: 'bar', label: 'Bar option' }, + ]; + const customRendered = await render({ + feedbackDialogResponses: customResponses, + }); + expect(customRendered.getByText('Foo option')).toBeInTheDocument(); + expect(customRendered.getByText('Bar option')).toBeInTheDocument(); + }); + + it('handles saving user responses', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByRole('checkbox', { name: 'Incorrect info' }), + ); + await userEvent.click( + rendered.getByRole('checkbox', { name: 'Other (please specify below)' }), + ); + await userEvent.type( + getByRole( + rendered.getByTestId('feedback-response-dialog-comments-input'), + 'textbox', + ), + 'test comments', + ); + await userEvent.click( + rendered.getByTestId('feedback-response-dialog-submit-button'), + ); + + await waitFor(() => { + expect(feedbackApi.recordResponse).toHaveBeenCalledWith( + 'component:default/test', + { + comments: 'test comments', + consent: true, + response: 'incorrect,other', + }, + ); + }); + }); +}); diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx new file mode 100644 index 0000000000..c3f935e432 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/FeedbackResponseDialog.tsx @@ -0,0 +1,176 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; +import { Progress } from '@backstage/core-components'; +import { ErrorApiError, errorApiRef, useApi } from '@backstage/core-plugin-api'; +import { + Button, + Checkbox, + Dialog, + DialogActions, + DialogContent, + DialogTitle, + FormControl, + FormControlLabel, + FormGroup, + FormLabel, + Grid, + makeStyles, + Switch, + TextField, + Typography, +} from '@material-ui/core'; +import React, { ReactNode, useState } from 'react'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +import { entityFeedbackApiRef } from '../../api'; + +/** + * @public + */ +export interface EntityFeedbackResponse { + id: string; + label: string; +} + +const defaultFeedbackResponses: EntityFeedbackResponse[] = [ + { id: 'incorrect', label: 'Incorrect info' }, + { id: 'missing', label: 'Missing info' }, + { id: 'other', label: 'Other (please specify below)' }, +]; + +/** + * @public + */ +export interface FeedbackResponseDialogProps { + entity: Entity; + feedbackDialogResponses?: EntityFeedbackResponse[]; + feedbackDialogTitle?: ReactNode; + open: boolean; + onClose: () => void; +} + +const useStyles = makeStyles({ + contactConsent: { + marginTop: '5px', + }, +}); + +export const FeedbackResponseDialog = (props: FeedbackResponseDialogProps) => { + const { + entity, + feedbackDialogResponses = defaultFeedbackResponses, + feedbackDialogTitle = 'Please provide feedback on what can be improved', + open, + onClose, + } = props; + const classes = useStyles(); + const errorApi = useApi(errorApiRef); + const feedbackApi = useApi(entityFeedbackApiRef); + const [responseSelections, setResponseSelections] = useState( + Object.fromEntries(feedbackDialogResponses.map(r => [r.id, false])), + ); + const [comments, setComments] = useState(''); + const [consent, setConsent] = useState(true); + + const [{ loading: saving }, saveResponse] = useAsyncFn(async () => { + try { + await feedbackApi.recordResponse(stringifyEntityRef(entity), { + comments, + consent, + response: Object.keys(responseSelections) + .filter(id => responseSelections[id]) + .join(','), + }); + onClose(); + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, [comments, consent, entity, feedbackApi, onClose, responseSelections]); + + return ( + !saving && onClose()}> + {saving && } + {feedbackDialogTitle} + + + Choose all that applies + + {feedbackDialogResponses.map(response => ( + + setResponseSelections({ + ...responseSelections, + [e.target.name]: e.target.checked, + }) + } + /> + } + label={response.label} + /> + ))} + + + + setComments(e.target.value)} + variant="outlined" + value={comments} + /> + + + Can we reach out to you for more info? + + No + + setConsent(e.target.checked)} + /> + + Yes + + + + + + + + + ); +}; diff --git a/plugins/entity-feedback/src/components/FeedbackResponseDialog/index.ts b/plugins/entity-feedback/src/components/FeedbackResponseDialog/index.ts new file mode 100644 index 0000000000..8ce88f4b87 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseDialog/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FeedbackResponseDialog'; diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.test.tsx b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.test.tsx new file mode 100644 index 0000000000..c5d3d51884 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.test.tsx @@ -0,0 +1,76 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import React from 'react'; +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackResponseTable } from './FeedbackResponseTable'; + +describe('FeedbackResponseTable', () => { + const sampleResponses = [ + { + userRef: 'user:default/foo', + consent: true, + response: 'resp1,resp2', + comments: 'test comment 1', + }, + { + userRef: 'user:default/bar', + consent: false, + response: 'resp3,resp4', + comments: 'test comment 2', + }, + ]; + + const feedbackApi: Partial = { + getResponses: jest.fn().mockImplementation(async () => sampleResponses), + }; + + const render = async (props: any = {}) => + renderInTestApp( + + + , + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('renders all responses correctly', async () => { + const rendered = await render(); + + expect(feedbackApi.getResponses).toHaveBeenCalledWith( + 'component:default/test', + ); + + expect(rendered.getByText('Entity Responses')).toBeInTheDocument(); + + expect(rendered.getByText('foo')).toBeInTheDocument(); + expect(rendered.getByText('resp1')).toBeInTheDocument(); + expect(rendered.getByText('resp2')).toBeInTheDocument(); + expect(rendered.getByText('test comment 1')).toBeInTheDocument(); + expect(rendered.getByText('bar')).toBeInTheDocument(); + expect(rendered.getByText('resp3')).toBeInTheDocument(); + expect(rendered.getByText('resp4')).toBeInTheDocument(); + expect(rendered.getByText('test comment 2')).toBeInTheDocument(); + }); + + it('renders a custom title correctly', async () => { + const rendered = await render({ title: 'Custom Title' }); + expect(rendered.getByText('Custom Title')).toBeInTheDocument(); + }); +}); diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx new file mode 100644 index 0000000000..ba954c7747 --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/FeedbackResponseTable.tsx @@ -0,0 +1,121 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { parseEntityRef } from '@backstage/catalog-model'; +import { ErrorPanel, Table } from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; +import { FeedbackResponse } from '@backstage/plugin-entity-feedback-common'; +import { BackstageTheme } from '@backstage/theme'; +import { Chip, makeStyles } from '@material-ui/core'; +import CheckIcon from '@material-ui/icons/Check'; +import React from 'react'; +import useAsync from 'react-use/lib/useAsync'; + +import { entityFeedbackApiRef } from '../../api'; + +type ResponseRow = Omit; + +const useStyles = makeStyles(theme => ({ + consentCheck: { + color: theme.palette.status.ok, + }, +})); + +/** + * @public + */ +export interface FeedbackResponseTableProps { + entityRef: string; + title?: string; +} + +export const FeedbackResponseTable = (props: FeedbackResponseTableProps) => { + const { entityRef, title = 'Entity Responses' } = props; + const classes = useStyles(); + const feedbackApi = useApi(entityFeedbackApiRef); + + const { + error, + loading, + value: responses, + } = useAsync(async () => { + if (!entityRef) { + return []; + } + + return feedbackApi.getResponses(entityRef); + }, [entityRef, feedbackApi]); + + const columns = [ + { + title: 'User', + field: 'userRef', + width: '15%', + render: (response: ResponseRow) => + humanizeEntityRef(parseEntityRef(response.userRef), { + defaultKind: 'user', + }), + }, + { + title: 'OK to contact?', + field: 'consent', + width: '10%', + render: (response: ResponseRow) => + response.consent ? : '', + }, + { + title: 'Responses', + field: 'response', + width: '35%', + render: (response: ResponseRow) => ( + <> + {response.response?.split(',').map(res => ( + + ))} + + ), + }, + { title: 'Comments', field: 'comments', width: '40%' }, + ]; + + if (error) { + return ( + + ); + } + + return ( + + columns={columns} + data={(responses ?? []) as ResponseRow[]} + isLoading={loading} + options={{ + emptyRowsWhenPaging: false, + loadingType: 'linear', + pageSize: 20, + pageSizeOptions: [20, 50, 100], + paging: true, + showEmptyDataSourceMessage: !loading, + }} + title={title} + /> + ); +}; diff --git a/plugins/entity-feedback/src/components/FeedbackResponseTable/index.ts b/plugins/entity-feedback/src/components/FeedbackResponseTable/index.ts new file mode 100644 index 0000000000..73f4632fbf --- /dev/null +++ b/plugins/entity-feedback/src/components/FeedbackResponseTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './FeedbackResponseTable'; diff --git a/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.test.tsx b/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.test.tsx new file mode 100644 index 0000000000..1edf578d3e --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.test.tsx @@ -0,0 +1,147 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { + ErrorApi, + errorApiRef, + IdentityApi, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { AsyncEntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackRatings, LikeDislikeButtons } from './LikeDislikeButtons'; + +jest.mock('../FeedbackResponseDialog', () => ({ + FeedbackResponseDialog: ({ open }: { open: boolean }) => { + return <>{open && dialog is open}; + }, +})); + +describe('LikeDislikeButtons', () => { + const sampleRatings = [ + { + userRef: 'user:default/me', + rating: FeedbackRatings.like, + }, + { + userRef: 'user:default/someone', + rating: FeedbackRatings.dislike, + }, + ]; + + const testEntity = { + kind: 'component', + metadata: { name: 'test', namespace: 'default' }, + } as Entity; + + const errorApi: Partial = { post: jest.fn() }; + + const feedbackApi: Partial = { + getRatings: jest.fn().mockImplementation(async () => sampleRatings), + recordRating: jest.fn().mockImplementation(() => Promise.resolve()), + }; + + const identityApi: Partial = { + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'user:default/me', + ownershipEntityRefs: [], + }), + }; + + const render = async (props: any = {}) => + renderInTestApp( + + + + + , + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('loads the previous rating if it exists', async () => { + await render(); + expect(feedbackApi.getRatings).toHaveBeenCalledWith( + 'component:default/test', + ); + }); + + it('applies a rating correctly', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-dislike-button'), + ); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.dislike, + ); + + jest.clearAllMocks(); + + await userEvent.click(rendered.getByTestId('entity-feedback-like-button')); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.like, + ); + }); + + it('removes an existing rating correctly', async () => { + const rendered = await render(); + + await userEvent.click(rendered.getByTestId('entity-feedback-like-button')); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.neutral, + ); + }); + + it('opens a response dialog if dislike is selected', async () => { + const rendered = await render(); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-dislike-button'), + ); + expect(rendered.getByText('dialog is open')).toBeInTheDocument(); + }); + + it('does not open a response dialog on dislike if configured not to', async () => { + const rendered = await render({ requestResponse: false }); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-dislike-button'), + ); + expect(rendered.queryByText('dialog is open')).toBeNull(); + }); +}); diff --git a/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.tsx b/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.tsx new file mode 100644 index 0000000000..2d38e60ac2 --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeButtons/LikeDislikeButtons.tsx @@ -0,0 +1,154 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Progress } from '@backstage/core-components'; +import { + ErrorApiError, + errorApiRef, + identityApiRef, + useApi, +} from '@backstage/core-plugin-api'; +import { useAsyncEntity } from '@backstage/plugin-catalog-react'; +import { IconButton } from '@material-ui/core'; +import ThumbDownIcon from '@material-ui/icons/ThumbDown'; +import ThumbUpIcon from '@material-ui/icons/ThumbUp'; +import ThumbDownOutlinedIcon from '@material-ui/icons/ThumbDownOutlined'; +import ThumbUpOutlinedIcon from '@material-ui/icons/ThumbUpOutlined'; +import React, { ReactNode, useCallback, useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +import { entityFeedbackApiRef } from '../../api'; +import { + EntityFeedbackResponse, + FeedbackResponseDialog, +} from '../FeedbackResponseDialog'; + +export enum FeedbackRatings { + like = 'LIKE', + dislike = 'DISLIKE', + neutral = 'NEUTRAL', +} + +/** + * @public + */ +export interface LikeDislikeButtonsProps { + feedbackDialogResponses?: EntityFeedbackResponse[]; + feedbackDialogTitle?: ReactNode; + requestResponse?: boolean; +} + +export const LikeDislikeButtons = (props: LikeDislikeButtonsProps) => { + const { + feedbackDialogResponses, + feedbackDialogTitle, + requestResponse = true, + } = props; + const errorApi = useApi(errorApiRef); + const feedbackApi = useApi(entityFeedbackApiRef); + const identityApi = useApi(identityApiRef); + const [rating, setRating] = useState( + FeedbackRatings.neutral, + ); + const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false); + const { entity, loading: loadingEntity } = useAsyncEntity(); + + const { loading: loadingFeedback } = useAsync(async () => { + // Wait until entity is loaded + if (!entity) { + return; + } + + try { + const identity = await identityApi.getBackstageIdentity(); + const prevFeedback = await feedbackApi.getRatings( + stringifyEntityRef(entity), + ); + setRating( + (prevFeedback.find(r => r.userRef === identity.userEntityRef)?.rating ?? + rating) as FeedbackRatings, + ); + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, [entity, feedbackApi, setRating]); + + const [{ loading: savingFeedback }, saveFeedback] = useAsyncFn( + async (feedback: FeedbackRatings) => { + try { + await feedbackApi.recordRating(stringifyEntityRef(entity!), feedback); + setRating(feedback); + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, + [entity, feedbackApi, setRating], + ); + + const applyRating = useCallback( + (feedback: FeedbackRatings) => { + // Clear rating if feedback is same as current + if (feedback === rating) { + saveFeedback(FeedbackRatings.neutral); + return; + } + + saveFeedback(feedback); + if (feedback === FeedbackRatings.dislike && requestResponse) { + setOpenFeedbackDialog(true); + } + }, + [rating, requestResponse, saveFeedback, setOpenFeedbackDialog], + ); + + if (loadingEntity || loadingFeedback || savingFeedback) { + return ; + } + + return ( + <> + applyRating(FeedbackRatings.like)} + > + {rating === FeedbackRatings.like ? ( + + ) : ( + + )} + + applyRating(FeedbackRatings.dislike)} + > + {rating === FeedbackRatings.dislike ? ( + + ) : ( + + )} + + setOpenFeedbackDialog(false)} + feedbackDialogResponses={feedbackDialogResponses} + feedbackDialogTitle={feedbackDialogTitle} + /> + + ); +}; diff --git a/plugins/entity-feedback/src/components/LikeDislikeButtons/index.ts b/plugins/entity-feedback/src/components/LikeDislikeButtons/index.ts new file mode 100644 index 0000000000..58038c32e1 --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeButtons/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './LikeDislikeButtons'; diff --git a/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.test.tsx b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.test.tsx new file mode 100644 index 0000000000..9f04d32ece --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.test.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; + +import { FeedbackRatings } from '../LikeDislikeButtons'; +import { LikeDislikeRatingsTable } from './LikeDislikeRatingsTable'; + +jest.mock('../FeedbackRatingsTable', () => ({ + FeedbackRatingsTable: ({ ratingValues }: { ratingValues: string[] }) => { + return ( + + {ratingValues.map(v => ( + {v} + ))} + + ); + }, +})); + +describe('LikeDislikeRatingsTable', () => { + it('renders like-dislike ratings correctly', async () => { + const rendered = await renderInTestApp(); + expect(rendered.getByText(FeedbackRatings.like)).toBeInTheDocument(); + expect(rendered.queryByText(FeedbackRatings.neutral)).toBeNull(); + expect(rendered.getByText(FeedbackRatings.dislike)).toBeInTheDocument(); + }); +}); diff --git a/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.tsx b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.tsx new file mode 100644 index 0000000000..5466ba2b03 --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/LikeDislikeRatingsTable.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { FeedbackRatingsTable } from '../FeedbackRatingsTable'; +import { FeedbackRatings } from '../LikeDislikeButtons'; + +/** + * @public + */ +export interface LikeDislikeRatingsTableProps { + allEntities?: boolean; + ownerRef?: string; + title?: string; +} + +export const LikeDislikeRatingsTable = ( + props: LikeDislikeRatingsTableProps, +) => { + const { allEntities, ownerRef, title } = props; + + return ( + r !== FeedbackRatings.neutral, + )} + title={title} + /> + ); +}; diff --git a/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/index.ts b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/index.ts new file mode 100644 index 0000000000..d093b8bd2c --- /dev/null +++ b/plugins/entity-feedback/src/components/LikeDislikeRatingsTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './LikeDislikeRatingsTable'; diff --git a/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.test.tsx b/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.test.tsx new file mode 100644 index 0000000000..82fc843494 --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.test.tsx @@ -0,0 +1,159 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { Entity } from '@backstage/catalog-model'; +import { + ErrorApi, + errorApiRef, + IdentityApi, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { AsyncEntityProvider } from '@backstage/plugin-catalog-react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; +import userEvent from '@testing-library/user-event'; +import React from 'react'; + +import { EntityFeedbackApi, entityFeedbackApiRef } from '../../api'; +import { FeedbackRatings, StarredRatingButtons } from './StarredRatingButtons'; + +jest.mock('../FeedbackResponseDialog', () => ({ + FeedbackResponseDialog: ({ open }: { open: boolean }) => { + return <>{open && dialog is open}; + }, +})); + +describe('StarredRatingButtons', () => { + const sampleRatings = [ + { + userRef: 'user:default/me', + rating: FeedbackRatings.two, + }, + { + userRef: 'user:default/someone', + rating: FeedbackRatings.five, + }, + ]; + + const testEntity = { + kind: 'component', + metadata: { name: 'test', namespace: 'default' }, + } as Entity; + + const errorApi: Partial = { post: jest.fn() }; + + const feedbackApi: Partial = { + getRatings: jest.fn().mockImplementation(async () => sampleRatings), + recordRating: jest.fn().mockImplementation(() => Promise.resolve()), + }; + + const identityApi: Partial = { + getBackstageIdentity: async () => ({ + type: 'user', + userEntityRef: 'user:default/me', + ownershipEntityRefs: [], + }), + }; + + const render = async (props: any = {}) => + renderInTestApp( + + + + + , + ); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('loads the previous rating if it exists', async () => { + await render(); + expect(feedbackApi.getRatings).toHaveBeenCalledWith( + 'component:default/test', + ); + }); + + it('applies a rating correctly', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-3'), + ); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.three.toString(), + ); + + jest.clearAllMocks(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-5'), + ); + expect(feedbackApi.recordRating).toHaveBeenCalledWith( + 'component:default/test', + FeedbackRatings.five.toString(), + ); + }); + + it('ignores an existing rating correctly', async () => { + const rendered = await render(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-2'), + ); + expect(feedbackApi.recordRating).not.toHaveBeenCalled(); + }); + + it('opens a response dialog if a rating under the threshold is selected', async () => { + const rendered = await render(); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-1'), + ); + expect(rendered.getByText('dialog is open')).toBeInTheDocument(); + }); + + it('opens a response dialog if a rating under a custom threshold is selected', async () => { + const rendered = await render({ requestResponseThreshold: 4 }); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-3'), + ); + expect(rendered.getByText('dialog is open')).toBeInTheDocument(); + }); + + it('does not open a response dialog if configured not to', async () => { + const rendered = await render({ requestResponse: false }); + + expect(rendered.queryByText('dialog is open')).toBeNull(); + + await userEvent.click( + rendered.getByTestId('entity-feedback-star-button-1'), + ); + expect(rendered.queryByText('dialog is open')).toBeNull(); + }); +}); diff --git a/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.tsx b/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.tsx new file mode 100644 index 0000000000..51f7da1099 --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingButtons/StarredRatingButtons.tsx @@ -0,0 +1,160 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { Progress } from '@backstage/core-components'; +import { + ErrorApiError, + errorApiRef, + identityApiRef, + useApi, +} from '@backstage/core-plugin-api'; +import { useAsyncEntity } from '@backstage/plugin-catalog-react'; +import { IconButton } from '@material-ui/core'; +import StarOutlineIcon from '@material-ui/icons/StarOutline'; +import StarIcon from '@material-ui/icons/Star'; +import React, { ReactNode, useCallback, useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import useAsyncFn from 'react-use/lib/useAsyncFn'; + +import { entityFeedbackApiRef } from '../../api'; +import { + EntityFeedbackResponse, + FeedbackResponseDialog, +} from '../FeedbackResponseDialog'; + +export enum FeedbackRatings { + one = 1, + two = 2, + three = 3, + four = 4, + five = 5, +} + +/** + * @public + */ +export interface StarredRatingButtonsProps { + feedbackDialogResponses?: EntityFeedbackResponse[]; + feedbackDialogTitle?: ReactNode; + requestResponse?: boolean; + requestResponseThreshold?: number; +} + +export const StarredRatingButtons = (props: StarredRatingButtonsProps) => { + const { + feedbackDialogResponses, + feedbackDialogTitle, + requestResponse = true, + requestResponseThreshold = FeedbackRatings.two, + } = props; + const errorApi = useApi(errorApiRef); + const feedbackApi = useApi(entityFeedbackApiRef); + const identityApi = useApi(identityApiRef); + const [rating, setRating] = useState(); + const [openFeedbackDialog, setOpenFeedbackDialog] = useState(false); + const { entity, loading: loadingEntity } = useAsyncEntity(); + + const { loading: loadingFeedback } = useAsync(async () => { + // Wait until entity is loaded + if (!entity) { + return; + } + + try { + const identity = await identityApi.getBackstageIdentity(); + const prevFeedback = await feedbackApi.getRatings( + stringifyEntityRef(entity), + ); + + const prevRating = prevFeedback.find( + r => r.userRef === identity.userEntityRef, + )?.rating; + if (prevRating) { + setRating(parseInt(prevRating, 10)); + } + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, [entity, feedbackApi, setRating]); + + const [{ loading: savingFeedback }, saveFeedback] = useAsyncFn( + async (feedback: FeedbackRatings) => { + try { + await feedbackApi.recordRating( + stringifyEntityRef(entity!), + feedback.toString(), + ); + setRating(feedback); + } catch (e) { + errorApi.post(e as ErrorApiError); + } + }, + [entity, feedbackApi, setRating], + ); + + const applyRating = useCallback( + (feedback: FeedbackRatings) => { + // Ignore rating if feedback is same as current + if (feedback === rating) { + return; + } + + saveFeedback(feedback); + if (feedback <= requestResponseThreshold && requestResponse) { + setOpenFeedbackDialog(true); + } + }, + [ + rating, + requestResponse, + requestResponseThreshold, + saveFeedback, + setOpenFeedbackDialog, + ], + ); + + if (loadingEntity || loadingFeedback || savingFeedback) { + return ; + } + + return ( + <> + {Object.values(FeedbackRatings) + .filter(o => typeof o === 'number') + .map(starRating => ( + applyRating(starRating as FeedbackRatings)} + > + {rating && rating >= starRating ? ( + + ) : ( + + )} + + ))} + setOpenFeedbackDialog(false)} + feedbackDialogResponses={feedbackDialogResponses} + feedbackDialogTitle={feedbackDialogTitle} + /> + + ); +}; diff --git a/plugins/entity-feedback/src/components/StarredRatingButtons/index.ts b/plugins/entity-feedback/src/components/StarredRatingButtons/index.ts new file mode 100644 index 0000000000..659c914749 --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingButtons/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './StarredRatingButtons'; diff --git a/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.test.tsx b/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.test.tsx new file mode 100644 index 0000000000..be53d59d5f --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.test.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { renderInTestApp } from '@backstage/test-utils'; +import React from 'react'; + +import { FeedbackRatings } from '../StarredRatingButtons'; +import { StarredRatingsTable } from './StarredRatingsTable'; + +jest.mock('../FeedbackRatingsTable', () => ({ + FeedbackRatingsTable: ({ ratingValues }: { ratingValues: string[] }) => { + return ( + + {ratingValues.map(v => ( + {v} + ))} + + ); + }, +})); + +describe('StarredRatingsTable', () => { + it('renders starred ratings correctly', async () => { + const rendered = await renderInTestApp(); + expect(rendered.getByText(FeedbackRatings.one)).toBeInTheDocument(); + expect(rendered.getByText(FeedbackRatings.two)).toBeInTheDocument(); + expect(rendered.getByText(FeedbackRatings.three)).toBeInTheDocument(); + expect(rendered.getByText(FeedbackRatings.four)).toBeInTheDocument(); + expect(rendered.getByText(FeedbackRatings.five)).toBeInTheDocument(); + }); +}); diff --git a/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.tsx b/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.tsx new file mode 100644 index 0000000000..d52fd64640 --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingsTable/StarredRatingsTable.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { FeedbackRatingsTable } from '../FeedbackRatingsTable'; +import { FeedbackRatings } from '../StarredRatingButtons'; + +/** + * @public + */ +export interface StarredRatingsTableProps { + allEntities?: boolean; + ownerRef?: string; + title?: string; +} + +export const StarredRatingsTable = (props: StarredRatingsTableProps) => { + const { allEntities, ownerRef, title } = props; + + return ( + typeof o === 'number') + .map(r => r.toString())} + title={title} + /> + ); +}; diff --git a/plugins/entity-feedback/src/components/StarredRatingsTable/index.ts b/plugins/entity-feedback/src/components/StarredRatingsTable/index.ts new file mode 100644 index 0000000000..9621712e0b --- /dev/null +++ b/plugins/entity-feedback/src/components/StarredRatingsTable/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './StarredRatingsTable'; diff --git a/plugins/entity-feedback/src/components/index.ts b/plugins/entity-feedback/src/components/index.ts new file mode 100644 index 0000000000..a88d525f09 --- /dev/null +++ b/plugins/entity-feedback/src/components/index.ts @@ -0,0 +1,25 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export type { + EntityFeedbackResponse, + FeedbackResponseDialogProps, +} from './FeedbackResponseDialog'; +export type { FeedbackResponseTableProps } from './FeedbackResponseTable'; +export type { LikeDislikeButtonsProps } from './LikeDislikeButtons'; +export type { LikeDislikeRatingsTableProps } from './LikeDislikeRatingsTable'; +export type { StarredRatingButtonsProps } from './StarredRatingButtons'; +export type { StarredRatingsTableProps } from './StarredRatingsTable'; diff --git a/plugins/entity-feedback/src/index.ts b/plugins/entity-feedback/src/index.ts new file mode 100644 index 0000000000..bbcf6bc626 --- /dev/null +++ b/plugins/entity-feedback/src/index.ts @@ -0,0 +1,32 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** + * Entity Feedback frontend plugin + * + * @packageDocumentation + */ +export type { + LikeDislikeButtonsProps, + LikeDislikeRatingsTableProps, + EntityFeedbackResponse, + FeedbackResponseDialogProps, + FeedbackResponseTableProps, + StarredRatingButtonsProps, + StarredRatingsTableProps, +} from './components'; +export * from './plugin'; +export * from './api'; diff --git a/plugins/entity-feedback/src/plugin.test.ts b/plugins/entity-feedback/src/plugin.test.ts new file mode 100644 index 0000000000..a56d37f9ab --- /dev/null +++ b/plugins/entity-feedback/src/plugin.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { entityFeedbackPlugin } from './plugin'; + +describe('entity-feedback', () => { + it('should export plugin', () => { + expect(entityFeedbackPlugin).toBeDefined(); + }); +}); diff --git a/plugins/entity-feedback/src/plugin.tsx b/plugins/entity-feedback/src/plugin.tsx new file mode 100644 index 0000000000..9ebdc5d7af --- /dev/null +++ b/plugins/entity-feedback/src/plugin.tsx @@ -0,0 +1,212 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { stringifyEntityRef } from '@backstage/catalog-model'; +import { + createApiFactory, + createComponentExtension, + createPlugin, + createRoutableExtension, + discoveryApiRef, + fetchApiRef, +} from '@backstage/core-plugin-api'; +import { useAsyncEntity } from '@backstage/plugin-catalog-react'; +import React from 'react'; + +import { entityFeedbackApiRef, EntityFeedbackClient } from './api'; +import { rootRouteRef } from './routes'; + +/** + * @public + */ +export const entityFeedbackPlugin = createPlugin({ + id: 'entity-feedback', + routes: { + root: rootRouteRef, + }, + apis: [ + createApiFactory({ + api: entityFeedbackApiRef, + deps: { + discoveryApi: discoveryApiRef, + fetchApi: fetchApiRef, + }, + factory: ({ discoveryApi, fetchApi }) => + new EntityFeedbackClient({ discoveryApi, fetchApi }), + }), + ], +}); + +/** + * @public + */ +export const LikeDislikeButtons = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'LikeDislikeButtons', + component: { + lazy: () => + import('./components/LikeDislikeButtons').then( + m => m.LikeDislikeButtons, + ), + }, + }), +); + +/** + * @public + */ +export const StarredRatingButtons = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'StarredRatingButtons', + component: { + lazy: () => + import('./components/StarredRatingButtons').then( + m => m.StarredRatingButtons, + ), + }, + }), +); + +/** + * @public + */ +export const FeedbackResponseDialog = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'FeedbackResponseDialog', + component: { + lazy: () => + import('./components/FeedbackResponseDialog').then( + m => m.FeedbackResponseDialog, + ), + }, + }), +); + +/** + * @public + */ +export const EntityFeedbackResponseContent = entityFeedbackPlugin.provide( + createRoutableExtension({ + name: 'EntityFeedbackResponseContent', + mountPoint: rootRouteRef, + component: () => + import('./components/FeedbackResponseTable').then( + ({ FeedbackResponseTable }) => { + return () => { + const { entity } = useAsyncEntity(); + return ( + + ); + }; + }, + ), + }), +); + +/** + * @public + */ +export const FeedbackResponseTable = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'FeedbackResponseTable', + component: { + lazy: () => + import('./components/FeedbackResponseTable').then( + m => m.FeedbackResponseTable, + ), + }, + }), +); + +/** + * @public + */ +export const EntityLikeDislikeRatingsCard = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'EntityLikeDislikeRatingsCard', + component: { + lazy: () => + import('./components/LikeDislikeRatingsTable').then( + ({ LikeDislikeRatingsTable }) => { + return () => { + const { entity } = useAsyncEntity(); + return ( + + ); + }; + }, + ), + }, + }), +); + +/** + * @public + */ +export const LikeDislikeRatingsTable = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'LikeDislikeRatingsTable', + component: { + lazy: () => + import('./components/LikeDislikeRatingsTable').then( + m => m.LikeDislikeRatingsTable, + ), + }, + }), +); + +/** + * @public + */ +export const EntityStarredRatingsCard = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'EntityStarredRatingsCard', + component: { + lazy: () => + import('./components/StarredRatingsTable').then( + ({ StarredRatingsTable }) => { + return () => { + const { entity } = useAsyncEntity(); + return ( + + ); + }; + }, + ), + }, + }), +); + +/** + * @public + */ +export const StarredRatingsTable = entityFeedbackPlugin.provide( + createComponentExtension({ + name: 'StarredRatingsTable', + component: { + lazy: () => + import('./components/StarredRatingsTable').then( + m => m.StarredRatingsTable, + ), + }, + }), +); diff --git a/plugins/entity-feedback/src/routes.ts b/plugins/entity-feedback/src/routes.ts new file mode 100644 index 0000000000..cde7241931 --- /dev/null +++ b/plugins/entity-feedback/src/routes.ts @@ -0,0 +1,21 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'entity-feedback', +}); diff --git a/plugins/entity-feedback/src/setupTests.ts b/plugins/entity-feedback/src/setupTests.ts new file mode 100644 index 0000000000..bf4855cb6e --- /dev/null +++ b/plugins/entity-feedback/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/entity-validation/.eslintrc.js b/plugins/entity-validation/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/entity-validation/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/entity-validation/CHANGELOG.md b/plugins/entity-validation/CHANGELOG.md new file mode 100644 index 0000000000..aa1e41a299 --- /dev/null +++ b/plugins/entity-validation/CHANGELOG.md @@ -0,0 +1,19 @@ +# @backstage/plugin-entity-validation + +## 0.1.0-next.0 + +### Minor Changes + +- d34b3267f9: First implementation for the entity validation UI. For more information refer to its `README.md`. + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 diff --git a/plugins/entity-validation/README.md b/plugins/entity-validation/README.md new file mode 100644 index 0000000000..1f864d0857 --- /dev/null +++ b/plugins/entity-validation/README.md @@ -0,0 +1,53 @@ +# entity-validation + +This plugin creates a new page in Backstage where the user can validate the entities. + +![Entity Validation](./docs/entity-validation.png) + +## Getting started + +First of all, install the package in the `app` package by running the following command: + +```bash +yarn add --cwd packages/app @backstage/plugin-entity-validation +``` + +Add the new route to the app by adding the following line: + +```typescript +// In packages/app/src/App.tsx +import { EntityValidationPage } from '@backstage/plugin-entity-validation'; + +const routes = ( + + {/* ...other routes */} + } /> + +); +``` + +To add the new page to your sidebar, you must include these lines in your `Root.tsx` file: + +```typescript + // In packages/app/src/components/Root/Root.tsx + import BuildIcon from '@material-ui/icons/Build'; + + ... + + export const Root = ({ children }: PropsWithChildren<{}>) => ( + + + {/* ...other elements */} + }> + {/* Global nav, not org-specific */} + {/* ...other sidebars */} + + {/* End global nav */} + + + {/* ...other elements */} + + {children} + + ); +``` diff --git a/plugins/entity-validation/api-report.md b/plugins/entity-validation/api-report.md new file mode 100644 index 0000000000..2fccbf9471 --- /dev/null +++ b/plugins/entity-validation/api-report.md @@ -0,0 +1,27 @@ +## API Report File for "@backstage/plugin-entity-validation" + +> 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 { RouteRef } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export const EntityValidationPage: (props: { + defaultYaml?: string | undefined; + defaultLocation?: string | undefined; +}) => JSX.Element; + +// @public (undocumented) +export const entityValidationPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {}, + {} +>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/entity-validation/dev/index.tsx b/plugins/entity-validation/dev/index.tsx new file mode 100644 index 0000000000..1e1dfa760c --- /dev/null +++ b/plugins/entity-validation/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { entityValidationPlugin, EntityValidationPage } from '../src/plugin'; + +createDevApp() + .registerPlugin(entityValidationPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/entity-validation', + }) + .render(); diff --git a/plugins/entity-validation/docs/entity-validation.png b/plugins/entity-validation/docs/entity-validation.png new file mode 100644 index 0000000000..00cb5fafe0 Binary files /dev/null and b/plugins/entity-validation/docs/entity-validation.png differ diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json new file mode 100644 index 0000000000..6a00e4ad16 --- /dev/null +++ b/plugins/entity-validation/package.json @@ -0,0 +1,68 @@ +{ + "name": "@backstage/plugin-entity-validation", + "version": "0.1.0-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/entity-validation" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-common": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/theme": "workspace:^", + "@codemirror/language": "^6.0.0", + "@codemirror/legacy-modes": "^6.1.0", + "@codemirror/view": "^6.0.0", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "@react-hookz/web": "^20.0.0", + "@uiw/react-codemirror": "^4.9.3", + "lodash": "^4.17.21", + "react-use": "^17.2.4", + "yaml": "^2.0.0" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.49.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx b/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx new file mode 100644 index 0000000000..c39f8d5ed7 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityTextArea/EntityTextArea.tsx @@ -0,0 +1,97 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useMemo, useState } from 'react'; +import { Box, makeStyles } from '@material-ui/core'; +import CodeMirror from '@uiw/react-codemirror'; +import { showPanel } from '@codemirror/view'; +import { StreamLanguage } from '@codemirror/language'; +import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; +import { useKeyboardEvent } from '@react-hookz/web'; + +const useStyles = makeStyles(theme => ({ + container: { + position: 'relative', + width: '100%', + height: '100%', + minHeight: '400px', + }, + codeMirror: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + }, + infoPanel: { + color: theme.palette.info.main, + lineHeight: 2, + margin: theme.spacing(0, 1), + }, +})); + +type TemplateTextAreaProps = { + catalogYaml: string; + onChange: (value: string) => void; + onValidate: () => void; +}; + +export const EntityTextArea = ({ + catalogYaml, + onChange, + onValidate, +}: TemplateTextAreaProps) => { + const classes = useStyles(); + const [close, setClose] = useState(false); + + const panelExtension = useMemo(() => { + if (close) { + return showPanel.of(null); + } + + const dom = document.createElement('div'); + dom.classList.add(classes.infoPanel); + dom.textContent = + 'To validate your catalog-info.yaml click on the "Validate" button or use "Ctrl + S" or "Ctrl + Enter"'; + dom.onclick = () => setClose(true); + return showPanel.of(() => ({ dom, top: true })); + }, [classes, close]); + + // Triggers a validation when Ctrl+S or Ctrl+Enter instead of default behavior + useKeyboardEvent( + e => (e.key === 's' || e.key === 'Enter') && (e.ctrlKey || e.metaKey), + e => { + e.preventDefault(); + onValidate(); + }, + ); + + return ( + + { + // Prevent new line if Ctrl + Enter was clicked + if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) e.preventDefault(); + }} + /> + + ); +}; diff --git a/plugins/entity-validation/src/components/EntityTextArea/index.ts b/plugins/entity-validation/src/components/EntityTextArea/index.ts new file mode 100644 index 0000000000..d2a5e41d0f --- /dev/null +++ b/plugins/entity-validation/src/components/EntityTextArea/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './EntityTextArea'; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx b/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx new file mode 100644 index 0000000000..85d47fa2c2 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/EntityResult.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; +import { ValidateEntityResponse } from '@backstage/catalog-client'; +import { useApp } from '@backstage/core-plugin-api'; +import { + Collapse, + IconButton, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + makeStyles, + Paper, +} from '@material-ui/core'; +import { humanizeEntityRef } from '@backstage/plugin-catalog-react'; +import WorkIcon from '@material-ui/icons/Work'; +import ExpandLessIcon from '@material-ui/icons/ExpandLess'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import { MarkdownContent } from '@backstage/core-components'; +import { ValidationOutputOk } from '../../types'; + +const useStyles = makeStyles(theme => ({ + validationOk: { + color: theme.palette.success.main, + }, + validationNotOk: { + color: theme.palette.error.main, + }, + errorContainer: { + color: theme.palette.error.main, + background: theme.palette.background.paper, + padding: theme.spacing(2), + }, +})); + +type EntityResultProps = { + isFirstError?: boolean; + item: ValidationOutputOk; +}; + +export const EntityResult = ({ + isFirstError = false, + item, +}: EntityResultProps) => { + const classes = useStyles(); + const app = useApp(); + const [expanded, setExpanded] = useState(isFirstError); + + const Icon = + app.getSystemIcon(`kind:${item.entity.kind.toLocaleLowerCase('en-US')}`) ?? + WorkIcon; + + const fetchErrorMessages = (response: ValidateEntityResponse) => { + if (!response.valid) { + return response.errors.map(err => err.message).join('\n\n'); + } + return ''; + }; + + return ( + <> + + + + + setExpanded(!expanded)} + /> + {!item.response.valid && ( + + setExpanded(!expanded)}> + {expanded ? : } + + + )} + + {!item.response.valid && ( + + + + + + )} + + ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx b/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx new file mode 100644 index 0000000000..bc7e22ef54 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/EntityValidationOutput.tsx @@ -0,0 +1,162 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { identityApiRef, useApi } from '@backstage/core-plugin-api'; +import { + catalogApiRef, + humanizeEntityRef, +} from '@backstage/plugin-catalog-react'; +import { List, makeStyles, Typography } from '@material-ui/core'; +import useAsync from 'react-use/lib/useAsync'; +import { InfoCard, Progress } from '@backstage/core-components'; +import Alert from '@material-ui/lab/Alert'; +import { + CatalogProcessorResult, + ValidationOutput, + ValidationOutputError, + ValidationOutputOk, +} from '../../types'; +import { EntityResult } from './EntityResult'; + +const useStyles = makeStyles(theme => ({ + validationOk: { + color: theme.palette.success.main, + }, + validationNotOk: { + color: theme.palette.error.main, + }, +})); + +function sortResults(items: Array) { + return items.sort((a, b) => + humanizeEntityRef(a.entity).localeCompare(humanizeEntityRef(b.entity)), + ); +} + +type EntityValidationOutputProps = { + processorResults?: CatalogProcessorResult[]; + locationUrl: string; +}; + +export const EntityValidationOutput = ({ + processorResults, + locationUrl, +}: EntityValidationOutputProps) => { + const classes = useStyles(); + const catalogApi = useApi(catalogApiRef); + const identityApi = useApi(identityApiRef); + + const { value = [], loading } = useAsync(async (): Promise< + ValidationOutput[] | undefined + > => { + if (!processorResults) { + return undefined; + } + + const location = locationUrl + ? `url:${locationUrl}` + : 'url:http://localhost'; + + return await Promise.all( + processorResults.map(async item => { + if (item.type === 'entity') { + const { token } = await identityApi.getCredentials(); + + const validateResult = await catalogApi.validateEntity( + item.entity, + location, + { + token, + }, + ); + return { + type: 'valid', + entity: item.entity, + response: validateResult, + }; + } else if (item.type === 'error') { + return { + type: 'error', + processingError: `Malformed entity YAML, ${item.error}`, + }; + } + return { + type: 'error', + processingError: `Internal error, failed to parse entity`, + }; + }), + ); + }, [processorResults]); + + if (loading) { + return ; + } + + const errors = value.filter( + v => v.type === 'error', + ) as ValidationOutputError[]; + + const results = sortResults( + value.filter(v => v.type === 'valid') as ValidationOutputOk[], + ); + // If there are any errors, only the first one will be expanded + const firstErrorIndex = results.findIndex(r => !r.response.valid); + + if (errors.length !== 0) { + return ( + <> + {errors.map(err => ( + {err.processingError} + ))} + + ); + } + + return ( + + {results.length === 0 ? ( +
    + + No entity definitions found or validated yet + +
    + ) : ( + <> + + {results.map((item, key) => ( + + ))} + +
    + {results.every(r => r.response.valid) ? ( + + All the entities are valid! + + ) : ( + + One or more entities have validation errors + + )} +
    + + )} +
    + ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationOutput/index.ts b/plugins/entity-validation/src/components/EntityValidationOutput/index.ts new file mode 100644 index 0000000000..8d327581c6 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationOutput/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './EntityValidationOutput'; +export * from './EntityResult'; diff --git a/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx b/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx new file mode 100644 index 0000000000..dc86af8763 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationPage/EntityValidationPage.tsx @@ -0,0 +1,120 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useState } from 'react'; +import { Button, Content, Header, Page } from '@backstage/core-components'; +import { EntityTextArea } from '../EntityTextArea'; +import { Grid, TextField } from '@material-ui/core'; +import { CatalogProcessorResult } from '../../types'; +import { parseEntityYaml } from '../../utils'; +import { EntityValidationOutput } from '../EntityValidationOutput'; + +const EXAMPLE_CATALOG_INFO_YAML = `# Put your catalog-info.yaml below and validate it +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + name: test + description: Component description + links: [] + tags: [] + annotations: {} +spec: + type: service + lifecycle: experimental + owner: owner +`; + +export const EntityValidationPage = (props: { + defaultYaml?: string; + defaultLocation?: string; +}) => { + const { + defaultYaml = EXAMPLE_CATALOG_INFO_YAML, + defaultLocation = 'https://github.com/backstage/backstage/blob/master/catalog-info.yaml', + } = props; + + const [catalogYaml, setCatalogYaml] = useState(defaultYaml); + const [yamlFiles, setYamlFiles] = useState(); + const [locationUrl, setLocationUrl] = useState(''); + + const parseYaml = () => { + const parsedFiles = [ + ...parseEntityYaml(Buffer.from(catalogYaml), { + type: 'url', + target: locationUrl ? locationUrl : 'http://localhost', + }), + ]; + setYamlFiles(parsedFiles); + }; + + return ( + +
    + + + + setLocationUrl(e.target.value)} + /> + + + + + + + + + + + + setCatalogYaml(value)} + catalogYaml={catalogYaml} + /> + + + + + + + + + + + + ); +}; diff --git a/plugins/entity-validation/src/components/EntityValidationPage/index.ts b/plugins/entity-validation/src/components/EntityValidationPage/index.ts new file mode 100644 index 0000000000..24149a9387 --- /dev/null +++ b/plugins/entity-validation/src/components/EntityValidationPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './EntityValidationPage'; diff --git a/plugins/entity-validation/src/index.ts b/plugins/entity-validation/src/index.ts new file mode 100644 index 0000000000..33ae575dfe --- /dev/null +++ b/plugins/entity-validation/src/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { entityValidationPlugin, EntityValidationPage } from './plugin'; diff --git a/plugins/entity-validation/src/plugin.test.ts b/plugins/entity-validation/src/plugin.test.ts new file mode 100644 index 0000000000..eae69e8d3a --- /dev/null +++ b/plugins/entity-validation/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { entityValidationPlugin } from './plugin'; + +describe('entity-validation', () => { + it('should export plugin', () => { + expect(entityValidationPlugin).toBeDefined(); + }); +}); diff --git a/plugins/entity-validation/src/plugin.ts b/plugins/entity-validation/src/plugin.ts new file mode 100644 index 0000000000..9a3014da28 --- /dev/null +++ b/plugins/entity-validation/src/plugin.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createComponentExtension, + createPlugin, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +/** @public */ +export const entityValidationPlugin = createPlugin({ + id: 'entity-validation', + routes: { + root: rootRouteRef, + }, +}); + +/** @public */ +export const EntityValidationPage = entityValidationPlugin.provide( + createComponentExtension({ + name: 'EntityValidationPage', + component: { + lazy: () => + import('./components/EntityValidationPage').then( + m => m.EntityValidationPage, + ), + }, + }), +); diff --git a/plugins/entity-validation/src/processingResult.ts b/plugins/entity-validation/src/processingResult.ts new file mode 100644 index 0000000000..de9d8adbbd --- /dev/null +++ b/plugins/entity-validation/src/processingResult.ts @@ -0,0 +1,73 @@ +/* + * 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. + */ + +// File copied from https://github.com/backstage/backstage/blob/master/plugins/catalog-node/src/api/processingResult.ts +import { InputError, NotFoundError } from '@backstage/errors'; +import { Entity } from '@backstage/catalog-model'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { CatalogProcessorResult, EntityRelationSpec } from './types'; + +/** + * Factory functions for the standard processing result types. + * + * @public + */ +export const processingResult = Object.freeze({ + notFoundError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { + type: 'error', + location: atLocation, + error: new NotFoundError(message), + }; + }, + + inputError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { + type: 'error', + location: atLocation, + error: new InputError(message), + }; + }, + + generalError( + atLocation: LocationSpec, + message: string, + ): CatalogProcessorResult { + return { type: 'error', location: atLocation, error: new Error(message) }; + }, + + location(newLocation: LocationSpec): CatalogProcessorResult { + return { type: 'location', location: newLocation }; + }, + + entity(atLocation: LocationSpec, newEntity: Entity): CatalogProcessorResult { + return { type: 'entity', location: atLocation, entity: newEntity }; + }, + + relation(spec: EntityRelationSpec): CatalogProcessorResult { + return { type: 'relation', relation: spec }; + }, + + refresh(key: string): CatalogProcessorResult { + return { type: 'refresh', key }; + }, +} as const); diff --git a/plugins/entity-validation/src/routes.ts b/plugins/entity-validation/src/routes.ts new file mode 100644 index 0000000000..8cdc138782 --- /dev/null +++ b/plugins/entity-validation/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'entity-validation', +}); diff --git a/plugins/entity-validation/src/setupTests.ts b/plugins/entity-validation/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/entity-validation/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/entity-validation/src/types.ts b/plugins/entity-validation/src/types.ts new file mode 100644 index 0000000000..7ecc27bb25 --- /dev/null +++ b/plugins/entity-validation/src/types.ts @@ -0,0 +1,82 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { ValidateEntityResponse } from '@backstage/catalog-client'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; + +export type ValidationOutputError = { + type: 'error'; + processingError: string; +}; + +export type ValidationOutputOk = { + type: 'valid'; + entity: Entity; + response: ValidateEntityResponse; +}; + +export type ValidationOutput = ValidationOutputOk | ValidationOutputError; + +export type EntityRelationSpec = { + /** + * The source entity of this relation. + */ + source: CompoundEntityRef; + + /** + * The type of the relation. + */ + type: string; + + /** + * The target entity of this relation. + */ + target: CompoundEntityRef; +}; + +type CatalogProcessorLocationResult = { + type: 'location'; + location: LocationSpec; +}; + +type CatalogProcessorEntityResult = { + type: 'entity'; + entity: Entity; + location: LocationSpec; +}; + +type CatalogProcessorRelationResult = { + type: 'relation'; + relation: EntityRelationSpec; +}; + +type CatalogProcessorErrorResult = { + type: 'error'; + error: Error; + location: LocationSpec; +}; + +type CatalogProcessorRefreshKeysResult = { + type: 'refresh'; + key: string; +}; + +export type CatalogProcessorResult = + | CatalogProcessorLocationResult + | CatalogProcessorEntityResult + | CatalogProcessorRelationResult + | CatalogProcessorErrorResult + | CatalogProcessorRefreshKeysResult; diff --git a/plugins/entity-validation/src/utils.ts b/plugins/entity-validation/src/utils.ts new file mode 100644 index 0000000000..d81de6f6c9 --- /dev/null +++ b/plugins/entity-validation/src/utils.ts @@ -0,0 +1,56 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Entity, stringifyLocationRef } from '@backstage/catalog-model'; +import lodash from 'lodash'; +import yaml from 'yaml'; +import { LocationSpec } from '@backstage/plugin-catalog-common'; +import { CatalogProcessorResult } from './types'; +import { processingResult } from './processingResult'; + +// Copy from: https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/modules/util/parse.ts +export function* parseEntityYaml( + data: Buffer, + location: LocationSpec, +): Iterable { + let documents: yaml.Document.Parsed[]; + try { + documents = yaml.parseAllDocuments(data.toString('utf8')).filter(d => d); + } catch (e) { + const loc = stringifyLocationRef(location); + const message = `Failed to parse YAML at ${loc}, ${e}`; + yield processingResult.generalError(location, message); + return; + } + + for (const document of documents) { + if (document.errors?.length) { + const loc = stringifyLocationRef(location); + const message = `YAML error at ${loc}, ${document.errors[0]}`; + yield processingResult.generalError(location, message); + } else { + const json = document.toJSON(); + if (lodash.isPlainObject(json)) { + yield processingResult.entity(location, json as Entity); + } else if (json === null) { + // Ignore null values, these happen if there is an empty document in the + // YAML file, for example if --- is added to the end of the file. + } else { + const message = `Expected object at root, got ${typeof json}`; + yield processingResult.generalError(location, message); + } + } + } +} diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md index b3431b3510..31596ba402 100644 --- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md +++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-events-backend-module-aws-sqs +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json index 1925781020..bf06d46648 100644 --- a/plugins/events-backend-module-aws-sqs/package.json +++ b/plugins/events-backend-module-aws-sqs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-aws-sqs", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md index 12bc4a0798..83fadeba7b 100644 --- a/plugins/events-backend-module-azure/CHANGELOG.md +++ b/plugins/events-backend-module-azure/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-azure +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json index 6520ed83ca..12420646ae 100644 --- a/plugins/events-backend-module-azure/package.json +++ b/plugins/events-backend-module-azure/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-azure", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md index 182d5633ed..6808429921 100644 --- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md +++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-bitbucket-cloud +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json index 6a18b548e8..04e6248a5f 100644 --- a/plugins/events-backend-module-bitbucket-cloud/package.json +++ b/plugins/events-backend-module-bitbucket-cloud/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-bitbucket-cloud", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md index 4d1857f4d6..e05890268c 100644 --- a/plugins/events-backend-module-gerrit/CHANGELOG.md +++ b/plugins/events-backend-module-gerrit/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-backend-module-gerrit +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json index 8765f7777d..59cded8497 100644 --- a/plugins/events-backend-module-gerrit/package.json +++ b/plugins/events-backend-module-gerrit/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gerrit", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md index d15c3c82ce..88fe36262b 100644 --- a/plugins/events-backend-module-github/CHANGELOG.md +++ b/plugins/events-backend-module-github/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-github +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json index aa2f6b26fb..6d1fb5b412 100644 --- a/plugins/events-backend-module-github/package.json +++ b/plugins/events-backend-module-github/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-github", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md index 4e7598b084..4265fa2c71 100644 --- a/plugins/events-backend-module-gitlab/CHANGELOG.md +++ b/plugins/events-backend-module-gitlab/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-events-backend-module-gitlab +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json index bfca24f6bc..ad0b497421 100644 --- a/plugins/events-backend-module-gitlab/package.json +++ b/plugins/events-backend-module-gitlab/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend-module-gitlab", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md index d495725662..713df91935 100644 --- a/plugins/events-backend-test-utils/CHANGELOG.md +++ b/plugins/events-backend-test-utils/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-events-backend-test-utils +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-events-node@0.2.3-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json index 8032c98031..9df1f423e1 100644 --- a/plugins/events-backend-test-utils/package.json +++ b/plugins/events-backend-test-utils/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-events-backend-test-utils", "description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md index 1d5927c387..1d3ae288b4 100644 --- a/plugins/events-backend/CHANGELOG.md +++ b/plugins/events-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-events-backend +## 0.2.3-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-events-node@0.2.3-next.2 + - @backstage/config@1.0.6 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json index d120ae892b..1154fc0633 100644 --- a/plugins/events-backend/package.json +++ b/plugins/events-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-events-backend", - "version": "0.2.3-next.1", + "version": "0.2.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-backend/src/service/EventsPlugin.ts b/plugins/events-backend/src/service/EventsPlugin.ts index 29456d71f7..402104b56c 100644 --- a/plugins/events-backend/src/service/EventsPlugin.ts +++ b/plugins/events-backend/src/service/EventsPlugin.ts @@ -80,7 +80,7 @@ class EventsExtensionPointImpl implements EventsExtensionPoint { * @alpha */ export const eventsPlugin = createBackendPlugin({ - id: 'events', + pluginId: 'events', register(env) { const extensionPoint = new EventsExtensionPointImpl(); env.registerExtensionPoint(eventsExtensionPoint, extensionPoint); diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md index ef0d97ad18..99423d1f4a 100644 --- a/plugins/events-node/CHANGELOG.md +++ b/plugins/events-node/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-events-node +## 0.2.3-next.2 + +### Patch Changes + +- 19d4abf72c: Make `EventParams` typed for implementing tidier event handling. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + ## 0.2.3-next.1 ### Patch Changes diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 51136fd301..8428003b85 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -14,8 +14,8 @@ export interface EventBroker { } // @public (undocumented) -export interface EventParams { - eventPayload: unknown; +export interface EventParams { + eventPayload: TPayload; metadata?: Record; topic: string; } diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json index 2cfff749e3..e1b784c6c4 100644 --- a/plugins/events-node/package.json +++ b/plugins/events-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-events-node", "description": "The plugin-events-node module for @backstage/plugin-events-backend", - "version": "0.2.3-next.1", + "version": "0.2.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/events-node/src/api/EventParams.ts b/plugins/events-node/src/api/EventParams.ts index 6912827281..19a562aee0 100644 --- a/plugins/events-node/src/api/EventParams.ts +++ b/plugins/events-node/src/api/EventParams.ts @@ -17,7 +17,7 @@ /** * @public */ -export interface EventParams { +export interface EventParams { /** * Topic for which this event should be published. */ @@ -25,7 +25,7 @@ export interface EventParams { /** * Event payload. */ - eventPayload: unknown; + eventPayload: TPayload; /** * Metadata (e.g., HTTP headers and similar for events received from external). */ diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md index e3379a0082..d2212665fd 100644 --- a/plugins/example-todo-list-backend/CHANGELOG.md +++ b/plugins/example-todo-list-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @internal/plugin-todo-list-backend +## 1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + ## 1.0.10-next.1 ### Patch Changes diff --git a/plugins/example-todo-list-backend/api-report.md b/plugins/example-todo-list-backend/api-report.md index c4f8665a70..319fbbf226 100644 --- a/plugins/example-todo-list-backend/api-report.md +++ b/plugins/example-todo-list-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { Logger } from 'winston'; @@ -10,6 +11,9 @@ import { Logger } from 'winston'; // @public export function createRouter(options: RouterOptions): Promise; +// @alpha +export const exampleTodoListPlugin: () => BackendFeature; + // @public export interface RouterOptions { // (undocumented) diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json index eec3b47b67..5cda8b5f52 100644 --- a/plugins/example-todo-list-backend/package.json +++ b/plugins/example-todo-list-backend/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list-backend", - "version": "1.0.10-next.1", + "version": "1.0.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,11 +11,12 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -24,6 +25,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", @@ -42,6 +44,7 @@ "supertest": "^6.1.6" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/example-todo-list-backend/src/index.ts b/plugins/example-todo-list-backend/src/index.ts index ca73cb27ba..6917307db6 100644 --- a/plugins/example-todo-list-backend/src/index.ts +++ b/plugins/example-todo-list-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export { exampleTodoListPlugin } from './plugin'; diff --git a/plugins/example-todo-list-backend/src/plugin.ts b/plugins/example-todo-list-backend/src/plugin.ts new file mode 100644 index 0000000000..42985f6e88 --- /dev/null +++ b/plugins/example-todo-list-backend/src/plugin.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The example TODO list backend plugin. + * + * @alpha + */ +export const exampleTodoListPlugin = createBackendPlugin({ + pluginId: 'exampleTodoList', + register(env) { + env.registerInit({ + deps: { + identity: coreServices.identity, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ identity, logger, httpRouter }) { + httpRouter.use( + await createRouter({ + identity, + logger: loggerToWinstonLogger(logger), + }), + ); + }, + }); + }, +}); diff --git a/plugins/example-todo-list/.eslintrc.js b/plugins/example-todo-list/.eslintrc.js index 8c18e76229..e2a53a6ad2 100644 --- a/plugins/example-todo-list/.eslintrc.js +++ b/plugins/example-todo-list/.eslintrc.js @@ -1,14 +1 @@ -module.exports = { - extends: [require.resolve('@backstage/cli/config/eslint')], - rules: { - 'import/no-extraneous-dependencies': [ - 'error', - { - devDependencies: true, - optionalDependencies: true, - peerDependencies: true, - bundledDependencies: true, - }, - ], - }, -}; +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md index 17c56cc5d1..6b0da992bc 100644 --- a/plugins/example-todo-list/CHANGELOG.md +++ b/plugins/example-todo-list/CHANGELOG.md @@ -1,5 +1,14 @@ # @internal/plugin-todo-list +## 1.0.10-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 1.0.10-next.0 ### Patch Changes diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json index fe4ac8d1c3..7d4378b51f 100644 --- a/plugins/example-todo-list/package.json +++ b/plugins/example-todo-list/package.json @@ -1,6 +1,6 @@ { "name": "@internal/plugin-todo-list", - "version": "1.0.10-next.0", + "version": "1.0.10-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore-backend/CHANGELOG.md b/plugins/explore-backend/CHANGELOG.md index e9a25933de..80503f396a 100644 --- a/plugins/explore-backend/CHANGELOG.md +++ b/plugins/explore-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-explore-backend +## 0.0.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-search-common@1.2.1 + ## 0.0.4-next.1 ### Patch Changes diff --git a/plugins/explore-backend/README.md b/plugins/explore-backend/README.md index 3edf48249a..86f60ae226 100644 --- a/plugins/explore-backend/README.md +++ b/plugins/explore-backend/README.md @@ -76,10 +76,9 @@ To index explore tools you will need to register the search collator in the ```diff +import { ToolDocumentCollatorFactory } from '@backstage/plugin-explore-backend'; -async function createSearchEngine( - env: PluginEnvironment, -): Promise { - ... +... + ++ // collator gathers entities from explore. + indexBuilder.addCollator({ + schedule, + factory: ToolDocumentCollatorFactory.fromConfig(env.config, { @@ -87,8 +86,8 @@ async function createSearchEngine( + logger: env.logger, + }), + }); - ... -} + +... ``` ### Wire up the Frontend diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json index 8356240641..77b6b05374 100644 --- a/plugins/explore-backend/package.json +++ b/plugins/explore-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore-backend", - "version": "0.0.4-next.1", + "version": "0.0.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index dfa262e63d..58f14ca14d 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-explore +## 0.4.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-explore-common@0.0.1 + - @backstage/plugin-explore-react@0.0.25 + - @backstage/plugin-search-common@1.2.1 + ## 0.4.0-next.1 ### Minor Changes diff --git a/plugins/explore/README.md b/plugins/explore/README.md index 2388d97ac1..0bb7258ae6 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -30,7 +30,7 @@ any path for the route, but we recommend the following: ```diff // packages/app/src/App.tsx -+ import { ExplorePage, explorePlugin } from '@backstage/plugin-explore'; ++ import { ExplorePage } from '@backstage/plugin-explore'; ... diff --git a/plugins/explore/api-report.md b/plugins/explore/api-report.md index e7adde4b77..da960ae951 100644 --- a/plugins/explore/api-report.md +++ b/plugins/explore/api-report.md @@ -19,6 +19,7 @@ import { IndexableDocument } from '@backstage/plugin-search-common'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; import { TabProps } from '@material-ui/core'; // @public @deprecated (undocumented) @@ -126,7 +127,7 @@ export const ToolExplorerContent: (props: { // @public (undocumented) export const ToolSearchResultListItem: ( - props: ToolSearchResultListItemProps, + props: SearchResultListItemExtensionProps, ) => JSX.Element | null; // @public diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 8a6b856262..888040e1cb 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-explore", "description": "A Backstage plugin for building an exploration page of your software ecosystem", - "version": "0.4.0-next.1", + "version": "0.4.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx b/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx index c9e8946f26..eb12ba45d8 100644 --- a/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx +++ b/plugins/explore/src/components/ToolSearchResultListItem/ToolSearchResultListItem.tsx @@ -18,8 +18,6 @@ import React, { ReactNode } from 'react'; import { Box, Chip, - Divider, - ListItem, ListItemIcon, ListItemText, makeStyles, @@ -64,46 +62,42 @@ export function ToolSearchResultListItem(props: ToolSearchResultListItemProps) { return ( <> - - {props.icon && {props.icon}} -
    - - {props.highlight?.fields.title ? ( - - ) : ( - result.title - )} - - } - secondary={ - props.highlight?.fields.text ? ( + {props.icon && {props.icon}} +
    + + {props.highlight?.fields.title ? ( ) : ( - result.text - ) - } - /> - - {result.tags && - result.tags.map((tag: string) => ( - - ))} - -
    - - + result.title + )} + + } + secondary={ + props.highlight?.fields.text ? ( + + ) : ( + result.text + ) + } + /> + + {result.tags?.map((tag: string) => ( + + ))} + +
    ); } diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index 5a4bec53c5..84e7657355 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -22,7 +22,10 @@ import { discoveryApiRef, fetchApiRef, } from '@backstage/core-plugin-api'; -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react'; +import { + createSearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '@backstage/plugin-search-react'; import { ExploreClient, exploreApiRef } from './api'; import { ToolSearchResultListItemProps } from './components/ToolSearchResultListItem'; // import { exampleTools } from './util/examples'; @@ -70,7 +73,7 @@ export const explorePlugin = createPlugin({ /** @public */ export const ToolSearchResultListItem: ( - props: ToolSearchResultListItemProps, + props: SearchResultListItemExtensionProps, ) => JSX.Element | null = explorePlugin.provide( createSearchResultListItemExtension({ name: 'ToolSearchResultListItem', diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md index 73f7de642f..c4660e5479 100644 --- a/plugins/firehydrant/CHANGELOG.md +++ b/plugins/firehydrant/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-firehydrant +## 0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.32-next.1 ### Patch Changes diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json index 4b40c86c3c..8edcade92b 100644 --- a/plugins/firehydrant/package.json +++ b/plugins/firehydrant/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-firehydrant", "description": "A Backstage plugin that integrates towards FireHydrant", - "version": "0.1.32-next.1", + "version": "0.1.32-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index c0987cd461..1e906085e4 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-fossa +## 0.2.47-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.47-next.1 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 444471315c..a93d3b3922 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-fossa", "description": "A Backstage plugin that integrates towards FOSSA", - "version": "0.2.47-next.1", + "version": "0.2.47-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md index a1f2b5a0c7..5134ae8c3f 100644 --- a/plugins/gcalendar/CHANGELOG.md +++ b/plugins/gcalendar/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-gcalendar +## 0.3.11-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + ## 0.3.11-next.0 ### Patch Changes diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json index 48e78f175a..5c84ebe6cb 100644 --- a/plugins/gcalendar/package.json +++ b/plugins/gcalendar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcalendar", - "version": "0.3.11-next.0", + "version": "0.3.11-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index 0a8693a233..405a344b28 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-gcp-projects +## 0.3.34-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.3.34-next.0 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index a3894760cf..54bfc85dd2 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gcp-projects", "description": "A Backstage plugin that helps you manage projects in GCP", - "version": "0.3.34-next.0", + "version": "0.3.34-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md index 707f7be510..3fee0c167b 100644 --- a/plugins/git-release-manager/CHANGELOG.md +++ b/plugins/git-release-manager/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-git-release-manager +## 0.3.28-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + ## 0.3.28-next.0 ### Patch Changes diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 7ec7d5806c..171a909b74 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-git-release-manager", "description": "A Backstage plugin that helps you manage releases in git", - "version": "0.3.28-next.0", + "version": "0.3.28-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 160a0f8a22..5ae3bafb5d 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-actions +## 0.5.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.5.15-next.1 ### Patch Changes diff --git a/plugins/github-actions/dev/index.tsx b/plugins/github-actions/dev/index.tsx index ee761b3504..93e6ee4f74 100644 --- a/plugins/github-actions/dev/index.tsx +++ b/plugins/github-actions/dev/index.tsx @@ -22,6 +22,8 @@ import { Entity } from '@backstage/catalog-model'; import { GithubActionsApi, githubActionsApiRef } from '../src'; import getWorkflowRunResponse from '../src/__fixtures__/get-workflow-run.json'; import listJobsForWorkflowRunResponse from '../src/__fixtures__/list-jobs-for-workflow-run.json'; +import listWorkflowRuns from '../src/__fixtures__/list-workflow-runs.json'; +import { downloadJobLogsForWorkflowRun } from '../src/__fixtures__/downloadJobLogsForWorkflowRun'; const mockEntity: Entity = { apiVersion: 'backstage.io/v1alpha1', @@ -41,7 +43,9 @@ const mockEntity: Entity = { }; const mockGithubActionsApi: GithubActionsApi = { - async downloadJobLogsForWorkflowRun() {}, + async downloadJobLogsForWorkflowRun() { + return downloadJobLogsForWorkflowRun; + }, async getWorkflow() { return {} as any; }, @@ -52,7 +56,7 @@ const mockGithubActionsApi: GithubActionsApi = { return listJobsForWorkflowRunResponse as any; }, async listWorkflowRuns() { - return {} as any; + return listWorkflowRuns as any; }, async reRunWorkflow() { return {} as any; diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index 509255d343..5165820d62 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.5.15-next.1", + "version": "0.5.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-actions/src/__fixtures__/downloadJobLogsForWorkflowRun.ts b/plugins/github-actions/src/__fixtures__/downloadJobLogsForWorkflowRun.ts new file mode 100644 index 0000000000..7b882d0719 --- /dev/null +++ b/plugins/github-actions/src/__fixtures__/downloadJobLogsForWorkflowRun.ts @@ -0,0 +1,315 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const downloadJobLogsForWorkflowRun = ` +2022-12-09T17:25:25.6542422Z Requested labels: ubuntu-latest +2022-12-09T17:25:25.6542470Z Job defined at: octo-org/octo-template/dynamic/pages/pages-build-deployment@refs/heads/main +2022-12-09T17:25:25.6542493Z Waiting for a runner to pick up this job... +2022-12-09T17:25:25.9157301Z Job is waiting for a hosted runner to come online. +2022-12-09T17:25:29.3998345Z Job is about to start running on the hosted runner: Hosted Agent (hosted) +2022-12-09T17:25:31.7099053Z Current runner version: '2.299.1' +2022-12-09T17:25:31.7123983Z ##[group]Operating System +2022-12-09T17:25:31.7124496Z Ubuntu +2022-12-09T17:25:31.7124859Z 22.04.1 +2022-12-09T17:25:31.7125085Z LTS +2022-12-09T17:25:31.7125353Z ##[endgroup] +2022-12-09T17:25:31.7125657Z ##[group]Runner Image +2022-12-09T17:25:31.7125963Z Image: ubuntu-22.04 +2022-12-09T17:25:31.7126295Z Version: 20221204.2 +2022-12-09T17:25:31.7127048Z Included Software: https://github.com/actions/runner-images/blob/ubuntu22/20221204.2/images/linux/Ubuntu2204-Readme.md +2022-12-09T17:25:31.7127614Z Image Release: https://github.com/actions/runner-images/releases/tag/ubuntu22%2F20221204.2 +2022-12-09T17:25:31.7128026Z ##[endgroup] +2022-12-09T17:25:31.7128400Z ##[group]Runner Image Provisioner +2022-12-09T17:25:31.7128722Z 2.0.91.1 +2022-12-09T17:25:31.7128945Z ##[endgroup] +2022-12-09T17:25:31.7129586Z ##[group]GITHUB_TOKEN Permissions +2022-12-09T17:25:31.7130130Z Contents: read +2022-12-09T17:25:31.7130387Z Metadata: read +2022-12-09T17:25:31.7130849Z Pages: write +2022-12-09T17:25:31.7131169Z ##[endgroup] +2022-12-09T17:25:31.7134756Z Secret source: Actions +2022-12-09T17:25:31.7135232Z Prepare workflow directory +2022-12-09T17:25:31.7951647Z Prepare all required actions +2022-12-09T17:25:31.8134091Z Getting action download info +2022-12-09T17:25:32.1190857Z Download action repository 'actions/checkout@v3' (SHA:93ea575cb5d8a053eaa0ac8fa3b40d7e05a33cc8) +2022-12-09T17:25:32.4403240Z Download action repository 'actions/jekyll-build-pages@v1' (SHA:9b882383c88a22eef81e9c1a5a232c2ff9a6672b) +2022-12-09T17:25:32.7457755Z Download action repository 'actions/upload-pages-artifact@v1' (SHA:c8641e80048cf33d329c39df3bcc53196178335a) +2022-12-09T17:25:33.0262452Z Getting action download info +2022-12-09T17:25:33.1786870Z Download action repository 'actions/upload-artifact@main' (SHA:83fd05a356d7e2593de66fc9913b3002723633cb) +2022-12-09T17:25:33.4537715Z ##[group]Pull down action image 'ghcr.io/actions/jekyll-build-pages:v1.0.4' +2022-12-09T17:25:33.4611360Z ##[command]/usr/bin/docker pull ghcr.io/actions/jekyll-build-pages:v1.0.4 +2022-12-09T17:25:34.0231935Z v1.0.4: Pulling from actions/jekyll-build-pages +2022-12-09T17:25:34.0232298Z eff15d958d66: Pulling fs layer +2022-12-09T17:25:34.0232609Z 923e91ae3a1b: Pulling fs layer +2022-12-09T17:25:34.0232986Z 2aa5d3a4a151: Pulling fs layer +2022-12-09T17:25:34.0233248Z bc64adf2d0b2: Pulling fs layer +2022-12-09T17:25:34.0233543Z bfc5cca7d80e: Pulling fs layer +2022-12-09T17:25:34.0233827Z 61b6e3e25463: Pulling fs layer +2022-12-09T17:25:34.0234080Z 7c62fb63f605: Pulling fs layer +2022-12-09T17:25:34.0234380Z 0f68cc81f8f6: Pulling fs layer +2022-12-09T17:25:34.0234663Z 09f325326117: Pulling fs layer +2022-12-09T17:25:34.0234894Z a73a0a50c4db: Pulling fs layer +2022-12-09T17:25:34.0235171Z bc64adf2d0b2: Waiting +2022-12-09T17:25:34.0235470Z bfc5cca7d80e: Waiting +2022-12-09T17:25:34.0235840Z 61b6e3e25463: Waiting +2022-12-09T17:25:34.0236104Z 7c62fb63f605: Waiting +2022-12-09T17:25:34.0236314Z 0f68cc81f8f6: Waiting +2022-12-09T17:25:34.0236586Z 09f325326117: Waiting +2022-12-09T17:25:34.0236855Z a73a0a50c4db: Waiting +2022-12-09T17:25:34.1800756Z 2aa5d3a4a151: Verifying Checksum +2022-12-09T17:25:34.1801065Z 2aa5d3a4a151: Download complete +2022-12-09T17:25:34.4333121Z eff15d958d66: Verifying Checksum +2022-12-09T17:25:34.4335284Z eff15d958d66: Download complete +2022-12-09T17:25:34.5215161Z bc64adf2d0b2: Verifying Checksum +2022-12-09T17:25:34.5231169Z bc64adf2d0b2: Download complete +2022-12-09T17:25:34.5266358Z 923e91ae3a1b: Verifying Checksum +2022-12-09T17:25:34.5266637Z 923e91ae3a1b: Download complete +2022-12-09T17:25:34.6185999Z bfc5cca7d80e: Verifying Checksum +2022-12-09T17:25:34.6186341Z bfc5cca7d80e: Download complete +2022-12-09T17:25:34.6627434Z 7c62fb63f605: Verifying Checksum +2022-12-09T17:25:34.6629228Z 7c62fb63f605: Download complete +2022-12-09T17:25:34.8423112Z 09f325326117: Verifying Checksum +2022-12-09T17:25:34.8424383Z 09f325326117: Download complete +2022-12-09T17:25:35.0142784Z a73a0a50c4db: Verifying Checksum +2022-12-09T17:25:35.0143743Z a73a0a50c4db: Download complete +2022-12-09T17:25:35.2282893Z 0f68cc81f8f6: Verifying Checksum +2022-12-09T17:25:35.2283672Z 0f68cc81f8f6: Download complete +2022-12-09T17:25:35.8547685Z 61b6e3e25463: Verifying Checksum +2022-12-09T17:25:35.8547985Z 61b6e3e25463: Download complete +2022-12-09T17:25:36.3689873Z eff15d958d66: Pull complete +2022-12-09T17:25:38.9751393Z 923e91ae3a1b: Pull complete +2022-12-09T17:25:39.0574850Z 2aa5d3a4a151: Pull complete +2022-12-09T17:25:39.6785807Z bc64adf2d0b2: Pull complete +2022-12-09T17:25:39.7560781Z bfc5cca7d80e: Pull complete +2022-12-09T17:25:45.3145640Z 61b6e3e25463: Pull complete +2022-12-09T17:25:45.3626474Z 7c62fb63f605: Pull complete +2022-12-09T17:25:48.5182928Z 0f68cc81f8f6: Pull complete +2022-12-09T17:25:48.5924407Z 09f325326117: Pull complete +2022-12-09T17:25:48.6361139Z a73a0a50c4db: Pull complete +2022-12-09T17:25:48.6390662Z Digest: sha256:e56d7e4c5ac45aa2934b1d535f4c66769a442ef6538c7948f8b4e3b2950fd7b4 +2022-12-09T17:25:48.6435854Z Status: Downloaded newer image for ghcr.io/actions/jekyll-build-pages:v1.0.4 +2022-12-09T17:25:48.6436263Z ghcr.io/actions/jekyll-build-pages:v1.0.4 +2022-12-09T17:25:48.6444026Z ##[endgroup] +2022-12-09T17:25:48.6814585Z ##[group]Run actions/checkout@v3 +2022-12-09T17:25:48.6814842Z with: +2022-12-09T17:25:48.6815018Z ref: main +2022-12-09T17:25:48.6815204Z submodules: recursive +2022-12-09T17:25:48.6815442Z repository: octo-org/octo-template +2022-12-09T17:25:48.6815884Z token: *** +2022-12-09T17:25:48.6816068Z ssh-strict: true +2022-12-09T17:25:48.6816284Z persist-credentials: true +2022-12-09T17:25:48.6816500Z clean: true +2022-12-09T17:25:48.6816685Z fetch-depth: 1 +2022-12-09T17:25:48.6816872Z lfs: false +2022-12-09T17:25:48.6817076Z set-safe-directory: true +2022-12-09T17:25:48.6817275Z ##[endgroup] +2022-12-09T17:25:48.9194010Z Syncing repository: octo-org/octo-template +2022-12-09T17:25:48.9195798Z ##[group]Getting Git version info +2022-12-09T17:25:48.9196263Z Working directory is '/home/runner/work/octo-template/octo-template' +2022-12-09T17:25:48.9196777Z [command]/usr/bin/git version +2022-12-09T17:25:48.9267188Z git version 2.38.1 +2022-12-09T17:25:48.9293570Z ##[endgroup] +2022-12-09T17:25:48.9315385Z Temporarily overriding HOME='/home/runner/work/_temp/54730a3a-82b6-4069-a509-366e53e131cc' before making global git config changes +2022-12-09T17:25:48.9315845Z Adding repository directory to the temporary git global config as a safe directory +2022-12-09T17:25:48.9316365Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/octo-template/octo-template +2022-12-09T17:25:48.9362562Z Deleting the contents of '/home/runner/work/octo-template/octo-template' +2022-12-09T17:25:48.9366563Z ##[group]Initializing the repository +2022-12-09T17:25:48.9370913Z [command]/usr/bin/git init /home/runner/work/octo-template/octo-template +2022-12-09T17:25:48.9428579Z hint: Using 'master' as the name for the initial branch. This default branch name +2022-12-09T17:25:48.9429400Z hint: is subject to change. To configure the initial branch name to use in all +2022-12-09T17:25:48.9429828Z hint: of your new repositories, which will suppress this warning, call: +2022-12-09T17:25:48.9430089Z hint: +2022-12-09T17:25:48.9430525Z hint: git config --global init.defaultBranch +2022-12-09T17:25:48.9430769Z hint: +2022-12-09T17:25:48.9431094Z hint: Names commonly chosen instead of 'master' are 'main', 'trunk' and +2022-12-09T17:25:48.9431510Z hint: 'development'. The just-created branch can be renamed via this command: +2022-12-09T17:25:48.9431770Z hint: +2022-12-09T17:25:48.9431994Z hint: git branch -m +2022-12-09T17:25:48.9442316Z Initialized empty Git repository in /home/runner/work/octo-template/octo-template/.git/ +2022-12-09T17:25:48.9454154Z [command]/usr/bin/git remote add origin https://github.com/octo-org/octo-template +2022-12-09T17:25:48.9490442Z ##[endgroup] +2022-12-09T17:25:48.9491112Z ##[group]Disabling automatic garbage collection +2022-12-09T17:25:48.9494676Z [command]/usr/bin/git config --local gc.auto 0 +2022-12-09T17:25:48.9524540Z ##[endgroup] +2022-12-09T17:25:48.9525308Z ##[group]Setting up auth +2022-12-09T17:25:48.9532488Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand +2022-12-09T17:25:48.9563906Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || : +2022-12-09T17:25:48.9857748Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader +2022-12-09T17:25:48.9892225Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || : +2022-12-09T17:25:49.0110192Z [command]/usr/bin/git config --local http.https://github.com/.extraheader AUTHORIZATION: basic *** +2022-12-09T17:25:49.0146631Z ##[endgroup] +2022-12-09T17:25:49.0147330Z ##[group]Fetching the repository +2022-12-09T17:25:49.0156010Z [command]/usr/bin/git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin +refs/heads/main*:refs/remotes/origin/main* +refs/tags/main*:refs/tags/main* +2022-12-09T17:25:49.2837218Z remote: Enumerating objects: 3, done. +2022-12-09T17:25:49.2837524Z remote: Counting objects: 33% (1/3) +2022-12-09T17:25:49.2837809Z remote: Counting objects: 66% (2/3) +2022-12-09T17:25:49.2838044Z remote: Counting objects: 100% (3/3) +2022-12-09T17:25:49.2839733Z remote: Counting objects: 100% (3/3), done. +2022-12-09T17:25:49.2840661Z remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 +2022-12-09T17:25:49.2896737Z From https://github.com/octo-org/octo-template +2022-12-09T17:25:49.2897109Z * [new branch] main -> origin/main +2022-12-09T17:25:49.2924446Z ##[endgroup] +2022-12-09T17:25:49.2925129Z ##[group]Determining the checkout info +2022-12-09T17:25:49.2931526Z [command]/usr/bin/git branch --list --remote origin/main +2022-12-09T17:25:49.2958361Z origin/main +2022-12-09T17:25:49.2963283Z ##[endgroup] +2022-12-09T17:25:49.2964130Z ##[group]Checking out the ref +2022-12-09T17:25:49.2970560Z [command]/usr/bin/git checkout --progress --force -B main refs/remotes/origin/main +2022-12-09T17:25:49.3010381Z Switched to a new branch 'main' +2022-12-09T17:25:49.3014811Z branch 'main' set up to track 'origin/main'. +2022-12-09T17:25:49.3019861Z ##[endgroup] +2022-12-09T17:25:49.3022847Z ##[group]Setting up auth for fetching submodules +2022-12-09T17:25:49.3031486Z [command]/usr/bin/git config --global http.https://github.com/.extraheader AUTHORIZATION: basic *** +2022-12-09T17:25:49.3101009Z [command]/usr/bin/git config --global --unset-all url.https://github.com/.insteadOf +2022-12-09T17:25:49.3132496Z [command]/usr/bin/git config --global --add url.https://github.com/.insteadOf git@github.com: +2022-12-09T17:25:49.3159145Z ##[endgroup] +2022-12-09T17:25:49.3159671Z ##[group]Fetching submodules +2022-12-09T17:25:49.3163060Z [command]/usr/bin/git submodule sync --recursive +2022-12-09T17:25:49.3374628Z [command]/usr/bin/git -c protocol.version=2 submodule update --init --force --depth=1 --recursive +2022-12-09T17:25:49.3595435Z [command]/usr/bin/git submodule foreach --recursive git config --local gc.auto 0 +2022-12-09T17:25:49.3838526Z ##[endgroup] +2022-12-09T17:25:49.3839209Z ##[group]Persisting credentials for submodules +2022-12-09T17:25:49.3844491Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'url\.https\:\/\/github\.com\/\.insteadOf' && git config --local --unset-all 'url.https://github.com/.insteadOf' || : +2022-12-09T17:25:49.4113780Z [command]/usr/bin/git submodule foreach --recursive git config --local 'http.https://github.com/.extraheader' 'AUTHORIZATION: basic ***' && git config --local --show-origin --name-only --get-regexp remote.origin.url +2022-12-09T17:25:49.4324497Z [command]/usr/bin/git submodule foreach --recursive git config --local --add 'url.https://github.com/.insteadOf' 'git@github.com:' +2022-12-09T17:25:49.4536981Z ##[endgroup] +2022-12-09T17:25:49.4565161Z [command]/usr/bin/git log -1 --format='%H' +2022-12-09T17:25:49.4592051Z 'c85af0e5e5798047462143a13c1b455ee1275a64' +2022-12-09T17:25:49.4851246Z ##[group]Run actions/jekyll-build-pages@v1 +2022-12-09T17:25:49.4851491Z with: +2022-12-09T17:25:49.4851672Z source: . +2022-12-09T17:25:49.4851856Z destination: ./_site +2022-12-09T17:25:49.4852055Z future: false +2022-12-09T17:25:49.4852300Z build_revision: c85af0e5e5798047462143a13c1b455ee1275a64 +2022-12-09T17:25:49.4852525Z verbose: true +2022-12-09T17:25:49.4852887Z token: *** +2022-12-09T17:25:49.4853073Z ##[endgroup] +2022-12-09T17:25:49.5094807Z ##[command]/usr/bin/docker run --name ghcrioactionsjekyllbuildpagesv104_e36266 --label 290506 --workdir /github/workspace --rm -e "INPUT_SOURCE" -e "INPUT_DESTINATION" -e "INPUT_FUTURE" -e "INPUT_BUILD_REVISION" -e "INPUT_VERBOSE" -e "INPUT_TOKEN" -e "HOME" -e "GITHUB_JOB" -e "GITHUB_REF" -e "GITHUB_SHA" -e "GITHUB_REPOSITORY" -e "GITHUB_REPOSITORY_OWNER" -e "GITHUB_RUN_ID" -e "GITHUB_RUN_NUMBER" -e "GITHUB_RETENTION_DAYS" -e "GITHUB_RUN_ATTEMPT" -e "GITHUB_ACTOR" -e "GITHUB_TRIGGERING_ACTOR" -e "GITHUB_WORKFLOW" -e "GITHUB_HEAD_REF" -e "GITHUB_BASE_REF" -e "GITHUB_EVENT_NAME" -e "GITHUB_SERVER_URL" -e "GITHUB_API_URL" -e "GITHUB_GRAPHQL_URL" -e "GITHUB_REF_NAME" -e "GITHUB_REF_PROTECTED" -e "GITHUB_REF_TYPE" -e "GITHUB_WORKSPACE" -e "GITHUB_ACTION" -e "GITHUB_EVENT_PATH" -e "GITHUB_ACTION_REPOSITORY" -e "GITHUB_ACTION_REF" -e "GITHUB_PATH" -e "GITHUB_ENV" -e "GITHUB_STEP_SUMMARY" -e "GITHUB_STATE" -e "GITHUB_OUTPUT" -e "RUNNER_OS" -e "RUNNER_ARCH" -e "RUNNER_NAME" -e "RUNNER_TOOL_CACHE" -e "RUNNER_TEMP" -e "RUNNER_WORKSPACE" -e "ACTIONS_RUNTIME_URL" -e "ACTIONS_RUNTIME_TOKEN" -e "ACTIONS_CACHE_URL" -e "ACTIONS_ID_TOKEN_REQUEST_URL" -e "ACTIONS_ID_TOKEN_REQUEST_TOKEN" -e GITHUB_ACTIONS=true -e CI=true -v "/var/run/docker.sock":"/var/run/docker.sock" -v "/home/runner/work/_temp/_github_home":"/github/home" -v "/home/runner/work/_temp/_github_workflow":"/github/workflow" -v "/home/runner/work/_temp/_runner_file_commands":"/github/file_commands" -v "/home/runner/work/octo-template/octo-template":"/github/workspace" ghcr.io/actions/jekyll-build-pages:v1.0.4 +2022-12-09T17:25:50.5151506Z [33mConfiguration file: none[0m +2022-12-09T17:25:50.5313330Z Logging at level: debug +2022-12-09T17:25:50.5313941Z GitHub Pages: github-pages v227 +2022-12-09T17:25:50.5314484Z GitHub Pages: jekyll v3.9.2 +2022-12-09T17:25:50.5315560Z Theme: jekyll-theme-primer +2022-12-09T17:25:50.5316125Z Theme source: /usr/local/bundle/gems/jekyll-theme-primer-0.6.0 +2022-12-09T17:25:50.5316710Z Requiring: jekyll-github-metadata +2022-12-09T17:25:50.6063961Z To use retry middleware with Faraday v2.0+, install faraday-retry gem +2022-12-09T17:25:52.4164334Z Requiring: jekyll-seo-tag +2022-12-09T17:25:52.4165089Z Requiring: jekyll-coffeescript +2022-12-09T17:25:52.4166139Z Requiring: jekyll-commonmark-ghpages +2022-12-09T17:25:52.4166490Z Requiring: jekyll-gist +2022-12-09T17:25:52.4167025Z Requiring: jekyll-github-metadata +2022-12-09T17:25:52.4167363Z Requiring: jekyll-paginate +2022-12-09T17:25:52.4167663Z Requiring: jekyll-relative-links +2022-12-09T17:25:52.4167989Z Requiring: jekyll-optional-front-matter +2022-12-09T17:25:52.4168298Z Requiring: jekyll-readme-index +2022-12-09T17:25:52.4168589Z Requiring: jekyll-default-layout +2022-12-09T17:25:52.4168888Z Requiring: jekyll-titles-from-headings +2022-12-09T17:25:52.4169143Z GitHub Metadata: Initializing... +2022-12-09T17:25:52.4169392Z Source: /github/workspace/. +2022-12-09T17:25:52.4169651Z Destination: /github/workspace/./_site +2022-12-09T17:25:52.4169981Z Incremental build: disabled. Enable with --incremental +2022-12-09T17:25:52.4170230Z Generating... +2022-12-09T17:25:52.4183638Z Generating: JekyllOptionalFrontMatter::Generator finished in 0.000150897 seconds. +2022-12-09T17:25:52.4184136Z Generating: JekyllReadmeIndex::Generator finished in 0.00169567 seconds. +2022-12-09T17:25:52.4185038Z Generating: Jekyll::Paginate::Pagination finished in 4.8e-06 seconds. +2022-12-09T17:25:52.4185483Z Generating: JekyllRelativeLinks::Generator finished in 2.97e-05 seconds. +2022-12-09T17:25:52.4185914Z Generating: JekyllDefaultLayout::Generator finished in 7.7999e-05 seconds. +2022-12-09T17:25:52.4186258Z Requiring: kramdown-parser-gfm +2022-12-09T17:25:52.4186586Z Generating: JekyllTitlesFromHeadings::Generator finished in 0.008350253 seconds. +2022-12-09T17:25:52.4186888Z Rendering: assets/css/style.scss +2022-12-09T17:25:52.4187176Z Pre-Render Hooks: assets/css/style.scss +2022-12-09T17:25:52.4187437Z Rendering Markup: assets/css/style.scss +2022-12-09T17:25:52.4187672Z Rendering: README.md +2022-12-09T17:25:52.4187920Z Pre-Render Hooks: README.md +2022-12-09T17:25:52.4188141Z Rendering Markup: README.md +2022-12-09T17:25:52.4188360Z Rendering Layout: README.md +2022-12-09T17:25:52.4188568Z Layout source: theme +2022-12-09T17:25:52.4188886Z GitHub Metadata: Generating for octo-org/octo-template +2022-12-09T17:25:52.4189408Z GitHub Metadata: Calling @client.repository("octo-org/octo-template", {:accept=>"application/vnd.github.drax-preview+json"}) +2022-12-09T17:25:52.4189903Z GitHub Metadata: Calling @client.pages("octo-org/octo-template", {}) +2022-12-09T17:25:52.4190223Z Writing: /github/workspace/_site/assets/css/style.css +2022-12-09T17:25:52.4190520Z Writing: /github/workspace/_site/index.html +2022-12-09T17:25:52.4190772Z done in 1.524 seconds. +2022-12-09T17:25:52.4191074Z Auto-regeneration: disabled. Use --watch to enable. +2022-12-09T17:25:52.5460546Z ##[group]Run actions/upload-pages-artifact@v1 +2022-12-09T17:25:52.5460787Z with: +2022-12-09T17:25:52.5460968Z path: ./_site +2022-12-09T17:25:52.5461168Z name: github-pages +2022-12-09T17:25:52.5461361Z retention-days: 1 +2022-12-09T17:25:52.5461560Z ##[endgroup] +2022-12-09T17:25:52.5745795Z ##[group]Run chmod -c -R +rX . | while read line; do +2022-12-09T17:25:52.5746128Z [36;1mchmod -c -R +rX . | while read line; do[0m +2022-12-09T17:25:52.5746462Z [36;1m echo "::warning title=Invalid file permissions automatically fixed::$line"[0m +2022-12-09T17:25:52.5746740Z [36;1mdone[0m +2022-12-09T17:25:52.5746920Z [36;1mtar \[0m +2022-12-09T17:25:52.5747146Z [36;1m --dereference --hard-dereference \[0m +2022-12-09T17:25:52.5747410Z [36;1m --directory "$INPUT_PATH" \[0m +2022-12-09T17:25:52.5747668Z [36;1m -cvf "$RUNNER_TEMP/artifact.tar" \[0m +2022-12-09T17:25:52.5747896Z [36;1m --exclude=.git \[0m +2022-12-09T17:25:52.5748110Z [36;1m --exclude=.github \[0m +2022-12-09T17:25:52.5748352Z [36;1m .[0m +2022-12-09T17:25:52.5805770Z shell: /usr/bin/sh -e {0} +2022-12-09T17:25:52.5805980Z env: +2022-12-09T17:25:52.5806170Z INPUT_PATH: ./_site +2022-12-09T17:25:52.5806374Z ##[endgroup] +2022-12-09T17:25:52.5967973Z chmod: changing permissions of './_site': Operation not permitted +2022-12-09T17:25:52.5986749Z chmod: changing permissions of './_site/index.html': Operation not permitted +2022-12-09T17:25:52.5987217Z chmod: changing permissions of './_site/README.md': Operation not permitted +2022-12-09T17:25:52.5987625Z chmod: changing permissions of './_site/assets': Operation not permitted +2022-12-09T17:25:52.5988025Z chmod: changing permissions of './_site/assets/css': Operation not permitted +2022-12-09T17:25:52.5988455Z chmod: changing permissions of './_site/assets/css/style.css': Operation not permitted +2022-12-09T17:25:52.6003872Z ./ +2022-12-09T17:25:52.6004035Z ./index.html +2022-12-09T17:25:52.6004213Z ./README.md +2022-12-09T17:25:52.6004387Z ./assets/ +2022-12-09T17:25:52.6004556Z ./assets/css/ +2022-12-09T17:25:52.6004743Z ./assets/css/style.css +2022-12-09T17:25:52.6089697Z ##[group]Run actions/upload-artifact@main +2022-12-09T17:25:52.6089918Z with: +2022-12-09T17:25:52.6090104Z name: github-pages +2022-12-09T17:25:52.6090342Z path: /home/runner/work/_temp/artifact.tar +2022-12-09T17:25:52.6090677Z retention-days: 1 +2022-12-09T17:25:52.6090884Z if-no-files-found: warn +2022-12-09T17:25:52.6091085Z ##[endgroup] +2022-12-09T17:25:52.6745114Z With the provided path, there will be 1 file uploaded +2022-12-09T17:25:52.6748618Z Starting artifact upload +2022-12-09T17:25:52.6749367Z For more detailed logs during the artifact upload process, enable step-debugging: https://docs.github.com/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging#enabling-step-debug-logging +2022-12-09T17:25:52.6750012Z Artifact name is valid! +2022-12-09T17:25:52.7711407Z Container for artifact "github-pages" successfully created. Starting upload of file(s) +2022-12-09T17:25:52.8679144Z Total size of all the files uploaded is 12638 bytes +2022-12-09T17:25:52.8680162Z File upload process has finished. Finalizing the artifact upload +2022-12-09T17:25:52.8976033Z Artifact has been finalized. All files have been successfully uploaded! +2022-12-09T17:25:52.8976512Z +2022-12-09T17:25:52.8976816Z The raw size of all the files that were specified for upload is 92160 bytes +2022-12-09T17:25:52.8977329Z The size of all the files that were uploaded is 12638 bytes. This takes into account any gzip compression used to reduce the upload size, time and storage +2022-12-09T17:25:52.8977612Z +2022-12-09T17:25:52.8978179Z Note: The size of downloaded zips can differ significantly from the reported size. For more information see: https://github.com/actions/upload-artifact#zipped-artifact-downloads +2022-12-09T17:25:52.8978524Z +2022-12-09T17:25:52.8980509Z Artifact github-pages has been successfully uploaded! +2022-12-09T17:25:52.9084311Z Post job cleanup. +2022-12-09T17:25:53.0358449Z [command]/usr/bin/git version +2022-12-09T17:25:53.0407485Z git version 2.38.1 +2022-12-09T17:25:53.0465462Z Temporarily overriding HOME='/home/runner/work/_temp/52bccf8b-8b44-45e0-a383-0833ced20023' before making global git config changes +2022-12-09T17:25:53.0470560Z Adding repository directory to the temporary git global config as a safe directory +2022-12-09T17:25:53.0478850Z [command]/usr/bin/git config --global --add safe.directory /home/runner/work/octo-template/octo-template +2022-12-09T17:25:53.0517249Z [command]/usr/bin/git config --local --name-only --get-regexp core\.sshCommand +2022-12-09T17:25:53.0550835Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'core\.sshCommand' && git config --local --unset-all 'core.sshCommand' || : +2022-12-09T17:25:53.0769173Z [command]/usr/bin/git config --local --name-only --get-regexp http\.https\:\/\/github\.com\/\.extraheader +2022-12-09T17:25:53.0792441Z http.https://github.com/.extraheader +2022-12-09T17:25:53.0803756Z [command]/usr/bin/git config --local --unset-all http.https://github.com/.extraheader +2022-12-09T17:25:53.0838329Z [command]/usr/bin/git submodule foreach --recursive git config --local --name-only --get-regexp 'http\.https\:\/\/github\.com\/\.extraheader' && git config --local --unset-all 'http.https://github.com/.extraheader' || : +2022-12-09T17:25:53.1274888Z Cleaning up orphan processes +`; diff --git a/plugins/github-actions/src/__fixtures__/list-workflow-runs.json b/plugins/github-actions/src/__fixtures__/list-workflow-runs.json new file mode 100644 index 0000000000..4d1e1223ce --- /dev/null +++ b/plugins/github-actions/src/__fixtures__/list-workflow-runs.json @@ -0,0 +1,223 @@ +{ + "total_count": 1, + "workflow_runs": [ + { + "id": 30433642, + "name": "Build", + "node_id": "MDEyOldvcmtmbG93IFJ1bjI2OTI4OQ==", + "check_suite_id": 42, + "check_suite_node_id": "MDEwOkNoZWNrU3VpdGU0Mg==", + "head_branch": "master", + "head_sha": "acb5820ced9479c074f688cc328bf03f341a511d", + "run_number": 562, + "event": "push", + "status": "queued", + "conclusion": null, + "workflow_id": 159038, + "url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642", + "html_url": "https://github.com/octo-org/octo-repo/actions/runs/30433642", + "pull_requests": [], + "created_at": "2020-01-22T19:33:08Z", + "updated_at": "2020-01-22T19:33:08Z", + "actor": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "run_attempt": 1, + "run_started_at": "2020-01-22T19:33:08Z", + "triggering_actor": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/jobs", + "logs_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/logs", + "check_suite_url": "https://api.github.com/repos/octo-org/octo-repo/check-suites/414944374", + "artifacts_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/artifacts", + "cancel_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/cancel", + "rerun_url": "https://api.github.com/repos/octo-org/octo-repo/actions/runs/30433642/rerun", + "workflow_url": "https://api.github.com/repos/octo-org/octo-repo/actions/workflows/159038", + "head_commit": { + "id": "acb5820ced9479c074f688cc328bf03f341a511d", + "tree_id": "d23f6eedb1e1b9610bbc754ddb5197bfe7271223", + "message": "Create linter.yaml", + "timestamp": "2020-01-22T19:33:05Z", + "author": { + "name": "Octo Cat", + "email": "octocat@github.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 1296269, + "node_id": "MDEwOlJlcG9zaXRvcnkxMjk2MjY5", + "name": "Hello-World", + "full_name": "octocat/Hello-World", + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "private": false, + "html_url": "https://github.com/octocat/Hello-World", + "description": "This your first repo!", + "fork": false, + "url": "https://api.github.com/repos/octocat/Hello-World", + "archive_url": "https://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}", + "assignees_url": "https://api.github.com/repos/octocat/Hello-World/assignees{/user}", + "blobs_url": "https://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}", + "branches_url": "https://api.github.com/repos/octocat/Hello-World/branches{/branch}", + "collaborators_url": "https://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}", + "comments_url": "https://api.github.com/repos/octocat/Hello-World/comments{/number}", + "commits_url": "https://api.github.com/repos/octocat/Hello-World/commits{/sha}", + "compare_url": "https://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}", + "contents_url": "https://api.github.com/repos/octocat/Hello-World/contents/{+path}", + "contributors_url": "https://api.github.com/repos/octocat/Hello-World/contributors", + "deployments_url": "https://api.github.com/repos/octocat/Hello-World/deployments", + "downloads_url": "https://api.github.com/repos/octocat/Hello-World/downloads", + "events_url": "https://api.github.com/repos/octocat/Hello-World/events", + "forks_url": "https://api.github.com/repos/octocat/Hello-World/forks", + "git_commits_url": "https://api.github.com/repos/octocat/Hello-World/git/commits{/sha}", + "git_refs_url": "https://api.github.com/repos/octocat/Hello-World/git/refs{/sha}", + "git_tags_url": "https://api.github.com/repos/octocat/Hello-World/git/tags{/sha}", + "git_url": "git:github.com/octocat/Hello-World.git", + "issue_comment_url": "https://api.github.com/repos/octocat/Hello-World/issues/comments{/number}", + "issue_events_url": "https://api.github.com/repos/octocat/Hello-World/issues/events{/number}", + "issues_url": "https://api.github.com/repos/octocat/Hello-World/issues{/number}", + "keys_url": "https://api.github.com/repos/octocat/Hello-World/keys{/key_id}", + "labels_url": "https://api.github.com/repos/octocat/Hello-World/labels{/name}", + "languages_url": "https://api.github.com/repos/octocat/Hello-World/languages", + "merges_url": "https://api.github.com/repos/octocat/Hello-World/merges", + "milestones_url": "https://api.github.com/repos/octocat/Hello-World/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}", + "pulls_url": "https://api.github.com/repos/octocat/Hello-World/pulls{/number}", + "releases_url": "https://api.github.com/repos/octocat/Hello-World/releases{/id}", + "ssh_url": "git@github.com:octocat/Hello-World.git", + "stargazers_url": "https://api.github.com/repos/octocat/Hello-World/stargazers", + "statuses_url": "https://api.github.com/repos/octocat/Hello-World/statuses/{sha}", + "subscribers_url": "https://api.github.com/repos/octocat/Hello-World/subscribers", + "subscription_url": "https://api.github.com/repos/octocat/Hello-World/subscription", + "tags_url": "https://api.github.com/repos/octocat/Hello-World/tags", + "teams_url": "https://api.github.com/repos/octocat/Hello-World/teams", + "trees_url": "https://api.github.com/repos/octocat/Hello-World/git/trees{/sha}", + "hooks_url": "http://api.github.com/repos/octocat/Hello-World/hooks" + }, + "head_repository": { + "id": 217723378, + "node_id": "MDEwOlJlcG9zaXRvcnkyMTc3MjMzNzg=", + "name": "octo-repo", + "full_name": "octo-org/octo-repo", + "private": true, + "owner": { + "login": "octocat", + "id": 1, + "node_id": "MDQ6VXNlcjE=", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "url": "https://api.github.com/users/octocat", + "html_url": "https://github.com/octocat", + "followers_url": "https://api.github.com/users/octocat/followers", + "following_url": "https://api.github.com/users/octocat/following{/other_user}", + "gists_url": "https://api.github.com/users/octocat/gists{/gist_id}", + "starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/octocat/subscriptions", + "organizations_url": "https://api.github.com/users/octocat/orgs", + "repos_url": "https://api.github.com/users/octocat/repos", + "events_url": "https://api.github.com/users/octocat/events{/privacy}", + "received_events_url": "https://api.github.com/users/octocat/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/octo-org/octo-repo", + "description": null, + "fork": false, + "url": "https://api.github.com/repos/octo-org/octo-repo", + "forks_url": "https://api.github.com/repos/octo-org/octo-repo/forks", + "keys_url": "https://api.github.com/repos/octo-org/octo-repo/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/octo-org/octo-repo/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/octo-org/octo-repo/teams", + "hooks_url": "https://api.github.com/repos/octo-org/octo-repo/hooks", + "issue_events_url": "https://api.github.com/repos/octo-org/octo-repo/issues/events{/number}", + "events_url": "https://api.github.com/repos/octo-org/octo-repo/events", + "assignees_url": "https://api.github.com/repos/octo-org/octo-repo/assignees{/user}", + "branches_url": "https://api.github.com/repos/octo-org/octo-repo/branches{/branch}", + "tags_url": "https://api.github.com/repos/octo-org/octo-repo/tags", + "blobs_url": "https://api.github.com/repos/octo-org/octo-repo/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/octo-org/octo-repo/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/octo-org/octo-repo/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/octo-org/octo-repo/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/octo-org/octo-repo/statuses/{sha}", + "languages_url": "https://api.github.com/repos/octo-org/octo-repo/languages", + "stargazers_url": "https://api.github.com/repos/octo-org/octo-repo/stargazers", + "contributors_url": "https://api.github.com/repos/octo-org/octo-repo/contributors", + "subscribers_url": "https://api.github.com/repos/octo-org/octo-repo/subscribers", + "subscription_url": "https://api.github.com/repos/octo-org/octo-repo/subscription", + "commits_url": "https://api.github.com/repos/octo-org/octo-repo/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/octo-org/octo-repo/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/octo-org/octo-repo/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/octo-org/octo-repo/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/octo-org/octo-repo/contents/{+path}", + "compare_url": "https://api.github.com/repos/octo-org/octo-repo/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/octo-org/octo-repo/merges", + "archive_url": "https://api.github.com/repos/octo-org/octo-repo/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/octo-org/octo-repo/downloads", + "issues_url": "https://api.github.com/repos/octo-org/octo-repo/issues{/number}", + "pulls_url": "https://api.github.com/repos/octo-org/octo-repo/pulls{/number}", + "milestones_url": "https://api.github.com/repos/octo-org/octo-repo/milestones{/number}", + "notifications_url": "https://api.github.com/repos/octo-org/octo-repo/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/octo-org/octo-repo/labels{/name}", + "releases_url": "https://api.github.com/repos/octo-org/octo-repo/releases{/id}", + "deployments_url": "https://api.github.com/repos/octo-org/octo-repo/deployments" + } + } + ] +} diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index 5e0a5f3e15..f55cad8424 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -94,7 +94,7 @@ describe('', () => { ); it('renders a table with a row for each workflow', async () => { - const subject = await renderSubject(); + const subject = renderSubject(); workflowRuns.forEach(run => { expect(subject.getByText(run.message)).toBeInTheDocument(); @@ -102,7 +102,7 @@ describe('', () => { }); it('renders a workflow row correctly', async () => { - const subject = await renderSubject(); + const subject = renderSubject(); const [run] = workflowRuns; expect(subject.getByText(run.message).closest('a')).toHaveAttribute( 'href', diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md index 68156abda2..9c3d9c85e4 100644 --- a/plugins/github-deployments/CHANGELOG.md +++ b/plugins/github-deployments/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-github-deployments +## 0.1.46-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.46-next.1 ### Patch Changes diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json index ec596194c7..17d6239b90 100644 --- a/plugins/github-deployments/package.json +++ b/plugins/github-deployments/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-deployments", "description": "A Backstage plugin that integrates towards GitHub Deployments", - "version": "0.1.46-next.1", + "version": "0.1.46-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-issues/CHANGELOG.md b/plugins/github-issues/CHANGELOG.md index 38c0387429..35aa080016 100644 --- a/plugins/github-issues/CHANGELOG.md +++ b/plugins/github-issues/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-github-issues +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json index 284518bad3..574968d61a 100644 --- a/plugins/github-issues/package.json +++ b/plugins/github-issues/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-issues", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/github-pull-requests-board/CHANGELOG.md b/plugins/github-pull-requests-board/CHANGELOG.md index fa4123887b..8dd45cd956 100644 --- a/plugins/github-pull-requests-board/CHANGELOG.md +++ b/plugins/github-pull-requests-board/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-github-pull-requests-board +## 0.1.9-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json index e947557825..163b7a1967 100644 --- a/plugins/github-pull-requests-board/package.json +++ b/plugins/github-pull-requests-board/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-github-pull-requests-board", "description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index fa835e2131..4ade8515ff 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-gitops-profiles +## 0.3.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.3.33-next.0 ### Patch Changes diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 3f0f3614cd..f43e7bb14b 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gitops-profiles", "description": "A Backstage plugin that helps you manage GitOps profiles", - "version": "0.3.33-next.0", + "version": "0.3.33-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md index adecc5b5f1..01037953b0 100644 --- a/plugins/gocd/CHANGELOG.md +++ b/plugins/gocd/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-gocd +## 0.1.21-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.21-next.1 ### Patch Changes diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json index f659fe4525..f89a11047d 100644 --- a/plugins/gocd/package.json +++ b/plugins/gocd/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-gocd", "description": "A Backstage plugin that integrates towards GoCD", - "version": "0.1.21-next.1", + "version": "0.1.21-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 1be4fe30b5..68d6fa8e25 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-graphiql +## 0.2.47-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.2.47-next.0 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index b5c868d86d..15d15a38ca 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphiql", "description": "Backstage plugin for browsing GraphQL APIs", - "version": "0.2.47-next.0", + "version": "0.2.47-next.1", "publishConfig": { "access": "public", "main": "dist/index.esm.js", diff --git a/plugins/graphql-backend/CHANGELOG.md b/plugins/graphql-backend/CHANGELOG.md index 4686d4b103..28efff9546 100644 --- a/plugins/graphql-backend/CHANGELOG.md +++ b/plugins/graphql-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-graphql-backend +## 0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-catalog-graphql@0.3.18-next.1 + ## 0.1.32-next.1 ### Patch Changes diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json index d3b4fc8707..0f44248997 100644 --- a/plugins/graphql-backend/package.json +++ b/plugins/graphql-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-graphql-backend", "description": "An experimental Backstage backend plugin for GraphQL", - "version": "0.1.32-next.1", + "version": "0.1.32-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/graphql-voyager/.eslintrc.js b/plugins/graphql-voyager/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/graphql-voyager/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/graphql-voyager/CHANGELOG.md b/plugins/graphql-voyager/CHANGELOG.md new file mode 100644 index 0000000000..3712169ea1 --- /dev/null +++ b/plugins/graphql-voyager/CHANGELOG.md @@ -0,0 +1,16 @@ +# @backstage/plugin-graphql-voyager + +## 0.1.0-next.0 + +### Minor Changes + +- d1fb4b7bf1: Add the new GraphQL Voyager tool as a plugin for backstage, expose necessary API and components needed for it. + + For more information on how to use this plugin and configure it, please navigate to the [README](https://github.com/backstage/backstage/tree/master/plugins/graphql-voyager/README.md) of the plugin. + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 diff --git a/plugins/graphql-voyager/README.md b/plugins/graphql-voyager/README.md new file mode 100644 index 0000000000..2c903cd9e5 --- /dev/null +++ b/plugins/graphql-voyager/README.md @@ -0,0 +1,76 @@ +# graphql-voyager + +Welcome to the graphql-voyager plugin! + +Presents a graph structure of your entire GraphQL API, with tabs for multiple API urls: +![](./src/assets/plugin.png) + +## Getting started + +### Installing + +To get started, first install the plugin with the following command: + +```bash +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-graphql-voyager +``` + +### Adding the page + +In order to be able to navigate to the graphQL voyager page, a new route needs to be added to the app. This can be done in the following way: + +```tsx +// packages/app/App.tsx + + import { GraphQLVoyagerPage } from '@backstage/plugin-graphql-voyager'; + + const routes = ( + + }/> +``` + +### Configuration + +In order for the plugin to function correctly. GraphQL endpoints need to be added / configured through the GraphQLVoyager API. This can be done by implementing the `graphQlVoyagerApiRef` exported by this plugin. + +Add your own configuration to the `packages/app/src/apis.ts` file the following way: + +```ts +import { identityApiRef } from '@backstage/core-plugin-api'; +import { GraphQLVoyagerEndpoints } from '@backstage/plugin-graphql-voyager'; + +export const apis: AnyApiFactory[] = [ + createApiFactory({ + api: graphQlVoyagerApiRef, + deps: { identityApi: identityApiRef }, + factory: ({ identityApiRef }) => { + return GraphQLVoyagerEndpoints.from([ + { + id: `graphql-voyager-endpoint-id`, + title: 'endpoint-title', + introspectionErrorMessage: + 'Unable to perform introspection, make sure you are on the correct environment.', + introspection: async (query: any) => { + const token = 'someSecretJWTComingFromIdentityApiRef'; + + const res = await fetch('graphQLEndpoint', { + method: 'POST', + body: JSON.stringify({ query }), + headers: { + 'Content-Type': 'application/json', + Authorization: token, + }, + }); + + return res.json(); + }, + voyagerProps: { + hideDocs: true, + }, + }, + ]); + }, + }), +]; +``` diff --git a/plugins/graphql-voyager/api-report.md b/plugins/graphql-voyager/api-report.md new file mode 100644 index 0000000000..9b488f9356 --- /dev/null +++ b/plugins/graphql-voyager/api-report.md @@ -0,0 +1,71 @@ +## API Report File for "@backstage/plugin-graphql-voyager" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiRef } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { RouteRef } from '@backstage/core-plugin-api'; +import { VoyagerDisplayOptions } from 'graphql-voyager/typings/components/Voyager'; +import { WorkerCallback } from 'graphql-voyager/typings/utils/types'; + +// @public (undocumented) +export type GraphQLVoyagerApi = { + getEndpoints(): Promise; +}; + +// @public (undocumented) +export const graphQlVoyagerApiRef: ApiRef; + +// @public (undocumented) +export type GraphQLVoyagerEndpoint = { + id: string; + title: string; + introspection: (body: any) => Promise; + introspectionErrorMessage: string; + voyagerProps?: { + displayOptions?: VoyagerDisplayOptions; + hideDocs?: boolean; + hideSettings?: boolean; + workerURI?: string; + loadWorker?: WorkerCallback; + }; +}; + +// @public (undocumented) +export class GraphQLVoyagerEndpoints implements GraphQLVoyagerApi { + // (undocumented) + static from(endpoints: GraphQLVoyagerEndpoint[]): GraphQLVoyagerEndpoints; + // (undocumented) + getEndpoints(): Promise; +} + +// @public (undocumented) +export const GraphqlVoyagerPage: ( + props: GraphQLVoyagerPageProps, +) => JSX.Element; + +// @public (undocumented) +export type GraphQLVoyagerPageProps = { + title?: string; +}; + +// @public (undocumented) +export const graphqlVoyagerPlugin: BackstagePlugin< + { + root: RouteRef; + }, + {}, + {} +>; + +// @public (undocumented) +export const introspectionQuery: string; + +// @public (undocumented) +export const Router: (props: GraphQLVoyagerPageProps) => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/graphql-voyager/package.json b/plugins/graphql-voyager/package.json new file mode 100644 index 0000000000..b20d045640 --- /dev/null +++ b/plugins/graphql-voyager/package.json @@ -0,0 +1,53 @@ +{ + "name": "@backstage/plugin-graphql-voyager", + "description": "Backstage plugin for GraphQL Voyager", + "version": "0.1.0-next.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.57", + "graphql-voyager": "^1.0.0-rc.31", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.49.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/graphql-voyager/src/assets/plugin.png b/plugins/graphql-voyager/src/assets/plugin.png new file mode 100644 index 0000000000..602c7abde3 Binary files /dev/null and b/plugins/graphql-voyager/src/assets/plugin.png differ diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.test.tsx b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.test.tsx new file mode 100644 index 0000000000..76ccbd9965 --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderInTestApp } from '@backstage/test-utils'; +import { GraphQLVoyagerBrowser } from './GraphQLVoyagerBrowser'; + +jest.mock('graphql-voyager', () => ({ Voyager: () => '' })); + +describe('GraphQLVoyagerBrowser', () => { + it('should render error text if there are no endpoints', async () => { + const rendered = await renderInTestApp( + , + ); + + expect(rendered.getByText('No endpoints available')).toBeInTheDocument(); + }); + + it('should render endpoint tabs', async () => { + const rendered = await renderInTestApp( + , + ); + + expect(rendered.getByText('Endpoint A')).toBeInTheDocument(); + expect(rendered.getByText('Endpoint B')).toBeInTheDocument(); + }); + + it('Should render the introspection error message when introspection fails', async () => { + const rendered = await renderInTestApp( + , + ); + + expect(rendered.getByText('Failed to introspect A')).toBeInTheDocument(); + }); +}); diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.tsx b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.tsx new file mode 100644 index 0000000000..d2f9262983 --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/GraphQLVoyagerBrowser.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + GraphQLVoyagerEndpoint, + introspectionQuery, +} from '../../lib/api/types'; +import { useState } from 'react'; +import { makeStyles, Tab, Tabs, Typography } from '@material-ui/core'; +import React, { Suspense } from 'react'; +import { BackstageTheme } from '@backstage/theme'; +import { Content, ErrorPanel, Progress } from '@backstage/core-components'; +import { Voyager } from 'graphql-voyager'; +import useAsync from 'react-use/lib/useAsync'; + +const useStyles = makeStyles(() => ({ + root: { + height: '100%', + display: 'flex', + flexFlow: 'column nowrap', + }, +})); + +type GraphQLVoyagerBrowserProps = { + endpoints: GraphQLVoyagerEndpoint[]; +}; + +const NoEndpoints = () => { + return No endpoints available; +}; + +const VoyagerBrowserContent = ({ + endpoint, +}: { + endpoint: GraphQLVoyagerEndpoint; +}) => { + const { id, introspection, introspectionErrorMessage, voyagerProps } = + endpoint; + const { value, loading, error } = useAsync( + () => introspection(introspectionQuery), + [endpoint], + ); + + if (loading) { + return ; + } else if (error) { + return ; + } + return value.data ? ( + + ) : ( + + {introspectionErrorMessage} + + ); +}; + +const VoyagerBrowser = (props: GraphQLVoyagerBrowserProps) => { + const { endpoints } = props; + const [tabIndex, setTabIndex] = useState(0); + const classes = useStyles(); + + return ( +
    + }> + setTabIndex(value)} + indicatorColor="primary" + > + {endpoints.map(({ title, id }, index) => ( + + ))} + + + + + +
    + ); +}; + +/** @public */ +export const GraphQLVoyagerBrowser = (props: GraphQLVoyagerBrowserProps) => { + const hasEndpoints = checkEndpoints(props); + + if (!hasEndpoints) { + return ; + } + return ; +}; + +function checkEndpoints(props: GraphQLVoyagerBrowserProps) { + return props.endpoints.length; +} diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/index.ts b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/index.ts new file mode 100644 index 0000000000..3beb0ccd95 --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerBrowser/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './GraphQLVoyagerBrowser'; diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.test.tsx b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.test.tsx new file mode 100644 index 0000000000..67c257be2b --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.test.tsx @@ -0,0 +1,60 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { renderInTestApp, TestApiProvider } from '@backstage/test-utils'; + +import { GraphQLVoyagerPage } from './GraphQLVoyagerPage'; +import { GraphQLVoyagerApi, graphQlVoyagerApiRef } from '../../lib/api'; + +jest.mock('../GraphQLVoyagerBrowser', () => ({ + GraphQLVoyagerBrowser: () => '', +})); + +describe('GraphQLVoyagerPage', () => { + it('should show error when no endpoints are loaded', async () => { + const loadingApi: GraphQLVoyagerApi = { + async getEndpoints() { + throw new Error('No endpoints'); + }, + }; + + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByText('Welcome to Voyager!')).toBeInTheDocument(); + expect(rendered.getByText('Error: No endpoints')).toBeInTheDocument(); + }); + + it('should show GraphQLVoyagerBrowser', async () => { + const loadingApi: GraphQLVoyagerApi = { + async getEndpoints() { + return []; + }, + }; + + const rendered = await renderInTestApp( + + + , + ); + + expect(rendered.getByText('Welcome to Voyager!')).toBeInTheDocument(); + expect(rendered.getByText('')).toBeInTheDocument(); + }); +}); diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.tsx b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.tsx new file mode 100644 index 0000000000..f3c4f70b4f --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/GraphQLVoyagerPage.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + Header, + Page, + Content, + Progress, + ErrorPanel, +} from '@backstage/core-components'; +import { useApi } from '@backstage/core-plugin-api'; +import 'graphql-voyager/dist/voyager.css'; +import { graphQlVoyagerApiRef } from '../../lib/api'; +import useAsync from 'react-use/lib/useAsync'; +import { GraphQLVoyagerBrowser } from '../GraphQLVoyagerBrowser'; + +/** @public */ +export type GraphQLVoyagerPageProps = { + title?: string; +}; + +const VoyagerContent = () => { + const graphQLVoyagerApi = useApi(graphQlVoyagerApiRef); + + const { value, loading, error } = useAsync(() => + graphQLVoyagerApi.getEndpoints(), + ); + + if (loading) { + return ; + } else if (error) { + return ; + } + return ; +}; + +/** @public */ +export const GraphQLVoyagerPage = (props: GraphQLVoyagerPageProps) => { + const { title = 'Welcome to Voyager!' } = props; + + return ( + +
    + + + + + ); +}; diff --git a/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/index.ts b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/index.ts new file mode 100644 index 0000000000..5ee042fd5f --- /dev/null +++ b/plugins/graphql-voyager/src/components/GraphQLVoyagerPage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './GraphQLVoyagerPage'; diff --git a/plugins/graphql-voyager/src/components/index.ts b/plugins/graphql-voyager/src/components/index.ts new file mode 100644 index 0000000000..76c4229839 --- /dev/null +++ b/plugins/graphql-voyager/src/components/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './GraphQLVoyagerPage'; +export * from './GraphQLVoyagerBrowser'; diff --git a/plugins/graphql-voyager/src/index.ts b/plugins/graphql-voyager/src/index.ts new file mode 100644 index 0000000000..9e314c2eef --- /dev/null +++ b/plugins/graphql-voyager/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { graphqlVoyagerPlugin, GraphqlVoyagerPage } from './plugin'; +export { GraphQLVoyagerPage as Router } from './components'; +export type { GraphQLVoyagerPageProps } from './components'; +export * from './lib/api'; diff --git a/plugins/graphql-voyager/src/lib/api/GraphQLVoyagerEndpoints.ts b/plugins/graphql-voyager/src/lib/api/GraphQLVoyagerEndpoints.ts new file mode 100644 index 0000000000..4d08ffbce3 --- /dev/null +++ b/plugins/graphql-voyager/src/lib/api/GraphQLVoyagerEndpoints.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { GraphQLVoyagerApi, GraphQLVoyagerEndpoint } from './types'; + +/** @public */ +export class GraphQLVoyagerEndpoints implements GraphQLVoyagerApi { + private constructor(private readonly endpoints: GraphQLVoyagerEndpoint[]) {} + + static from(endpoints: GraphQLVoyagerEndpoint[]) { + return new GraphQLVoyagerEndpoints(endpoints); + } + + async getEndpoints() { + return this.endpoints; + } +} diff --git a/plugins/graphql-voyager/src/lib/api/index.ts b/plugins/graphql-voyager/src/lib/api/index.ts new file mode 100644 index 0000000000..c52eedafb7 --- /dev/null +++ b/plugins/graphql-voyager/src/lib/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './GraphQLVoyagerEndpoints'; +export * from './types'; diff --git a/plugins/graphql-voyager/src/lib/api/types.ts b/plugins/graphql-voyager/src/lib/api/types.ts new file mode 100644 index 0000000000..08e6f13923 --- /dev/null +++ b/plugins/graphql-voyager/src/lib/api/types.ts @@ -0,0 +1,141 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createApiRef } from '@backstage/core-plugin-api'; +import { VoyagerDisplayOptions } from 'graphql-voyager/typings/components/Voyager'; +import { WorkerCallback } from 'graphql-voyager/typings/utils/types'; + +/** @public */ +export type GraphQLVoyagerEndpoint = { + id: string; + title: string; + introspection: (body: any) => Promise; + introspectionErrorMessage: string; + voyagerProps?: { + displayOptions?: VoyagerDisplayOptions; + hideDocs?: boolean; + hideSettings?: boolean; + workerURI?: string; + loadWorker?: WorkerCallback; + }; +}; + +/** @public */ +export type GraphQLVoyagerApi = { + getEndpoints(): Promise; +}; + +/** @public */ +export const graphQlVoyagerApiRef = createApiRef({ + id: 'plugin.graphqlvoyager.api', +}); + +/** @public */ +export const introspectionQuery: string = `query IntrospectionQuery { + __schema { + + queryType { name } + mutationType { name } + subscriptionType { name } + types { + ...FullType + } + directives { + name + description + + locations + args { + ...InputValue + } + } + } + } + + fragment FullType on __Type { + kind + name + description + + fields(includeDeprecated: true) { + name + description + args { + ...InputValue + } + type { + ...TypeRef + } + isDeprecated + deprecationReason + } + inputFields { + ...InputValue + } + interfaces { + ...TypeRef + } + enumValues(includeDeprecated: true) { + name + description + isDeprecated + deprecationReason + } + possibleTypes { + ...TypeRef + } + } + + fragment InputValue on __InputValue { + name + description + type { ...TypeRef } + defaultValue + + + } + + fragment TypeRef on __Type { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + ofType { + kind + name + } + } + } + } + } + } + } + }`; diff --git a/plugins/graphql-voyager/src/plugin.test.ts b/plugins/graphql-voyager/src/plugin.test.ts new file mode 100644 index 0000000000..df7acc7143 --- /dev/null +++ b/plugins/graphql-voyager/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { graphqlVoyagerPlugin } from './plugin'; + +describe('graphql-voyager', () => { + it('should export plugin', () => { + expect(graphqlVoyagerPlugin).toBeDefined(); + }); +}); diff --git a/plugins/graphql-voyager/src/plugin.ts b/plugins/graphql-voyager/src/plugin.ts new file mode 100644 index 0000000000..bd33209a1d --- /dev/null +++ b/plugins/graphql-voyager/src/plugin.ts @@ -0,0 +1,38 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; + +import { rootRouteRef } from './routes'; + +/** @public */ +export const graphqlVoyagerPlugin = createPlugin({ + id: 'graphql-voyager', + routes: { + root: rootRouteRef, + }, +}); + +/** @public */ +export const GraphqlVoyagerPage = graphqlVoyagerPlugin.provide( + createRoutableExtension({ + name: 'GraphqlVoyagerPage', + component: () => import('./components').then(m => m.GraphQLVoyagerPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/graphql-voyager/src/routes.ts b/plugins/graphql-voyager/src/routes.ts new file mode 100644 index 0000000000..12f06c1cb5 --- /dev/null +++ b/plugins/graphql-voyager/src/routes.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + id: 'graphql-voyager', +}); diff --git a/plugins/graphql-voyager/src/setupTests.ts b/plugins/graphql-voyager/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/graphql-voyager/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md index e9698a5cde..400ca51ae9 100644 --- a/plugins/home/CHANGELOG.md +++ b/plugins/home/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-home +## 0.4.31-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.4.31-next.1 ### Patch Changes diff --git a/plugins/home/README.md b/plugins/home/README.md index 0cd307b706..1fdface722 100644 --- a/plugins/home/README.md +++ b/plugins/home/README.md @@ -9,7 +9,7 @@ For App Integrators, the system is designed to be composable to give total freed If you have a standalone app (you didn't clone this repo), then do ```bash -# From the Backstage repository root +# From your Backstage root directory yarn add --cwd packages/app @backstage/plugin-home ``` diff --git a/plugins/home/package.json b/plugins/home/package.json index 8f6a4f755f..6923b87c83 100644 --- a/plugins/home/package.json +++ b/plugins/home/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-home", "description": "A Backstage plugin that helps you build a home page", - "version": "0.4.31-next.1", + "version": "0.4.31-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md index d0edef70c6..a12bd0a010 100644 --- a/plugins/ilert/CHANGELOG.md +++ b/plugins/ilert/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-ilert +## 0.2.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.4-next.1 ### Patch Changes diff --git a/plugins/ilert/README.md b/plugins/ilert/README.md index c704108126..814baea507 100644 --- a/plugins/ilert/README.md +++ b/plugins/ilert/README.md @@ -26,7 +26,7 @@ In detail this plugin provides: Install the plugin: ```bash -# From the Backstage repository root +# From your Backstage root directory yarn add --cwd packages/app @backstage/plugin-ilert ``` diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json index 459f823255..757b91f78e 100644 --- a/plugins/ilert/package.json +++ b/plugins/ilert/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-ilert", "description": "A Backstage plugin that integrates towards iLert", - "version": "0.2.4-next.1", + "version": "0.2.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md index 9cbcc43f5f..00f1cdfe1b 100644 --- a/plugins/jenkins-backend/CHANGELOG.md +++ b/plugins/jenkins-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-jenkins-backend +## 0.1.32-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-jenkins-common@0.1.13-next.1 + - @backstage/plugin-permission-common@0.7.3 + ## 0.1.32-next.1 ### Patch Changes diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json index 43614edea2..d3107cba77 100644 --- a/plugins/jenkins-backend/package.json +++ b/plugins/jenkins-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins-backend", "description": "A Backstage backend plugin that integrates towards Jenkins", - "version": "0.1.32-next.1", + "version": "0.1.32-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins-common/CHANGELOG.md b/plugins/jenkins-common/CHANGELOG.md index 71719e20e2..75a1470ed0 100644 --- a/plugins/jenkins-common/CHANGELOG.md +++ b/plugins/jenkins-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-jenkins-common +## 0.1.13-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + ## 0.1.13-next.0 ### Patch Changes diff --git a/plugins/jenkins-common/package.json b/plugins/jenkins-common/package.json index c2d15a7da6..b37d1cfb8d 100644 --- a/plugins/jenkins-common/package.json +++ b/plugins/jenkins-common/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins-common", - "version": "0.1.13-next.0", + "version": "0.1.13-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 045236e8aa..291f175013 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-jenkins +## 0.7.14-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-jenkins-common@0.1.13-next.1 + ## 0.7.14-next.1 ### Patch Changes diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 2fc0ed67ca..d2c629136b 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-jenkins", "description": "A Backstage plugin that integrates towards Jenkins", - "version": "0.7.14-next.1", + "version": "0.7.14-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md index a07fb79fae..262cca0397 100644 --- a/plugins/kafka-backend/CHANGELOG.md +++ b/plugins/kafka-backend/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-kafka-backend +## 0.2.35-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + ## 0.2.35-next.1 ### Patch Changes diff --git a/plugins/kafka-backend/api-report.md b/plugins/kafka-backend/api-report.md index 20c8260b52..a748a543d9 100644 --- a/plugins/kafka-backend/api-report.md +++ b/plugins/kafka-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -10,6 +11,9 @@ import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; +// @alpha +export const kafkaPlugin: () => BackendFeature; + // @public (undocumented) export interface RouterOptions { // (undocumented) diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 93377dd9eb..f0c8d10bde 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -1,14 +1,15 @@ { "name": "@backstage/plugin-kafka-backend", "description": "A Backstage backend plugin that integrates towards Kafka", - "version": "0.2.35-next.1", + "version": "0.2.35-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "backend-plugin" @@ -26,7 +27,7 @@ "configSchema": "config.d.ts", "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -35,6 +36,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", @@ -54,6 +56,7 @@ }, "files": [ "dist", - "config.d.ts" + "config.d.ts", + "alpha" ] } diff --git a/plugins/kafka-backend/src/index.ts b/plugins/kafka-backend/src/index.ts index 70b7fb45a4..2000c8b89a 100644 --- a/plugins/kafka-backend/src/index.ts +++ b/plugins/kafka-backend/src/index.ts @@ -22,3 +22,4 @@ export type { RouterOptions } from './service/router'; export { createRouter } from './service/router'; +export { kafkaPlugin } from './plugin'; diff --git a/plugins/kafka-backend/src/plugin.ts b/plugins/kafka-backend/src/plugin.ts new file mode 100644 index 0000000000..2442eddbaa --- /dev/null +++ b/plugins/kafka-backend/src/plugin.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The Kafka backend plugin. + * + * @alpha + */ +export const kafkaPlugin = createBackendPlugin({ + pluginId: 'kafka', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ config, logger, httpRouter }) { + httpRouter.use( + await createRouter({ config, logger: loggerToWinstonLogger(logger) }), + ); + }, + }); + }, +}); diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 573a28a957..74d5b82c97 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-kafka +## 0.3.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.3.15-next.1 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 7583fd06b5..546cf976d5 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kafka", "description": "A Backstage plugin that integrates towards Kafka", - "version": "0.3.15-next.1", + "version": "0.3.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index fe00c7f8a1..a8726b238a 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-kubernetes-backend +## 0.9.3-next.2 + +### Patch Changes + +- 7ff81f7692: Kubernetes proxy endpoint now accepts content types that are not json +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-kubernetes-common@0.6.0-next.2 + ## 0.9.3-next.1 ### Patch Changes diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md index 4ec043a8f9..006f980c56 100644 --- a/plugins/kubernetes-backend/api-report.md +++ b/plugins/kubernetes-backend/api-report.md @@ -141,7 +141,7 @@ export class GoogleServiceAccountAuthTranslator ): Promise; } -// @alpha +// @public export const HEADER_KUBERNETES_CLUSTER: string; // @alpha (undocumented) diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 68225c489a..002a5e2651 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-backend", "description": "A Backstage backend plugin that integrates towards Kubernetes", - "version": "0.9.3-next.1", + "version": "0.9.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -36,7 +36,6 @@ "dependencies": { "@azure/identity": "^2.0.4", "@backstage/backend-common": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", @@ -66,6 +65,7 @@ "yn": "^4.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/aws4": "^1.5.1", "@types/http-proxy-middleware": "^0.19.3", diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts index 2766f9abfc..cc2dea26c0 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts @@ -31,6 +31,10 @@ import { import { KubernetesBuilder } from './KubernetesBuilder'; import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; import { CatalogApi } from '@backstage/catalog-client'; +import { HEADER_KUBERNETES_CLUSTER } from './KubernetesProxy'; +import { setupServer } from 'msw/node'; +import { setupRequestMockHandlers } from '@backstage/backend-test-utils'; +import { rest } from 'msw'; describe('KubernetesBuilder', () => { let app: express.Express; @@ -267,4 +271,65 @@ describe('KubernetesBuilder', () => { expect(response.status).toEqual(200); }); }); + + describe('post /proxy', () => { + const worker = setupServer(); + setupRequestMockHandlers(worker); + + beforeEach(() => { + worker.use( + rest.post('https://localhost:1234/api/v1/namespaces', (req, res, ctx) => + req + .arrayBuffer() + .then(body => + res( + ctx.set('content-type', `${req.headers.get('content-type')}`), + ctx.body(body), + ), + ), + ), + ); + }); + + it('returns the given request body', async () => { + const requestBody = { + kind: 'Namespace', + apiVersion: 'v1', + metadata: { + name: 'new-ns', + }, + }; + + const proxyEndpointRequest = request(app) + .post('/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .send(requestBody); + + worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); + + const response = await proxyEndpointRequest; + + expect(response.body).toStrictEqual(requestBody); + }); + + it('supports yaml content type', async () => { + const requestBody = `--- +kind: Namespace +apiVersion: v1 +metadata: + name: new-ns +`; + + const proxyEndpointRequest = request(app) + .post('/proxy/api/v1/namespaces') + .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster') + .set('content-type', 'application/yaml') + .send(requestBody); + + worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough())); + + const response = await proxyEndpointRequest; + expect(response.text).toEqual(requestBody); + }); + }); }); diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts index 1e7b704ec5..92773ece6d 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts @@ -265,6 +265,7 @@ export class KubernetesBuilder { ): express.Router { const logger = this.env.logger; const router = Router(); + router.use('/proxy', proxy.createRequestHandler()); router.use(express.json()); // @deprecated @@ -297,8 +298,6 @@ export class KubernetesBuilder { }); }); - router.use('/proxy', proxy.createRequestHandler()); - addResourceRoutesToRouter(router, catalogApi, objectsProvider); return router; diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts index 9d88d2ab84..cb36c647ac 100644 --- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts @@ -32,7 +32,7 @@ export const APPLICATION_JSON: string = 'application/json'; /** * The header that is used to specify the cluster name. * - * @alpha + * @public */ export const HEADER_KUBERNETES_CLUSTER: string = 'X-Kubernetes-Cluster'; diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md index 4f369bf289..cd4df4d54a 100644 --- a/plugins/kubernetes-common/CHANGELOG.md +++ b/plugins/kubernetes-common/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/plugin-kubernetes-common +## 0.6.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + ## 0.6.0-next.1 ### Patch Changes diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json index 21f6480aa1..4a4e2a30bf 100644 --- a/plugins/kubernetes-common/package.json +++ b/plugins/kubernetes-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-kubernetes-common", "description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin", - "version": "0.6.0-next.1", + "version": "0.6.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 2c375b8d0a..e30b168693 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-kubernetes +## 0.7.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-kubernetes-common@0.6.0-next.2 + ## 0.7.8-next.1 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 6c9c56db09..e51726b296 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.7.8-next.1", + "version": "0.7.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/lighthouse-backend/CHANGELOG.md b/plugins/lighthouse-backend/CHANGELOG.md index e69de29bb2..d64d035fa3 100644 --- a/plugins/lighthouse-backend/CHANGELOG.md +++ b/plugins/lighthouse-backend/CHANGELOG.md @@ -0,0 +1,18 @@ +# @backstage/plugin-lighthouse-backend + +## 0.1.0-next.0 + +### Minor Changes + +- eef62546ce: Introduce Lighthouse Backend Plugin to run scheduled Lighthouse Audits + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-common@0.1.0-next.0 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 diff --git a/plugins/lighthouse-backend/package.json b/plugins/lighthouse-backend/package.json index 560ed4e8f7..60b8570481 100644 --- a/plugins/lighthouse-backend/package.json +++ b/plugins/lighthouse-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse-backend", "description": "Backend functionalities for lighthouse", - "version": "0.0.0", + "version": "0.1.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/lighthouse-common/CHANGELOG.md b/plugins/lighthouse-common/CHANGELOG.md index e69de29bb2..189507371a 100644 --- a/plugins/lighthouse-common/CHANGELOG.md +++ b/plugins/lighthouse-common/CHANGELOG.md @@ -0,0 +1,12 @@ +# @backstage/plugin-lighthouse-common + +## 0.1.0-next.0 + +### Minor Changes + +- eef62546ce: Introduce @backstage/plugin-lighthouse-common with the API implementation + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 diff --git a/plugins/lighthouse-common/package.json b/plugins/lighthouse-common/package.json index 8dcc00960d..50281e17ba 100644 --- a/plugins/lighthouse-common/package.json +++ b/plugins/lighthouse-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse-common", "description": "Common functionalities for lighthouse, to be shared between lighthouse and lighthouse-backend plugin", - "version": "0.0.0", + "version": "0.1.0-next.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 1c54959f18..a79e4795e2 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-lighthouse +## 0.4.0-next.2 + +### Minor Changes + +- eef62546ce: Require @backstage/plugin-lighthouse-common package where API implementation moved to. + + **BREAKING**: the following types have been moved to @backstage/plugin-lighthouse-common: + + - `Audit` + - `AuditBase` + - `AuditCompleted` + - `AuditFailed` + - `AuditRunning` + - `FetchError` + - `LASListRequest` + - `LASListResponse` + - `LighthouseApi` + - `LighthouseCategoryAbbr` + - `LighthouseCategoryId` + - `LighthouseConfigSettings` + - `LighthouseRestApi` + - `TriggerAuditPayload` + - `Website` + - `WebsiteListResponse` + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-lighthouse-common@0.1.0-next.0 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.3.15-next.1 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index bee2b66a54..c52858b666 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-lighthouse", "description": "A Backstage plugin that integrates towards Lighthouse", - "version": "0.3.15-next.1", + "version": "0.4.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/lighthouse/src/components/Intro/index.tsx b/plugins/lighthouse/src/components/Intro/index.tsx index 57609476b8..8701c473aa 100644 --- a/plugins/lighthouse/src/components/Intro/index.tsx +++ b/plugins/lighthouse/src/components/Intro/index.tsx @@ -22,6 +22,7 @@ import LighthouseSupportButton from '../SupportButton'; import { ContentHeader, InfoCard, + Link, MarkdownContent, } from '@backstage/core-components'; @@ -50,8 +51,8 @@ with the environment variable \`LAS_CORS\` set to \`true\`._ When you have an instance running that Backstage can hook into, first install the plugin into your app: \`\`\`sh -cd packages/app -yarn add @backstage/plugin-lighthouse +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-lighthouse \`\`\` Modify your app routes in \`App.tsx\` to include the \`LighthousePage\` component exported from the plugin, for example: @@ -108,10 +109,9 @@ function GettingStartedCard() { diff --git a/plugins/linguist-backend/.eslintrc.js b/plugins/linguist-backend/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/linguist-backend/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/linguist-backend/README.md b/plugins/linguist-backend/README.md new file mode 100644 index 0000000000..cd63f5db03 --- /dev/null +++ b/plugins/linguist-backend/README.md @@ -0,0 +1,96 @@ +# Linguist Backend + +Welcome to the Linguist backend plugin! This plugin provides data for the Linguist frontend features. + +## Setup + +The following sections will help you get the Linguist Backend plugin setup and running. + +### Up and Running + +Here's how to get the backend up and running: + +1. First we need to add the `@backstage/plugin-linguist-backend` package to your backend: + + ```sh + # From the Backstage root directory + cd packages/backend + yarn add @backstage/plugin-linguist-backend + ``` + +2. Then we will create a new file named `packages/backend/src/plugins/linguist.ts`, and add the + following to it: + + ```ts + import { TaskScheduleDefinition } from '@backstage/backend-tasks'; + import { createRouter } from '@backstage/plugin-linguist-backend'; + import { Router } from 'express'; + import type { PluginEnvironment } from '../types'; + + export default async function createPlugin( + env: PluginEnvironment, + ): Promise { + const schedule: TaskScheduleDefinition = { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 90 }, + }; + + return createRouter({ schedule: schedule }, { ...env }); + } + ``` + +3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`: + + ```ts + import linguist from './plugins/linguist'; + // ... + async function main() { + // ... + // Add this line under the other lines that follow the useHotMemoize pattern + const linguistEnv = useHotMemoize(module, () => createEnv('linguist')); + // ... + // Insert this line under the other lines that add their routers to apiRouter in the same way + apiRouter.use('/linguist', await linguist(linguistEnv)); + ``` + +4. Now run `yarn start-backend` from the repo root +5. Finally open `http://localhost:7007/api/linguist/health` in a browser and it should return `{"status":"ok"}` + +## Batch Size + +The Linguist backend is setup to process entities by acting as a queue where it will pull down all the applicable entities from the Catalog and add them to it's database (saving just the `entityRef`). Then it will grab the `n` oldest entities that have not been processed to determine their languages and process them. To control the batch size simply provide that to the `createRouter` function in your `packages/backend/src/plugins/linguist.ts` like this: + +```ts +return createRouter({ schedule: schedule, batchSize: 40 }, { ...env }); +``` + +**Note:** The default batch size is 20 + +## Refresh + +The default setup will only generate the language breakdown for entities with the linguist annotation that have not been generated yet. If you want this process to also refresh the data you can do so by adding the `age` (as a `HumanDuration`) in your `packages/backend/src/plugins/linguist.ts` when you call `createRouter`: + +```ts +return createRouter({ schedule: schedule, age: { days: 30 } }, { ...env }); +``` + +With the `age` setup like this if the language breakdown is older than 15 days it will get regenerated. It's recommended that if you choose to use this configuration to set it to a large value - 30, 90, or 180 - as this data generally does not change drastically. + +## Use Source Location + +You may wish to use the `backstage.io/source-location` annotation over using the `backstage.io/linguist` as you may not be able to quickly add that annotation to your Entities. To do this you'll just need to set the `useSourceLocation` boolean to `true` in your `packages/backend/src/plugins/linguist.ts` when you call `createRouter`: + +```ts +return createRouter( + { schedule: schedule, useSourceLocation: true }, + { ...env }, +); +``` + +**Note:** This has the potential to cause a lot of processing, be very thoughtful about this before hand + +## Links + +- [Frontend part of the plugin](../linguist/README.md) +- [The Backstage homepage](https://backstage.io) diff --git a/plugins/linguist-backend/api-report.md b/plugins/linguist-backend/api-report.md new file mode 100644 index 0000000000..c11296b0b3 --- /dev/null +++ b/plugins/linguist-backend/api-report.md @@ -0,0 +1,102 @@ +## API Report File for "@backstage/plugin-linguist-backend" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +import { EntityResults } from '@backstage/plugin-linguist-common'; +import express from 'express'; +import { HumanDuration } from '@backstage/types'; +import { Knex } from 'knex'; +import { Languages } from '@backstage/plugin-linguist-common'; +import { Logger } from 'winston'; +import { PluginDatabaseManager } from '@backstage/backend-common'; +import { PluginEndpointDiscovery } from '@backstage/backend-common'; +import { PluginTaskScheduler } from '@backstage/backend-tasks'; +import { ProcessedEntity } from '@backstage/plugin-linguist-common'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; +import { UrlReader } from '@backstage/backend-common'; + +// @public (undocumented) +export function createRouter( + pluginOptions: PluginOptions, + routerOptions: RouterOptions, +): Promise; + +// @public (undocumented) +export class LinguistBackendApi { + constructor( + logger: Logger, + store: LinguistBackendStore, + urlReader: UrlReader, + discovery: PluginEndpointDiscovery, + age?: HumanDuration, + batchSize?: number, + useSourceLocation?: boolean, + ); + // (undocumented) + getEntityLanguages(entityRef: string): Promise; + // (undocumented) + processEntities(): Promise; +} + +// @public (undocumented) +export class LinguistBackendDatabase implements LinguistBackendStore { + constructor(db: Knex); + // (undocumented) + static create(knex: Knex): Promise; + // (undocumented) + getEntityResults(entityRef: string): Promise; + // (undocumented) + getProcessedEntities(): Promise; + // (undocumented) + getUnprocessedEntities(): Promise; + // (undocumented) + insertEntityResults(entityLanguages: EntityResults): Promise; + // (undocumented) + insertNewEntity(entityRef: string): Promise; +} + +// @public (undocumented) +export interface LinguistBackendStore { + // (undocumented) + getEntityResults(entityRef: string): Promise; + // (undocumented) + getProcessedEntities(): Promise; + // (undocumented) + getUnprocessedEntities(): Promise; + // (undocumented) + insertEntityResults(entityLanguages: EntityResults): Promise; + // (undocumented) + insertNewEntity(entityRef: string): Promise; +} + +// @public (undocumented) +export interface PluginOptions { + // (undocumented) + age?: HumanDuration; + // (undocumented) + batchSize?: number; + // (undocumented) + schedule?: TaskScheduleDefinition; + // (undocumented) + useSourceLocation?: boolean; +} + +// @public (undocumented) +export interface RouterOptions { + // (undocumented) + database: PluginDatabaseManager; + // (undocumented) + discovery: PluginEndpointDiscovery; + // (undocumented) + linguistBackendApi?: LinguistBackendApi; + // (undocumented) + logger: Logger; + // (undocumented) + reader: UrlReader; + // (undocumented) + scheduler?: PluginTaskScheduler; +} + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/linguist-backend/migrations/20221115_init.js b/plugins/linguist-backend/migrations/20221115_init.js new file mode 100644 index 0000000000..3bcf99e08b --- /dev/null +++ b/plugins/linguist-backend/migrations/20221115_init.js @@ -0,0 +1,63 @@ +/* + * Copyright 2022 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. + */ + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + // Note for the reader: the knex increments types automatically make it a + // primary column, whether you like it or not. That's why the id column is + // not marked as primary as one might have expected; it's only used for + // lookups by ID. Because, SQLite and MySQL don't return RETURNING on + // inserts ... so we want a manually generated key for lookups (an uuid), + // and also an index for ordering guarantees :) + await knex.schema.createTable('entity_result', table => { + table.comment('Table containing results from running Linguist'); + table + .bigIncrements('index') + .notNullable() + .comment('An insert counter to ensure ordering'); + table.uuid('id').notNullable().comment('The ID of the Linguist result'); + table + .text('entity_ref') + .unique() + .notNullable() + .comment('The entity ref that this Linguist result applies to'); + table.text('languages').comment('The results json as a string'); + table + .dateTime('created_at') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this entry was created'); + table + .dateTime('processed_date') + .defaultTo(knex.fn.now()) + .comment('The timestamp when this entity was processed'); + table.index('index', 'entity_result_index_idx'); + table.index('entity_ref', 'entity_result_entity_ref_idx'); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('entity_result', table => { + table.dropIndex([], 'entity_result_index_idx'); + table.dropIndex([], 'entity_result_entity_ref_idx'); + }); + await knex.schema.dropTable('entity_result'); +}; diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json new file mode 100644 index 0000000000..b1bf7be071 --- /dev/null +++ b/plugins/linguist-backend/package.json @@ -0,0 +1,56 @@ +{ + "name": "@backstage/plugin-linguist-backend", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "backend-plugin" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/backend-common": "workspace:^", + "@backstage/backend-tasks": "workspace:^", + "@backstage/catalog-client": "workspace:^", + "@backstage/catalog-model": "workspace:^", + "@backstage/config": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-auth-node": "workspace:^", + "@backstage/plugin-linguist-common": "workspace:^", + "@backstage/types": "workspace:^", + "@types/express": "*", + "express": "^4.18.1", + "express-promise-router": "^4.1.0", + "fs-extra": "^10.0.0", + "knex": "^2.0.0", + "linguist-js": "^2.5.3", + "luxon": "^2.0.2", + "node-fetch": "^2.6.7", + "uuid": "^8.3.2", + "winston": "^3.2.1", + "yn": "^4.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@types/supertest": "^2.0.8", + "msw": "^0.49.0", + "supertest": "^6.2.4" + }, + "files": [ + "dist", + "migrations/**/*.{js,d.ts}" + ] +} diff --git a/plugins/linguist-backend/src/api/LinguistBackendApi.ts b/plugins/linguist-backend/src/api/LinguistBackendApi.ts new file mode 100644 index 0000000000..34d04b02f9 --- /dev/null +++ b/plugins/linguist-backend/src/api/LinguistBackendApi.ts @@ -0,0 +1,218 @@ +/* + * Copyright 2022 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 { + EntitiesOverview, + EntityResults, + Language, + Languages, +} from '@backstage/plugin-linguist-common'; +import { + CATALOG_FILTER_EXISTS, + CatalogClient, + GetEntitiesRequest, +} from '@backstage/catalog-client'; +import { PluginEndpointDiscovery, UrlReader } from '@backstage/backend-common'; + +import { DateTime } from 'luxon'; +import { LINGUIST_ANNOTATION } from '@backstage/plugin-linguist-common'; +import { LinguistBackendStore } from '../db'; +import { Logger } from 'winston'; +import fs from 'fs-extra'; +import linguist from 'linguist-js'; +import { + ANNOTATION_SOURCE_LOCATION, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { assertError } from '@backstage/errors'; +import { HumanDuration } from '@backstage/types'; + +/** @public */ +export class LinguistBackendApi { + private readonly logger: Logger; + private readonly store: LinguistBackendStore; + private readonly urlReader: UrlReader; + private readonly discovery: PluginEndpointDiscovery; + private readonly catalogClient: CatalogClient; + private readonly age?: HumanDuration; + private readonly batchSize?: number; + private readonly useSourceLocation?: boolean; + public constructor( + logger: Logger, + store: LinguistBackendStore, + urlReader: UrlReader, + discovery: PluginEndpointDiscovery, + age?: HumanDuration, + batchSize?: number, + useSourceLocation?: boolean, + ) { + this.logger = logger; + this.store = store; + this.urlReader = urlReader; + this.discovery = discovery; + this.catalogClient = new CatalogClient({ discoveryApi: this.discovery }); + + this.batchSize = batchSize; + this.age = age; + this.useSourceLocation = useSourceLocation; + } + + public async getEntityLanguages(entityRef: string): Promise { + this.logger?.debug(`Getting languages for entity "${entityRef}"`); + + return this.store.getEntityResults(entityRef); + } + + public async processEntities() { + this.logger?.info('Updating list of entities'); + + await this.addNewEntities(); + + this.logger?.info('Processing applicable entities through Linguist'); + + await this.generateEntitiesLanguages(); + } + + private async addNewEntities() { + const annotationKey = this.useSourceLocation + ? ANNOTATION_SOURCE_LOCATION + : LINGUIST_ANNOTATION; + const request: GetEntitiesRequest = { + filter: { + kind: ['API', 'Component', 'Template'], + [`metadata.annotations.${annotationKey}`]: CATALOG_FILTER_EXISTS, + }, + fields: ['kind', 'metadata'], + }; + + const response = await this.catalogClient.getEntities(request); + const entities = response.items; + + entities.forEach(entity => { + const entityRef = stringifyEntityRef(entity); + this.store.insertNewEntity(entityRef); + }); + } + + private async generateEntitiesLanguages() { + const entitiesOverview = await this.getEntitiesOverview(); + this.logger?.info( + `Entities overview: Entity: ${entitiesOverview.entityCount}, Processed: ${entitiesOverview.processedCount}, Pending: ${entitiesOverview.pendingCount}, Stale ${entitiesOverview.staleCount}`, + ); + + const entities = entitiesOverview.filteredEntities.slice( + this.batchSize ?? 20, + ); + entities.forEach(async entityRef => { + const entity = await this.catalogClient.getEntityByRef(entityRef); + const annotationKey = this.useSourceLocation + ? ANNOTATION_SOURCE_LOCATION + : LINGUIST_ANNOTATION; + + let url = entity?.metadata.annotations?.[annotationKey] ?? ''; + if (url.startsWith('url:')) { + url = url.slice(4); + } + + try { + await this.generateEntityLanguages(entityRef, url); + } catch (error) { + assertError(error); + this.logger.error( + `Unable to process "${entityRef}" using "${url}", message: ${error.message}, stack: ${error.stack}`, + ); + } + }); + } + + private async getEntitiesOverview(): Promise { + this.logger?.debug('Getting pending entities'); + + const processedEntities = await this.store.getProcessedEntities(); + const staleEntities = processedEntities + .filter(pe => { + if (this.age === undefined) return false; + const staleDate = DateTime.now().minus(this.age as HumanDuration); + return DateTime.fromJSDate(pe.processedDate) <= staleDate; + }) + .map(pe => pe.entityRef); + + const unprocessedEntities = await this.store.getUnprocessedEntities(); + const filteredEntities = staleEntities.concat(unprocessedEntities); + + const entitiesOverview: EntitiesOverview = { + entityCount: unprocessedEntities.length, + processedCount: processedEntities.length, + staleCount: staleEntities.length, + pendingCount: filteredEntities.length, + filteredEntities: filteredEntities, + }; + + return entitiesOverview; + } + + private async generateEntityLanguages( + entityRef: string, + url: string, + ): Promise { + this.logger?.info( + `Processing languages for entity ${entityRef} from ${url}`, + ); + + const readTreeResponse = await this.urlReader.readTree(url); + const dir = await readTreeResponse.dir(); + + const results = await linguist(dir); + + try { + const totalBytes = results.languages.bytes; + const langResults = results.languages.results; + + const breakdown: Language[] = []; + for (const key in langResults) { + if (Object.prototype.hasOwnProperty.call(langResults, key)) { + const lang: Language = { + name: key, + percentage: +((langResults[key].bytes / totalBytes) * 100).toFixed( + 2, + ), + bytes: langResults[key].bytes, + type: langResults[key].type, + color: langResults[key].color, + }; + breakdown.push(lang); + } + } + + const languages: Languages = { + languageCount: results.languages.count, + totalBytes: totalBytes, + processedDate: new Date().toISOString(), + breakdown: breakdown, + }; + + const entityResults: EntityResults = { + entityRef: entityRef, + results: languages, + }; + + return await this.store.insertEntityResults(entityResults); + } finally { + this.logger?.info(`Cleaning up files from ${dir}`); + await fs.remove(dir); + } + } +} diff --git a/plugins/linguist-backend/src/api/index.ts b/plugins/linguist-backend/src/api/index.ts new file mode 100644 index 0000000000..a88fa051d4 --- /dev/null +++ b/plugins/linguist-backend/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { LinguistBackendApi } from './LinguistBackendApi'; diff --git a/plugins/linguist-backend/src/db/LinguistBackendDatabase.ts b/plugins/linguist-backend/src/db/LinguistBackendDatabase.ts new file mode 100644 index 0000000000..cf93374800 --- /dev/null +++ b/plugins/linguist-backend/src/db/LinguistBackendDatabase.ts @@ -0,0 +1,147 @@ +/* + * Copyright 2022 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 { resolvePackagePath } from '@backstage/backend-common'; +import { Knex } from 'knex'; +import { v4 as uuid } from 'uuid'; +import { + EntityResults, + Languages, + ProcessedEntity, +} from '@backstage/plugin-linguist-common'; + +export type RawDbEntityResultRow = { + id: string; + entity_ref: string; + languages: string; + processed_date: Date; +}; + +/** @public */ +export interface LinguistBackendStore { + insertEntityResults(entityLanguages: EntityResults): Promise; + insertNewEntity(entityRef: string): Promise; + getEntityResults(entityRef: string): Promise; + getProcessedEntities(): Promise; + getUnprocessedEntities(): Promise; +} + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-linguist-backend', + 'migrations', +); + +/** @public */ +export class LinguistBackendDatabase implements LinguistBackendStore { + static async create(knex: Knex): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return new LinguistBackendDatabase(knex); + } + + constructor(private readonly db: Knex) {} + + async insertEntityResults(entityLanguages: EntityResults): Promise { + const entityLanguageId = uuid(); + const entityRef = entityLanguages.entityRef; + + const [result] = await this.db('entity_result') + .insert({ + id: entityLanguageId, + entity_ref: entityRef, + languages: JSON.stringify(entityLanguages.results), + processed_date: new Date(entityLanguages.results.processedDate), + }) + .onConflict('entity_ref') + .merge(['languages', 'processed_date']) + .returning('id'); + + return result.id; + } + + async insertNewEntity(entityRef: string): Promise { + const entityLanguageId = uuid(); + + await this.db('entity_result') + .insert({ + id: entityLanguageId, + entity_ref: entityRef, + }) + .onConflict('entity_ref') + .ignore(); // If the entity_ref is in the table already then we don't want to add it again + } + + async getEntityResults(entityRef: string): Promise { + const entityResults = await this.db('entity_result') + .whereNotNull('languages') + .where({ entity_ref: entityRef }) + .first(); + + if (!entityResults) { + const emptyResults: Languages = { + languageCount: 0, + totalBytes: 0, + processedDate: 'undefined', + breakdown: [], + }; + return emptyResults; + } + + try { + return JSON.parse(entityResults.languages); + } catch (error) { + throw new Error(`Failed to parse languages for '${entityRef}', ${error}`); + } + } + + async getProcessedEntities(): Promise { + const rawEntities = await this.db('entity_result') + .whereNotNull('processed_date') + .whereNotNull('languages'); + + if (!rawEntities) { + return []; + } + + const processedEntities = rawEntities.map(rawEntity => { + const processEntity = { + entityRef: rawEntity.entity_ref, + processedDate: rawEntity.processed_date, + }; + + return processEntity; + }); + + return processedEntities; + } + + async getUnprocessedEntities(): Promise { + const rawEntities = await this.db('entity_result') + .whereNull('languages') + .orderBy('created_at', 'asc'); + + if (!rawEntities) { + return []; + } + + const unprocessedEntities = rawEntities.map(rawEntity => { + return rawEntity.entity_ref; + }); + + return unprocessedEntities; + } +} diff --git a/plugins/linguist-backend/src/db/index.ts b/plugins/linguist-backend/src/db/index.ts new file mode 100644 index 0000000000..c8ec15da68 --- /dev/null +++ b/plugins/linguist-backend/src/db/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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 { LinguistBackendDatabase } from './LinguistBackendDatabase'; +export type { LinguistBackendStore } from './LinguistBackendDatabase'; diff --git a/plugins/linguist-backend/src/index.ts b/plugins/linguist-backend/src/index.ts new file mode 100644 index 0000000000..da1e0ffb00 --- /dev/null +++ b/plugins/linguist-backend/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 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 './service/router'; +export { LinguistBackendApi } from './api'; +export { LinguistBackendDatabase } from './db'; +export type { LinguistBackendStore } from './db'; diff --git a/plugins/linguist-backend/src/run.ts b/plugins/linguist-backend/src/run.ts new file mode 100644 index 0000000000..d945aa13f0 --- /dev/null +++ b/plugins/linguist-backend/src/run.ts @@ -0,0 +1,33 @@ +/* + * Copyright 2022 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 { getRootLogger } from '@backstage/backend-common'; +import yn from 'yn'; +import { startStandaloneServer } from './service/standaloneServer'; + +const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007; +const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); +const logger = getRootLogger(); + +startStandaloneServer({ port, enableCors, logger }).catch(err => { + logger.error(err); + process.exit(1); +}); + +process.on('SIGINT', () => { + logger.info('CTRL+C pressed; exiting.'); + process.exit(0); +}); diff --git a/plugins/linguist-backend/src/service/router.test.ts b/plugins/linguist-backend/src/service/router.test.ts new file mode 100644 index 0000000000..34f16f4b21 --- /dev/null +++ b/plugins/linguist-backend/src/service/router.test.ts @@ -0,0 +1,92 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + DatabaseManager, + getVoidLogger, + PluginDatabaseManager, + PluginEndpointDiscovery, + UrlReaders, +} from '@backstage/backend-common'; +import express from 'express'; +import request from 'supertest'; +import { ConfigReader } from '@backstage/config'; +import { createRouter } from './router'; +import { LinguistBackendApi } from '../api'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; + +function createDatabase(): PluginDatabaseManager { + return DatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'better-sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('code-coverage'); +} + +const testDiscovery: jest.Mocked = { + getBaseUrl: jest + .fn() + .mockResolvedValue('http://localhost:7007/api/code-coverage'), + getExternalBaseUrl: jest.fn(), +}; + +const mockUrlReader = UrlReaders.default({ + logger: getVoidLogger(), + config: new ConfigReader({}), +}); + +const schedule: TaskScheduleDefinition = { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 15 }, +}; + +describe('createRouter', () => { + let linguistBackendApi: jest.Mocked; + let app: express.Express; + + beforeAll(async () => { + const router = await createRouter( + { schedule: schedule, age: { days: 30 }, useSourceLocation: false }, + { + linguistBackendApi, + discovery: testDiscovery, + database: createDatabase(), + reader: mockUrlReader, + logger: getVoidLogger(), + }, + ); + app = express().use(router); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('GET /health', () => { + it('returns ok', async () => { + const response = await request(app).get('/health'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual({ status: 'ok' }); + }); + }); +}); diff --git a/plugins/linguist-backend/src/service/router.ts b/plugins/linguist-backend/src/service/router.ts new file mode 100644 index 0000000000..e3aeec604e --- /dev/null +++ b/plugins/linguist-backend/src/service/router.ts @@ -0,0 +1,118 @@ +/* + * Copyright 2022 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 { + errorHandler, + PluginDatabaseManager, + PluginEndpointDiscovery, + UrlReader, +} from '@backstage/backend-common'; +import express from 'express'; +import Router from 'express-promise-router'; +import { Logger } from 'winston'; +import { LinguistBackendApi } from '../api'; +import { LinguistBackendDatabase } from '../db'; +import { + PluginTaskScheduler, + TaskScheduleDefinition, +} from '@backstage/backend-tasks'; +import { HumanDuration } from '@backstage/types'; + +/** @public */ +export interface PluginOptions { + schedule?: TaskScheduleDefinition; + age?: HumanDuration; + batchSize?: number; + useSourceLocation?: boolean; +} + +/** @public */ +export interface RouterOptions { + linguistBackendApi?: LinguistBackendApi; + logger: Logger; + reader: UrlReader; + database: PluginDatabaseManager; + discovery: PluginEndpointDiscovery; + scheduler?: PluginTaskScheduler; +} + +/** @public */ +export async function createRouter( + pluginOptions: PluginOptions, + routerOptions: RouterOptions, +): Promise { + const { schedule, age, batchSize, useSourceLocation } = pluginOptions; + + const { logger, reader, database, discovery, scheduler } = routerOptions; + + const linguistBackendStore = await LinguistBackendDatabase.create( + await database.getClient(), + ); + + const linguistBackendApi = + routerOptions.linguistBackendApi || + new LinguistBackendApi( + logger, + linguistBackendStore, + reader, + discovery, + age, + batchSize, + useSourceLocation, + ); + + if (scheduler && schedule) { + logger.info( + `Scheduling processing of entities with: ${JSON.stringify(schedule)}`, + ); + await scheduler.scheduleTask({ + id: 'linguist_process_entities', + frequency: schedule.frequency, + timeout: schedule.timeout, + initialDelay: schedule.initialDelay, + scope: schedule.scope, + fn: async () => { + await linguistBackendApi.processEntities(); + }, + }); + } + + const router = Router(); + router.use(express.json()); + + router.get('/health', (_, response) => { + response.send({ status: 'ok' }); + }); + + /** + * /entity-languages?entity=component:default/my-component + */ + router.get('/entity-languages', async (req, res) => { + const { entityRef: entityRef } = req.query; + + if (!entityRef) { + throw new Error('No entityRef was provided'); + } + + const entityLanguages = await linguistBackendApi.getEntityLanguages( + entityRef as string, + ); + res.status(200).json(entityLanguages); + }); + + router.use(errorHandler()); + return router; +} diff --git a/plugins/linguist-backend/src/service/standaloneServer.ts b/plugins/linguist-backend/src/service/standaloneServer.ts new file mode 100644 index 0000000000..828c83630a --- /dev/null +++ b/plugins/linguist-backend/src/service/standaloneServer.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2022 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 { + createServiceBuilder, + loadBackendConfig, + SingleHostDiscovery, + UrlReaders, + useHotMemoize, +} from '@backstage/backend-common'; +import { Server } from 'http'; +import { Logger } from 'winston'; +import { createRouter } from './router'; +import knexFactory from 'knex'; +import { TaskScheduleDefinition } from '@backstage/backend-tasks'; + +export interface ServerOptions { + port: number; + enableCors: boolean; + logger: Logger; +} + +export async function startStandaloneServer( + options: ServerOptions, +): Promise { + const logger = options.logger.child({ service: 'linguist-backend' }); + const config = await loadBackendConfig({ logger, argv: process.argv }); + const db = useHotMemoize(module, () => { + const knex = knexFactory({ + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }); + + knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => { + resource.run('PRAGMA foreign_keys = ON', () => {}); + }); + + return knex; + }); + + const schedule: TaskScheduleDefinition = { + frequency: { minutes: 2 }, + timeout: { minutes: 15 }, + initialDelay: { seconds: 15 }, + }; + + logger.debug('Starting application server...'); + const router = await createRouter( + { schedule: schedule, age: { days: 30 }, useSourceLocation: false }, + { + database: { getClient: async () => db }, + discovery: SingleHostDiscovery.fromConfig(config), + reader: UrlReaders.default({ logger, config }), + logger, + }, + ); + + let service = createServiceBuilder(module) + .setPort(options.port) + .addRouter('/linguist', router); + if (options.enableCors) { + service = service.enableCors({ origin: 'http://localhost:3000' }); + } + + return await service.start().catch(err => { + logger.error(err); + process.exit(1); + }); +} + +module.hot?.accept(); diff --git a/plugins/linguist-backend/src/setupTests.ts b/plugins/linguist-backend/src/setupTests.ts new file mode 100644 index 0000000000..813cdeaae3 --- /dev/null +++ b/plugins/linguist-backend/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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/linguist-common/.eslintrc.js b/plugins/linguist-common/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/linguist-common/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/linguist-common/README.md b/plugins/linguist-common/README.md new file mode 100644 index 0000000000..9408f7fcc5 --- /dev/null +++ b/plugins/linguist-common/README.md @@ -0,0 +1,3 @@ +# Linguist Common + +Common types, permissions, and constants for the Linguist plugin. diff --git a/plugins/linguist-common/api-report.md b/plugins/linguist-common/api-report.md new file mode 100644 index 0000000000..954bef7904 --- /dev/null +++ b/plugins/linguist-common/api-report.md @@ -0,0 +1,48 @@ +## API Report File for "@backstage/plugin-linguist-common" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +// @public (undocumented) +export type EntitiesOverview = { + entityCount: number; + processedCount: number; + pendingCount: number; + staleCount: number; + filteredEntities: string[]; +}; + +// @public (undocumented) +export type EntityResults = { + entityRef: string; + results: Languages; +}; + +// @public (undocumented) +export type Language = { + name: string; + percentage: number; + bytes: number; + type: string; + color?: `#${string}`; +}; + +// @public (undocumented) +export type Languages = { + languageCount: number; + totalBytes: number; + processedDate: string; + breakdown: Language[]; +}; + +// @public (undocumented) +export const LINGUIST_ANNOTATION = 'backstage.io/linguist'; + +// @public (undocumented) +export type ProcessedEntity = { + entityRef: string; + processedDate: Date; +}; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/linguist-common/package.json b/plugins/linguist-common/package.json new file mode 100644 index 0000000000..116f785216 --- /dev/null +++ b/plugins/linguist-common/package.json @@ -0,0 +1,31 @@ +{ + "name": "@backstage/plugin-linguist-common", + "description": "Common functionalities for the linguist plugin", + "version": "0.0.0", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.cjs.js", + "module": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "backstage": { + "role": "common-library" + }, + "scripts": { + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "devDependencies": { + "@backstage/cli": "workspace:^" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/linguist-common/src/constants.ts b/plugins/linguist-common/src/constants.ts new file mode 100644 index 0000000000..55b3d21d02 --- /dev/null +++ b/plugins/linguist-common/src/constants.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @public */ +export const LINGUIST_ANNOTATION = 'backstage.io/linguist'; diff --git a/plugins/linguist-common/src/index.ts b/plugins/linguist-common/src/index.ts new file mode 100644 index 0000000000..88fe08c714 --- /dev/null +++ b/plugins/linguist-common/src/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './types'; +export * from './constants'; diff --git a/plugins/linguist-common/src/setupTests.ts b/plugins/linguist-common/src/setupTests.ts new file mode 100644 index 0000000000..8b9b6bd586 --- /dev/null +++ b/plugins/linguist-common/src/setupTests.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 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/linguist-common/src/types.ts b/plugins/linguist-common/src/types.ts new file mode 100644 index 0000000000..28a25ed059 --- /dev/null +++ b/plugins/linguist-common/src/types.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/** @public */ +export type EntityResults = { + entityRef: string; + results: Languages; +}; + +/** @public */ +export type Languages = { + languageCount: number; + totalBytes: number; + processedDate: string; + breakdown: Language[]; +}; + +/** @public */ +export type Language = { + name: string; + percentage: number; + bytes: number; + type: string; + color?: `#${string}`; +}; + +/** @public */ +export type ProcessedEntity = { + entityRef: string; + processedDate: Date; +}; + +/** @public */ +export type EntitiesOverview = { + entityCount: number; + processedCount: number; + pendingCount: number; + staleCount: number; + filteredEntities: string[]; +}; diff --git a/plugins/linguist/.eslintrc.js b/plugins/linguist/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/linguist/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/linguist/README.md b/plugins/linguist/README.md new file mode 100644 index 0000000000..d5ca2199dd --- /dev/null +++ b/plugins/linguist/README.md @@ -0,0 +1,88 @@ +# Linguist + +Welcome to the Linguist plugin! + +## Features + +The Linguist plugin consists of a card that will give you the breakdown of the languages used by the configured Entity in the Catalog. + +When there is no Linguist data yet the card will be empty: + +![Example of empty Linguist card](./docs/linguist-no-data.png) + +Clicking on the refresh icon will generate the language breakdown: + +![Example of Linguist card with data](./docs/linguist-with-data.png) + +Here's an example where the permissions are hiding the refresh button: + +![Example of Linguist card without the refresh button](./docs/linguist-no-refresh.png) + +## Setup + +The following sections will help you get the Linguist plugin setup and running. + +### Backend + +You need to setup the [Linguist backend plugin](../linguist-backend/README.md) before you move forward with any of the following steps if you haven't already. + +### Entity Annotation + +To be able to use the Linguist plugin you need to add the following annotation to any entities you want to use it with: + +```yaml +backstage.io/linguist: https://url.to/your/srouce/code +``` + +Linguist uses the `UrlReader` to pull down the files before it scans them to determine the languages, this means you can pull files from all the supported providers - GitHub, GitLab, Bitbucket, Azure, Google GCS, AWS S3, etc. + +Here's what that will look like in action using the Backstage repo as an example: + +```yaml +# Example catalog-info.yaml entity definition file +apiVersion: backstage.io/v1alpha1 +kind: Component +metadata: + # ... + annotations: + backstage.io/linguist: https://github.com/backstage/backstage +spec: + type: service + # ... +``` + +### Frontend + +To setup the Linguist Card frontend you'll need to do the following steps: + +1. First we need to add the `@backstage/plugin-linguist` package to your frontend app: + + ```sh + # From your Backstage root directory + yarn add --cwd packages/app @backstage/plugin-linguist + ``` + +2. Second we need to add the `EntityLinguistCard` extension to the entity page in your app: + + ```tsx + // In packages/app/src/components/catalog/EntityPage.tsx + import { isLinguistAvailable, EntityLinguistCard } from '@backstage/plugin-linguist'; + + // For example in the Overview section + const overviewContent = ( + + // ... + + + + + + + + // ... + + ``` + +**Notes:** + +- The `if` prop is optional on the `EntitySwitch.Case`, you can remove it if you always want to see the tab even if the entity being viewed does not have the needed annotation diff --git a/plugins/linguist/api-report.md b/plugins/linguist/api-report.md new file mode 100644 index 0000000000..469b3b93c0 --- /dev/null +++ b/plugins/linguist/api-report.md @@ -0,0 +1,21 @@ +## API Report File for "@backstage/plugin-linguist" + +> 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'; + +// @public (undocumented) +export const EntityLinguistCard: () => JSX.Element; + +// @public (undocumented) +export const isLinguistAvailable: (entity: Entity) => boolean; + +// @public (undocumented) +export const linguistPlugin: BackstagePlugin<{}, {}, {}>; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/linguist/dev/index.tsx b/plugins/linguist/dev/index.tsx new file mode 100644 index 0000000000..bb6fb64b52 --- /dev/null +++ b/plugins/linguist/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2022 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 { createDevApp } from '@backstage/dev-utils'; +import { linguistPlugin, EntityLinguistCard } from '../src/plugin'; + +createDevApp() + .registerPlugin(linguistPlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/linguist', + }) + .render(); diff --git a/plugins/linguist/docs/linguist-no-data.png b/plugins/linguist/docs/linguist-no-data.png new file mode 100644 index 0000000000..208b8df5d5 Binary files /dev/null and b/plugins/linguist/docs/linguist-no-data.png differ diff --git a/plugins/linguist/docs/linguist-no-refresh.png b/plugins/linguist/docs/linguist-no-refresh.png new file mode 100644 index 0000000000..a3b4e4fde7 Binary files /dev/null and b/plugins/linguist/docs/linguist-no-refresh.png differ diff --git a/plugins/linguist/docs/linguist-with-data.png b/plugins/linguist/docs/linguist-with-data.png new file mode 100644 index 0000000000..e673f994fa Binary files /dev/null and b/plugins/linguist/docs/linguist-with-data.png differ diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json new file mode 100644 index 0000000000..18a9ca0ac5 --- /dev/null +++ b/plugins/linguist/package.json @@ -0,0 +1,63 @@ +{ + "name": "@backstage/plugin-linguist", + "version": "0.0.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/linguist" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/catalog-model": "workspace:^", + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/plugin-linguist-common": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.9.13", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "^4.0.0-alpha.57", + "luxon": "^2.0.2", + "react-use": "^17.2.4", + "slugify": "^1.6.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "*", + "cross-fetch": "^3.1.5", + "msw": "^0.49.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/linguist/src/api/LinguistApi.ts b/plugins/linguist/src/api/LinguistApi.ts new file mode 100644 index 0000000000..6593453f31 --- /dev/null +++ b/plugins/linguist/src/api/LinguistApi.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2022 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 { Languages } from '@backstage/plugin-linguist-common'; + +export const linguistApiRef = createApiRef({ + id: 'plugin.linguist.service', +}); + +export interface LinguistApi { + getLanguages(entityRef: string): Promise; +} diff --git a/plugins/linguist/src/api/LinguistClient.ts b/plugins/linguist/src/api/LinguistClient.ts new file mode 100644 index 0000000000..3e327ff21e --- /dev/null +++ b/plugins/linguist/src/api/LinguistClient.ts @@ -0,0 +1,59 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { Languages } from '@backstage/plugin-linguist-common'; +import { LinguistApi } from './LinguistApi'; + +export class LinguistClient implements LinguistApi { + private readonly discoveryApi: DiscoveryApi; + private readonly identityApi: IdentityApi; + + public constructor(options: { + discoveryApi: DiscoveryApi; + identityApi: IdentityApi; + }) { + this.discoveryApi = options.discoveryApi; + this.identityApi = options.identityApi; + } + + public async getLanguages(entityRef: string): Promise { + const queryString = new URLSearchParams(); + queryString.append('entityRef', entityRef); + + const urlSegment = `entity-languages?${queryString}`; + + const items = await this.get(urlSegment); + return items; + } + + private async get(path: string): Promise { + const baseUrl = `${await this.discoveryApi.getBaseUrl('linguist')}/`; + const url = new URL(path, baseUrl); + + const { token } = await this.identityApi.getCredentials(); + const response = await fetch(url.toString(), { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response.json() as Promise; + } +} diff --git a/plugins/linguist/src/api/index.ts b/plugins/linguist/src/api/index.ts new file mode 100644 index 0000000000..ca60b60d53 --- /dev/null +++ b/plugins/linguist/src/api/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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 './LinguistApi'; +export * from './LinguistClient'; diff --git a/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx new file mode 100644 index 0000000000..6014487d30 --- /dev/null +++ b/plugins/linguist/src/components/LinguistCard/LinguistCard.tsx @@ -0,0 +1,153 @@ +/* + * Copyright 2022 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 { + Box, + Chip, + Tooltip, + Typography, + makeStyles, + Grid, + useTheme, +} from '@material-ui/core'; +import { InfoCard, Progress } from '@backstage/core-components'; +import Alert from '@material-ui/lab/Alert'; +import { DateTime } from 'luxon'; +import React from 'react'; +import slugify from 'slugify'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { useLanguages } from '../../hooks'; + +const useStyles = makeStyles(theme => ({ + infoCard: { + marginBottom: theme.spacing(3), + }, + barContainer: { + height: theme.spacing(2), + marginBottom: theme.spacing(3), + borderRadius: '4px', + backgroundColor: 'transparent', + overflow: 'hidden', + }, + bar: { + height: '100%', + position: 'relative', + }, + languageDot: { + width: '10px', + height: '10px', + borderRadius: '50%', + marginRight: theme.spacing(1), + display: 'inline-block', + }, + label: { + color: 'inherit', + }, +})); + +export const LinguistCard = () => { + const classes = useStyles(); + const theme = useTheme(); + const { entity } = useEntity(); + const { items, loading, error } = useLanguages(entity); + let barWidth = 0; + + if (loading) { + return ; + } else if (error) { + return {error.message}; + } + + if (items && items.languageCount === 0 && items.totalBytes === 0) { + return ( + + + + + There is currently no language data for this entity. + + + + + ); + } + + const breakdown = items?.breakdown.sort((a, b) => + a.percentage < b.percentage ? 1 : -1, + ); + const processedDate = items?.processedDate; + + return breakdown && processedDate ? ( + + + {breakdown.map((language, index: number) => { + barWidth = barWidth + language.percentage; + return ( + + + + ); + })} + + + + {breakdown.map(languages => ( + + + {languages.name} - {languages.percentage}% + + } + variant="outlined" + key={slugify(languages.name, { lower: true })} + /> + ))} + + + + ) : ( + <> + ); +}; diff --git a/plugins/linguist/src/components/LinguistCard/index.ts b/plugins/linguist/src/components/LinguistCard/index.ts new file mode 100644 index 0000000000..aefae143fd --- /dev/null +++ b/plugins/linguist/src/components/LinguistCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { LinguistCard } from './LinguistCard'; diff --git a/plugins/linguist/src/hooks/index.ts b/plugins/linguist/src/hooks/index.ts new file mode 100644 index 0000000000..6f8913cf35 --- /dev/null +++ b/plugins/linguist/src/hooks/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 './useLanguages'; diff --git a/plugins/linguist/src/hooks/useLanguages.ts b/plugins/linguist/src/hooks/useLanguages.ts new file mode 100644 index 0000000000..3e99243e94 --- /dev/null +++ b/plugins/linguist/src/hooks/useLanguages.ts @@ -0,0 +1,41 @@ +/* + * Copyright 2022 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, stringifyEntityRef } from '@backstage/catalog-model'; +import { linguistApiRef } from '../api'; +import { useApi } from '@backstage/core-plugin-api'; +import useAsync from 'react-use/lib/useAsync'; + +import { Languages } from '@backstage/plugin-linguist-common'; + +export function useLanguages(entity: Entity): { + items?: Languages; + loading: boolean; + error?: Error; +} { + const entityRef = stringifyEntityRef(entity); + + const api = useApi(linguistApiRef); + const { value, loading, error } = useAsync(() => { + return api.getLanguages(entityRef); + }, [api, entityRef]); + + return { + items: value, + loading, + error, + }; +} diff --git a/plugins/linguist/src/index.ts b/plugins/linguist/src/index.ts new file mode 100644 index 0000000000..ce9cd98cbd --- /dev/null +++ b/plugins/linguist/src/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2022 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 { + linguistPlugin, + isLinguistAvailable, + EntityLinguistCard, +} from './plugin'; diff --git a/plugins/linguist/src/plugin.test.ts b/plugins/linguist/src/plugin.test.ts new file mode 100644 index 0000000000..d2a1016a22 --- /dev/null +++ b/plugins/linguist/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2022 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 { linguistPlugin } from './plugin'; + +describe('linguist', () => { + it('should export plugin', () => { + expect(linguistPlugin).toBeDefined(); + }); +}); diff --git a/plugins/linguist/src/plugin.ts b/plugins/linguist/src/plugin.ts new file mode 100644 index 0000000000..917a13ca9a --- /dev/null +++ b/plugins/linguist/src/plugin.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2022 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 { + createApiFactory, + createComponentExtension, + createPlugin, + discoveryApiRef, + identityApiRef, +} from '@backstage/core-plugin-api'; +import { linguistApiRef, LinguistClient } from './api'; +import { LINGUIST_ANNOTATION } from '@backstage/plugin-linguist-common'; +import { Entity } from '@backstage/catalog-model'; + +/** @public */ +export const isLinguistAvailable = (entity: Entity) => + Boolean(entity.metadata.annotations?.[LINGUIST_ANNOTATION]); + +/** @public */ +export const linguistPlugin = createPlugin({ + id: 'linguist', + apis: [ + createApiFactory({ + api: linguistApiRef, + deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, + factory: ({ discoveryApi, identityApi }) => + new LinguistClient({ discoveryApi, identityApi }), + }), + ], +}); + +/** @public */ +export const EntityLinguistCard = linguistPlugin.provide( + createComponentExtension({ + name: 'EntityLinguistCard', + component: { + lazy: () => import('./components/LinguistCard').then(m => m.LinguistCard), + }, + }), +); diff --git a/plugins/linguist/src/setupTests.ts b/plugins/linguist/src/setupTests.ts new file mode 100644 index 0000000000..2b5b615b19 --- /dev/null +++ b/plugins/linguist/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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/microsoft-calendar/.eslintrc.js b/plugins/microsoft-calendar/.eslintrc.js new file mode 100644 index 0000000000..e2a53a6ad2 --- /dev/null +++ b/plugins/microsoft-calendar/.eslintrc.js @@ -0,0 +1 @@ +module.exports = require('@backstage/cli/config/eslint-factory')(__dirname); diff --git a/plugins/microsoft-calendar/README.md b/plugins/microsoft-calendar/README.md new file mode 100644 index 0000000000..219a7cf2dc --- /dev/null +++ b/plugins/microsoft-calendar/README.md @@ -0,0 +1,51 @@ +# Microsoft-Calendar Plugin + +Welcome to the Microsoft calendar plugin! +_This plugin was created through the Backstage CLI_ + +## Features + +1. You can switch between calendars, using the select menu on the calendar card header. +2. Card showing the list of events on the selected date and the selected calendar (provided by Outlook calendar). +3. Link to join the online meeting on the event card if provided. so you can join your meetings right away hassle-free. +4. Hovering over the event will pop over a card showing the event summary message, and list of attendees. +5. attendee's chips will have a badge over them symbolizing their responses. + - green --> accepted + - red --> declined + - nothing --> not responded yet + +## Setup + +The following sections will help you set up the Microsoft calendar plugin. + +### Microsoft azure authentication provider + +> You need to setup [Microsoft Azure authentication provider](https://backstage.io/docs/auth/microsoft/provider), before you move forward with any of the below step if you haven't already. + +1. Install the plugin by running this command + +```bash +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-microsoft-calendar +``` + +2. Import the Microsoft calendar React component from `@backstage/plugin-microsoft-calendar`. +3. You can then use the provided React component `MicrosoftCalendar` in the backstage frontend where ever you want + +```tsx +import { MicrosoftCalendar } from '@backstage/plugin-microsoft-calendar'; + +// ... + + +; +// ... +``` + +![Microsoft Calendar plugin demo](https://user-images.githubusercontent.com/23618736/215717491-25db5fa6-b237-487f-8c00-28f572e8da05.mp4) + +![Sample](./docs/microsoft-calendar-plugin.png) + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/microsoft-calendar/api-report.md b/plugins/microsoft-calendar/api-report.md new file mode 100644 index 0000000000..d6839f19ea --- /dev/null +++ b/plugins/microsoft-calendar/api-report.md @@ -0,0 +1,39 @@ +## API Report File for "@backstage/plugin-microsoft-calendar" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { ApiRef } from '@backstage/core-plugin-api'; +import { Calendar } from '@microsoft/microsoft-graph-types'; +import { Event as Event_2 } from '@microsoft/microsoft-graph-types'; +import { FetchApi } from '@backstage/core-plugin-api'; +import { OAuthApi } from '@backstage/core-plugin-api'; + +// @public (undocumented) +export class MicrosoftCalendarApiClient { + constructor(options: { authApi: OAuthApi; fetchApi: FetchApi }); + // (undocumented) + getCalendars(): Promise; + // (undocumented) + getEvents( + calendarId: string, + params: { + startDateTime: string; + endDateTime: string; + }, + headers: { + [key in string]: any; + }, + ): Promise; +} + +// @public (undocumented) +export const microsoftCalendarApiRef: ApiRef; + +// @public (undocumented) +export const MicrosoftCalendarCard: () => JSX.Element; + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/microsoft-calendar/dev/index.tsx b/plugins/microsoft-calendar/dev/index.tsx new file mode 100644 index 0000000000..d611bca20c --- /dev/null +++ b/plugins/microsoft-calendar/dev/index.tsx @@ -0,0 +1,63 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { microsoftCalendarPlugin, MicrosoftCalendarCard } from '../src/plugin'; +import { microsoftCalendarApiRef } from '../src'; +import responseMock from './mock.json'; +import { microsoftAuthApiRef } from '@backstage/core-plugin-api'; +import { Content, Page } from '@backstage/core-components'; +import { Grid } from '@material-ui/core'; + +createDevApp() + .registerPlugin(microsoftCalendarPlugin) + .registerApi({ + api: microsoftAuthApiRef, + deps: {}, + factory: () => + ({ + async getAccessToken() { + return Promise.resolve('token'); + }, + } as unknown as typeof microsoftAuthApiRef.T), + }) + .registerApi({ + api: microsoftCalendarApiRef, + deps: {}, + factory: () => + ({ + async getCalendars() { + return Promise.resolve(responseMock.calendars); + }, + async getEvents() { + return Promise.resolve(responseMock.events); + }, + } as unknown as typeof microsoftCalendarApiRef.T), + }) + .addPage({ + element: ( + + + + + + + + ), + title: 'Microsoft-Calendar Plugin Demo', + path: '/microsoft-calendar', + }) + .render(); diff --git a/plugins/microsoft-calendar/dev/mock.json b/plugins/microsoft-calendar/dev/mock.json new file mode 100644 index 0000000000..b305e187eb --- /dev/null +++ b/plugins/microsoft-calendar/dev/mock.json @@ -0,0 +1,305 @@ +{ + "calendars": [ + { + "id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIyswAAAA==", + "name": "Calendar", + "color": "lightBlue", + "hexColor": "#9fe1e7", + "isDefaultCalendar": true, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADYw==", + "canShare": true, + "canViewPrivateItems": true, + "canEdit": true, + "allowedOnlineMeetingProviders": ["teamsForBusiness"], + "defaultOnlineMeetingProvider": "teamsForBusiness", + "isTallyingResponses": true, + "isRemovable": false, + "owner": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAEGAACkUjuDFCV2Q4tYcM6WC3rQAAAZs65BAAA=", + "name": "United Kingdom holidays", + "color": "auto", + "hexColor": "", + "isDefaultCalendar": false, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAGanqjg==", + "canShare": false, + "canViewPrivateItems": true, + "canEdit": false, + "allowedOnlineMeetingProviders": [], + "defaultOnlineMeetingProvider": "unknown", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAEGAACkUjuDFCV2Q4tYcM6WC3rQAAAZs65CAAA=", + "name": "Birthdays1", + "color": "auto", + "hexColor": "", + "isDefaultCalendar": false, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAGanrfQ==", + "canShare": false, + "canViewPrivateItems": true, + "canEdit": false, + "allowedOnlineMeetingProviders": [], + "defaultOnlineMeetingProvider": "unknown", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + { + "id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIytQAAAA==", + "name": "Birthdays", + "color": "auto", + "hexColor": "#92e1c0", + "isDefaultCalendar": false, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADdw==", + "canShare": false, + "canViewPrivateItems": true, + "canEdit": false, + "allowedOnlineMeetingProviders": [], + "defaultOnlineMeetingProvider": "unknown", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + { + "id": "AQMkADQyMDdlODU1AC05OGNiLTRhZmItOGM2Mi1kODZhMzJkYWU2OTAARgAAAwWXy9axQ31DtBuHDIFwgtYHAKRSO4MUJXZDi1hwzpYLetAAAAIBBgAAAKRSO4MUJXZDi1hwzpYLetAAAAIytgAAAA==", + "name": "Holidays in India", + "color": "auto", + "hexColor": "#16a765", + "isDefaultCalendar": false, + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAAAADug==", + "canShare": false, + "canViewPrivateItems": true, + "canEdit": false, + "allowedOnlineMeetingProviders": [], + "defaultOnlineMeetingProvider": "unknown", + "isTallyingResponses": false, + "isRemovable": true, + "owner": { + "name": "Abhay Soni", + "address": "abhay.s@statusneo.com" + } + } + ], + "events": [ + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", + "createdDateTime": "2022-12-22T08:28:36.6131501Z", + "lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z", + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "India Standard Time", + "originalEndTimeZone": "India Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "Abhay Soni", + "bodyPreview": "", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": false, + "responseRequested": true, + "seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", + "showAs": "tentative", + "type": "occurrence", + "webLink": "", + "onlineMeetingUrl": "", + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "notResponded", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "" + }, + "start": { + "dateTime": "2023-01-16T11:00:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "end": { + "dateTime": "2023-01-16T11:30:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [ + { + "type": "required", + "status": { + "response": "none", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Nishkarsh Raj", + "address": "nishkarshraj@nishkarsh.com" + } + }, + { + "type": "required", + "status": { + "response": "accepted", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Hakam Ram", + "address": "hakam_ram@ram.com" + } + }, + { + "type": "required", + "status": { + "response": "declined", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + } + ], + "organizer": { + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://abhay-soni-developer.github.io/MyReusme/" + } + }, + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", + "createdDateTime": "2022-12-22T08:28:36.6131501Z", + "lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z", + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "India Standard Time", + "originalEndTimeZone": "India Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "https://abhay-soni-developer.github.io/MyReusme/", + "bodyPreview": "", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": false, + "responseRequested": true, + "seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", + "showAs": "tentative", + "type": "occurrence", + "webLink": "", + "onlineMeetingUrl": "", + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "notResponded", + "time": "0001-01-01T00:00:00Z" + }, + "attendees": [ + { + "type": "required", + "status": { + "response": "none", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Nishkarsh Raj", + "address": "nishkarshraj@nishkarsh.com" + } + }, + { + "type": "required", + "status": { + "response": "accepted", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Hakam Ram", + "address": "hakam_ram@ram.com" + } + }, + { + "type": "required", + "status": { + "response": "declined", + "time": "0001-01-01T00:00:00Z" + }, + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + } + ], + "body": { + "contentType": "html", + "content": "" + }, + "start": { + "dateTime": "2023-01-16T11:00:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "end": { + "dateTime": "2023-01-16T11:30:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "organizer": { + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://abhay-soni-developer.github.io/MyReusme/" + } + } + ] +} diff --git a/plugins/microsoft-calendar/docs/microsoft-calendar-plugin.png b/plugins/microsoft-calendar/docs/microsoft-calendar-plugin.png new file mode 100644 index 0000000000..376ae73717 Binary files /dev/null and b/plugins/microsoft-calendar/docs/microsoft-calendar-plugin.png differ diff --git a/plugins/microsoft-calendar/package.json b/plugins/microsoft-calendar/package.json new file mode 100644 index 0000000000..2a2e0ca013 --- /dev/null +++ b/plugins/microsoft-calendar/package.json @@ -0,0 +1,63 @@ +{ + "name": "@backstage/plugin-microsoft-calendar", + "version": "0.0.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" + }, + "backstage": { + "role": "frontend-plugin" + }, + "author": { + "name": "Abhay Soni", + "email": "abhaysoni.developer@gmail.com", + "url": "https://github.com/Abhay-soni-developer" + }, + "scripts": { + "start": "backstage-cli package start", + "build": "backstage-cli package build", + "lint": "backstage-cli package lint", + "test": "backstage-cli package test", + "clean": "backstage-cli package clean", + "prepack": "backstage-cli package prepack", + "postpack": "backstage-cli package postpack" + }, + "dependencies": { + "@backstage/core-components": "workspace:^", + "@backstage/core-plugin-api": "workspace:^", + "@backstage/errors": "workspace:^", + "@backstage/theme": "workspace:^", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@microsoft/microsoft-graph-types": "^2.25.0", + "@tanstack/react-query": "^4.1.3", + "classnames": "^2.3.1", + "dompurify": "^2.3.6", + "lodash": "^4.17.21", + "luxon": "^3.0.0", + "material-ui-popup-state": "^1.9.3", + "react-use": "^17.2.4" + }, + "peerDependencies": { + "react": "^16.13.1 || ^17.0.0" + }, + "devDependencies": { + "@backstage/cli": "workspace:^", + "@backstage/core-app-api": "workspace:^", + "@backstage/dev-utils": "workspace:^", + "@backstage/test-utils": "workspace:^", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^12.1.3", + "@testing-library/user-event": "^14.0.0", + "@types/node": "^16.11.26", + "cross-fetch": "^3.1.5", + "msw": "^0.49.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/microsoft-calendar/src/api/client.ts b/plugins/microsoft-calendar/src/api/client.ts new file mode 100644 index 0000000000..fe842b5aef --- /dev/null +++ b/plugins/microsoft-calendar/src/api/client.ts @@ -0,0 +1,95 @@ +/* + * Copyright 2022 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 { OAuthApi, createApiRef, FetchApi } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { MicrosoftCalendarEvent, MicrosoftCalendar } from './types'; + +/** @public */ +export const microsoftCalendarApiRef = createApiRef( + { + id: 'plugin.microsoft-calendar.service', + }, +); + +/** @public */ +export class MicrosoftCalendarApiClient { + private readonly authApi: OAuthApi; + private readonly fetchApi: FetchApi; + + constructor(options: { authApi: OAuthApi; fetchApi: FetchApi }) { + this.authApi = options.authApi; + this.fetchApi = options.fetchApi; + } + + private async get( + path: string, + params: { [key in string]: any } = {}, + headers?: any, + ): Promise { + const query = new URLSearchParams(params); + const url = new URL( + `${path}?${query.toString()}`, + 'https://graph.microsoft.com', + ); + const token = await this.authApi.getAccessToken(); + let temp: any = {}; + + if (headers && typeof headers === 'object') { + temp = { + ...headers, + }; + } + + if (token) { + temp.Authorization = `Bearer ${token}`; + } + + const response = await this.fetchApi.fetch(url.toString(), { + headers: temp, + }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + + return response.json() as Promise; + } + + public async getCalendars(): Promise { + const data = await this.get<{ + id: string; + value: MicrosoftCalendar[]; + }>('v1.0/me/calendars'); + + return data.value; + } + + public async getEvents( + calendarId: string, + params: { + startDateTime: string; + endDateTime: string; + }, + headers: { [key in string]: any }, + ): Promise { + const data = await this.get<{ + id: string; + value: [MicrosoftCalendarEvent]; + }>(`v1.0/me/calendars/${calendarId}/calendarview`, params, headers); + return data.value; + } +} diff --git a/plugins/microsoft-calendar/src/api/index.ts b/plugins/microsoft-calendar/src/api/index.ts new file mode 100644 index 0000000000..db4616b586 --- /dev/null +++ b/plugins/microsoft-calendar/src/api/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './client'; +export * from './types'; diff --git a/plugins/microsoft-calendar/src/api/types.ts b/plugins/microsoft-calendar/src/api/types.ts new file mode 100644 index 0000000000..f4cb2811c7 --- /dev/null +++ b/plugins/microsoft-calendar/src/api/types.ts @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Calendar as MicrosoftCalendar, + Event as MicrosoftCalendarEvent, + Attendee, + ResponseStatus, + DateTimeTimeZone, + EmailAddress, +} from '@microsoft/microsoft-graph-types'; + +export type { + MicrosoftCalendar, + MicrosoftCalendarEvent, + Attendee, + ResponseStatus, + DateTimeTimeZone, + EmailAddress, +}; + +export const ResponseStatusMap = { + needsAction: 'none', + accepted: 'accepted', + declined: 'declined', + maybe: 'tentativelyAccepted', + notResponded: 'notResponded', +}; diff --git a/plugins/microsoft-calendar/src/components/AttendeeChip.tsx b/plugins/microsoft-calendar/src/components/AttendeeChip.tsx new file mode 100644 index 0000000000..67dd601da7 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/AttendeeChip.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2022 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 { BackstageTheme } from '@backstage/theme'; + +import { Badge, Chip, makeStyles } from '@material-ui/core'; +import CancelIcon from '@material-ui/icons/Cancel'; +import CheckIcon from '@material-ui/icons/CheckCircle'; + +import { Attendee, ResponseStatusMap } from '../api'; + +const useStyles = makeStyles((theme: BackstageTheme) => { + const getIconColor = (responseStatus?: string) => { + if (!responseStatus) return theme.palette.primary.light; + + return { + [ResponseStatusMap.accepted]: theme.palette.status.ok, + [ResponseStatusMap.declined]: theme.palette.status.error, + }[responseStatus]; + }; + + return { + responseStatus: { + color: ({ responseStatus }: { responseStatus?: string }) => + getIconColor(responseStatus), + }, + badge: { + right: 10, + top: 5, + '& svg': { + height: 16, + width: 16, + background: '#fff', + }, + }, + }; +}); + +const ResponseIcon = ({ responseStatus }: { responseStatus: string }) => { + if (responseStatus === ResponseStatusMap.accepted) { + return ; + } + if (responseStatus === ResponseStatusMap.declined) { + return ; + } + + return null; +}; + +type AttendeeChipProps = { + user: Attendee; +}; + +export const AttendeeChip = ({ user }: AttendeeChipProps) => { + const classes = useStyles({ responseStatus: user.status?.response || '' }); + + return ( + + } + > + + + ); +}; diff --git a/plugins/microsoft-calendar/src/components/CalendarCard.tsx b/plugins/microsoft-calendar/src/components/CalendarCard.tsx new file mode 100644 index 0000000000..dd94bad148 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/CalendarCard.tsx @@ -0,0 +1,141 @@ +/* + * Copyright 2022 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 { sortBy } from 'lodash'; +import { DateTime } from 'luxon'; +import React, { useState } from 'react'; + +import { InfoCard, Progress } from '@backstage/core-components'; + +import { Box, IconButton, Typography } from '@material-ui/core'; +import PrevIcon from '@material-ui/icons/NavigateBefore'; +import NextIcon from '@material-ui/icons/NavigateNext'; + +import { useCalendarsQuery, useEventsQuery, useSignIn } from '../hooks'; +import calendarIcon from '../icons/calendar.svg'; +import { CalendarEvent } from './CalendarEvent'; +import { CalendarSelect } from './CalendarSelect'; +import { SignInContent } from './SignInContent'; +import { getStartDate } from './util'; +import useAsync from 'react-use/lib/useAsync'; + +export const CalendarCard = () => { + const [date, setDate] = useState(DateTime.now()); + const [selectedCalendarId, setSelectedCalendarId] = useState(''); + + const changeDay = (offset = 1) => { + setDate(prev => prev.plus({ day: offset })); + }; + + const { isSignedIn, isInitialized, signIn } = useSignIn(); + + useAsync(async () => signIn(true), [signIn]); + + const { + isLoading: isCalendarLoading, + isFetching: isCalendarFetching, + data: calendars = [], + } = useCalendarsQuery({ + enabled: isSignedIn, + }); + + const defaultCalendarId = calendars.find(c => c.isDefaultCalendar)?.id; + + const { data: events, isLoading: isEventLoading } = useEventsQuery({ + calendarId: selectedCalendarId || defaultCalendarId || '', + enabled: isSignedIn && calendars.length > 0, + timeMin: date.startOf('day').toISO(), + timeMax: date.endOf('day').toISO(), + timeZone: date.zoneName, + }); + + const showLoader = + (isCalendarLoading && isCalendarFetching) || + isEventLoading || + !isInitialized; + + return ( + + + Microsoft Calendar + + {isSignedIn ? ( + <> + changeDay(-1)} size="small"> + + + changeDay(1)} size="small"> + + + + + {date.toLocaleString({ + weekday: 'short', + month: 'short', + day: 'numeric', + })} + + + + + + + ) : ( + Agenda + )} + + } + deepLink={{ + link: 'https://outlook.office.com/calendar/', + title: 'Go to Calendar', + }} + > + + {showLoader && ( + + + + )} + {!isSignedIn && isInitialized && ( + signIn(false)} /> + )} + {!isEventLoading && !isCalendarLoading && isSignedIn && ( + + {events?.length === 0 && ( + + + No events + + + )} + {sortBy(events, [getStartDate]).map(event => ( + + ))} + + )} + + + ); +}; diff --git a/plugins/microsoft-calendar/src/components/CalendarEvent.tsx b/plugins/microsoft-calendar/src/components/CalendarEvent.tsx new file mode 100644 index 0000000000..e56e25a09b --- /dev/null +++ b/plugins/microsoft-calendar/src/components/CalendarEvent.tsx @@ -0,0 +1,166 @@ +/* + * Copyright 2022 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 classnames from 'classnames'; +import { + bindPopover, + bindTrigger, + usePopupState, +} from 'material-ui-popup-state/hooks'; +import React, { useState } from 'react'; + +import { Link } from '@backstage/core-components'; + +import { + Box, + Paper, + Popover, + Tooltip, + Typography, + makeStyles, +} from '@material-ui/core'; + +import webcamIcon from '../icons/webcam.svg'; +import { CalendarEventPopoverContent } from './CalendarEventPopoverContent'; +import { MicrosoftCalendarEvent } from '../api'; +import { + getOnlineMeetingLink, + getTimePeriod, + isAllDay, + isPassed, +} from './util'; + +const useStyles = makeStyles( + theme => ({ + event: { + display: 'flex', + alignItems: 'center', + marginBottom: theme.spacing(1), + cursor: 'pointer', + paddingRight: 12, + }, + declined: { + textDecoration: 'line-through', + }, + passed: { + opacity: 0.6, + transition: 'opacity 0.15s ease-in-out', + '&:hover': { + opacity: 1, + }, + }, + link: { + width: 48, + height: 48, + display: 'inline-block', + padding: 8, + borderRadius: '50%', + '&:hover': { + backgroundColor: theme.palette.grey[100], + }, + }, + calendarColor: { + width: 8, + borderTopLeftRadius: 4, + borderBottomLeftRadius: 4, + }, + }), + { + name: 'MicrosoftCalendarEvent', + }, +); + +export const CalendarEvent = ({ event }: { event: MicrosoftCalendarEvent }) => { + const classes = useStyles(); + const popoverState = usePopupState({ + variant: 'popover', + popupId: event.id, + disableAutoFocus: true, + }); + const [hovered, setHovered] = useState(false); + const onlineMeetingLink = getOnlineMeetingLink(event); + + const { onClick, ...restBindProps } = bindTrigger(popoverState); + + return ( + <> + { + onClick(e); + }} + {...restBindProps} + onMouseEnter={() => setHovered(true)} + onMouseLeave={() => setHovered(false)} + elevation={hovered ? 4 : 1} + className={classnames(classes.event, { + [classes.passed]: isPassed(event), + })} + data-testid="microsoft-calendar-event" + > + + + + {event.subject} + + {!isAllDay(event) && ( + + {getTimePeriod(event)} + + )} + + + {event.isOnlineMeeting && ( + + { + e.stopPropagation(); + }} + noTrack + > + {/* we can use onlineMeetingProvider to show icon accordingly */} + Online Meeting link + + + )} + + + + + + + ); +}; diff --git a/plugins/microsoft-calendar/src/components/CalendarEventPopoverContent.tsx b/plugins/microsoft-calendar/src/components/CalendarEventPopoverContent.tsx new file mode 100644 index 0000000000..715a1ad276 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/CalendarEventPopoverContent.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2022 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 { sortBy } from 'lodash'; +import React from 'react'; +import DOMPurify from 'dompurify'; + +import { Link } from '@backstage/core-components'; +import { + Box, + Divider, + IconButton, + Tooltip, + Typography, + makeStyles, +} from '@material-ui/core'; +import ArrowForwardIcon from '@material-ui/icons/ArrowForward'; + +import { AttendeeChip } from './AttendeeChip'; +import { MicrosoftCalendarEvent } from '../api'; +import { getTimePeriod, getOnlineMeetingLink } from './util'; + +const useStyles = makeStyles( + theme => ({ + description: { + wordBreak: 'break-word', + '& a': { + color: theme.palette.primary.main, + fontWeight: 500, + }, + }, + divider: { + marginTop: theme.spacing(2), + marginBottom: theme.spacing(2), + }, + }), + { + name: 'MicrosoftCalendarEventPopoverContent', + }, +); + +type CalendarEventPopoverProps = { + event: MicrosoftCalendarEvent; +}; + +export const CalendarEventPopoverContent = ({ + event, +}: CalendarEventPopoverProps) => { + const classes = useStyles(); + const onlineMeetingLink = getOnlineMeetingLink(event); + + return ( + + + + {event.subject} + {getTimePeriod(event)} + + {event.webLink && ( + + {}} + noTrack + > + + + + + + )} + + {onlineMeetingLink && ( + {}} noTrack> + Join Online Meeting + + )} + + {event.bodyPreview && ( + <> + + + + )} + + {event.attendees && ( + <> + + + Attendees + + {sortBy(event.attendees || [], 'emailAddress').map(user => ( + + ))} + + + )} + + ); +}; diff --git a/plugins/microsoft-calendar/src/components/CalendarSelect.tsx b/plugins/microsoft-calendar/src/components/CalendarSelect.tsx new file mode 100644 index 0000000000..59e2376079 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/CalendarSelect.tsx @@ -0,0 +1,92 @@ +/* + * Copyright 2022 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 { sortBy } from 'lodash'; +import React from 'react'; + +import { + Checkbox, + FormControl, + Input, + ListItemText, + MenuItem, + Select, + Typography, + makeStyles, +} from '@material-ui/core'; +import { MicrosoftCalendar } from '../api'; + +const useStyles = makeStyles( + { + formControl: { + width: 120, + }, + selectedCalendars: { + textOverflow: 'ellipsis', + overflow: 'hidden', + }, + }, + { + name: 'MicrosoftCalendarSelect', + }, +); + +type CalendarSelectProps = { + disabled: boolean; + selectedCalendarId?: string; + setSelectedCalendarId: (value: string) => void; + calendars: MicrosoftCalendar[]; +}; + +export const CalendarSelect = ({ + disabled, + selectedCalendarId, + setSelectedCalendarId, + calendars, +}: CalendarSelectProps) => { + const classes = useStyles(); + return ( + + } + renderValue={selected => { + return ( + + {calendars.find(c => c.id === selected)?.name} + + ); + }} + MenuProps={{ + PaperProps: { + style: { + width: 350, + }, + }, + }} + > + {sortBy(calendars, 'name').map(c => ( + + + + + ))} + + + ); +}; diff --git a/plugins/microsoft-calendar/src/components/MicrosoftCalendar.tsx b/plugins/microsoft-calendar/src/components/MicrosoftCalendar.tsx new file mode 100644 index 0000000000..3bb15f7762 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/MicrosoftCalendar.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2022 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 { QueryClient, QueryClientProvider } from '@tanstack/react-query'; + +import { CalendarCard } from './CalendarCard'; + +const queryClient = new QueryClient(); + +export const MicrosoftCalendar = () => { + return ( + + + + ); +}; diff --git a/plugins/microsoft-calendar/src/components/SignInContent.tsx b/plugins/microsoft-calendar/src/components/SignInContent.tsx new file mode 100644 index 0000000000..d33698f68c --- /dev/null +++ b/plugins/microsoft-calendar/src/components/SignInContent.tsx @@ -0,0 +1,62 @@ +/* + * Copyright 2022 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, Button, styled } from '@material-ui/core'; +import { CalendarEvent } from './CalendarEvent'; +import mockEvents from './eventMock.json'; +import { MicrosoftCalendarEvent } from '../api'; + +type Props = { + handleAuthClick: React.MouseEventHandler; +}; + +const TransparentBox = styled(Box)({ + opacity: 0.3, + filter: 'blur(1.5px)', +}); + +export const SignInContent = ({ handleAuthClick }: Props) => { + return ( + + + {(mockEvents as MicrosoftCalendarEvent[]).map(event => ( + + ))} + + + + + + + ); +}; diff --git a/plugins/microsoft-calendar/src/components/eventMock.json b/plugins/microsoft-calendar/src/components/eventMock.json new file mode 100644 index 0000000000..b648d1b486 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/eventMock.json @@ -0,0 +1,138 @@ +[ + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", + "createdDateTime": "2022-12-22T08:28:36.6131501Z", + "lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z", + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "India Standard Time", + "originalEndTimeZone": "India Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "Abhay Soni", + "bodyPreview": "", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": false, + "responseRequested": true, + "seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", + "showAs": "tentative", + "type": "occurrence", + "webLink": "", + "onlineMeetingUrl": "", + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "notResponded", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "" + }, + "start": { + "dateTime": "2023-01-16T11:00:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "end": { + "dateTime": "2023-01-16T11:30:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [], + "organizer": { + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://abhay-soni-developer.github.io/MyReusme/" + } + }, + { + "id": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MAFRAAgI2vdUnGxAAEYAAAAABZfL1rFDfUO0G4cMgXCC1gcApFI7gxQldkOLWHDOlgt60AAAAAABDQAApFI7gxQldkOLWHDOlgt60AAAYl_I0AAAEA==", + "createdDateTime": "2022-12-22T08:28:36.6131501Z", + "lastModifiedDateTime": "2023-01-20T05:24:31.9666518Z", + "changeKey": "pFI7gxQldkOLWHDOlgt60AAAd5KhaA==", + "categories": [], + "transactionId": null, + "originalStartTimeZone": "India Standard Time", + "originalEndTimeZone": "India Standard Time", + "iCalUId": "040000008200E00074C5B7101A82E00807E7011006D3A5A92DC7D80100000000000000001000000002BB709705E28C4493E46124FF07DD89", + "reminderMinutesBeforeStart": 15, + "isReminderOn": true, + "hasAttachments": false, + "subject": "https://abhay-soni-developer.github.io/MyReusme/", + "bodyPreview": "", + "importance": "normal", + "sensitivity": "normal", + "isAllDay": false, + "isCancelled": false, + "isOrganizer": false, + "responseRequested": true, + "seriesMasterId": "AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=", + "showAs": "tentative", + "type": "occurrence", + "webLink": "", + "onlineMeetingUrl": "", + "isOnlineMeeting": true, + "onlineMeetingProvider": "teamsForBusiness", + "allowNewTimeProposals": true, + "occurrenceId": "OID.AAMkADQyMDdlODU1LTk4Y2ItNGFmYi04YzYyLWQ4NmEzMmRhZTY5MABGAAAAAAAFl8vWsUN9Q7QbhwyBcILWBwCkUjuDFCV2Q4tYcM6WC3rQAAAAAAENAACkUjuDFCV2Q4tYcM6WC3rQAABiX4jQAAA=.2023-01-16", + "isDraft": false, + "hideAttendees": false, + "responseStatus": { + "response": "notResponded", + "time": "0001-01-01T00:00:00Z" + }, + "body": { + "contentType": "html", + "content": "" + }, + "start": { + "dateTime": "2023-01-16T11:00:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "end": { + "dateTime": "2023-01-16T11:30:00.0000000", + "timeZone": "Asia/Calcutta" + }, + "location": { + "displayName": "", + "locationType": "default", + "uniqueIdType": "unknown", + "address": {}, + "coordinates": {} + }, + "locations": [], + "recurrence": null, + "attendees": [], + "organizer": { + "emailAddress": { + "name": "Abhay Soni", + "address": "abhaysoni.developer@gmail.com" + } + }, + "onlineMeeting": { + "joinUrl": "https://abhay-soni-developer.github.io/MyReusme/" + } + } +] diff --git a/plugins/microsoft-calendar/src/components/index.ts b/plugins/microsoft-calendar/src/components/index.ts new file mode 100644 index 0000000000..8149695804 --- /dev/null +++ b/plugins/microsoft-calendar/src/components/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 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 { MicrosoftCalendar } from './MicrosoftCalendar'; diff --git a/plugins/microsoft-calendar/src/components/util.ts b/plugins/microsoft-calendar/src/components/util.ts new file mode 100644 index 0000000000..dd52402d9d --- /dev/null +++ b/plugins/microsoft-calendar/src/components/util.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2022 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 { DateTime } from 'luxon'; + +import { MicrosoftCalendarEvent } from '../api'; + +export function getOnlineMeetingLink(event: MicrosoftCalendarEvent) { + const onlineEntrypoint = + event.onlineMeeting?.joinUrl || event.onlineMeetingUrl; + if (onlineEntrypoint) { + return onlineEntrypoint; + } + return ''; +} + +export function getTimePeriod(event: MicrosoftCalendarEvent) { + if (isAllDay(event)) { + return getAllDayTimePeriod(event); + } + + const format: Intl.DateTimeFormatOptions = { + hour: '2-digit', + minute: '2-digit', + }; + + const startTime = DateTime.fromISO(event.start?.dateTime || ''); + const endTime = DateTime.fromISO(event.end?.dateTime || ''); + + return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString( + format, + )}`; +} + +function getAllDayTimePeriod(event: MicrosoftCalendarEvent) { + const format: Intl.DateTimeFormatOptions = { month: 'long', day: 'numeric' }; + const startTime = DateTime.fromISO(event.start?.dateTime || ''); + const endTime = DateTime.fromISO(event.end?.dateTime || '').minus({ day: 1 }); + + if (startTime.toISO() === endTime.toISO()) { + return startTime.toLocaleString(format); + } + + return `${startTime.toLocaleString(format)} - ${endTime.toLocaleString( + format, + )}`; +} + +export function isPassed(event: MicrosoftCalendarEvent) { + if (!event.end?.dateTime) return false; + const eventDate = DateTime.fromISO(event.end?.dateTime!); + return DateTime.now() >= eventDate; +} + +export function isAllDay(event: MicrosoftCalendarEvent) { + const startTime = DateTime.fromISO(event.start?.dateTime || ''); + const endTime = DateTime.fromISO(event.end?.dateTime || ''); + + return endTime.diff(startTime, 'day').days >= 1; +} + +export function getStartDate(event: MicrosoftCalendarEvent) { + return DateTime.fromISO(event.start?.dateTime || ''); +} diff --git a/plugins/microsoft-calendar/src/hooks/index.ts b/plugins/microsoft-calendar/src/hooks/index.ts new file mode 100644 index 0000000000..a10f6a6c3a --- /dev/null +++ b/plugins/microsoft-calendar/src/hooks/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2022 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 { useCalendarsQuery } from './useCalendarsQuery'; +export { useEventsQuery } from './useEventsQuery'; +export { useSignIn } from './useSignIn'; diff --git a/plugins/microsoft-calendar/src/hooks/useCalendarsQuery.ts b/plugins/microsoft-calendar/src/hooks/useCalendarsQuery.ts new file mode 100644 index 0000000000..47a7990b58 --- /dev/null +++ b/plugins/microsoft-calendar/src/hooks/useCalendarsQuery.ts @@ -0,0 +1,52 @@ +/* + * Copyright 2022 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 { useQuery } from '@tanstack/react-query'; + +import { errorApiRef, useApi } from '@backstage/core-plugin-api'; + +import { microsoftCalendarApiRef } from '../api'; + +type Options = { + enabled: boolean; + refreshTime?: number; +}; + +export const useCalendarsQuery = ({ enabled }: Options) => { + const calendarApi = useApi(microsoftCalendarApiRef); + const errorApi = useApi(errorApiRef); + + return useQuery( + ['calendars-list'], + async () => { + const calendars = []; + const value = await calendarApi.getCalendars(); + calendars.push(...value); + return calendars; + }, + { + enabled, + // old data will be returned if last request was made within 3 mins. + staleTime: 180000, + refetchInterval: 3600000, + onError: () => { + errorApi.post({ + name: 'API error', + message: 'Failed to fetch calendars.', + }); + }, + }, + ); +}; diff --git a/plugins/microsoft-calendar/src/hooks/useEventsQuery.ts b/plugins/microsoft-calendar/src/hooks/useEventsQuery.ts new file mode 100644 index 0000000000..5e9dc76812 --- /dev/null +++ b/plugins/microsoft-calendar/src/hooks/useEventsQuery.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2022 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 { useQuery } from '@tanstack/react-query'; + +import { useApi } from '@backstage/core-plugin-api'; + +import { microsoftCalendarApiRef } from '../api'; + +type Options = { + calendarId: string; + timeMin: string; + timeMax: string; + enabled?: boolean; + timeZone?: string; + refreshTime?: number; +}; + +export const useEventsQuery = ({ + calendarId, + enabled, + timeMin, + timeMax, + timeZone, +}: Options) => { + const calendarApi = useApi(microsoftCalendarApiRef); + return useQuery( + ['calendarEvents', calendarId, timeMin, timeMax, timeZone], + async () => { + const data = await calendarApi.getEvents( + calendarId, + { + startDateTime: timeMin, + endDateTime: timeMax, + }, + { + Prefer: `outlook.timezone="${timeZone}"`, + }, + ); + return data; + }, + { + cacheTime: 300000, + enabled, + refetchInterval: 60000, + refetchIntervalInBackground: true, + }, + ); +}; diff --git a/plugins/microsoft-calendar/src/hooks/useSignIn.ts b/plugins/microsoft-calendar/src/hooks/useSignIn.ts new file mode 100644 index 0000000000..8b3ac54f92 --- /dev/null +++ b/plugins/microsoft-calendar/src/hooks/useSignIn.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2022 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 { useCallback, useState } from 'react'; + +import { microsoftAuthApiRef, useApi } from '@backstage/core-plugin-api'; + +export const useSignIn = () => { + const [isSignedIn, setSignedIn] = useState(false); + const [isInitialized, setInitialized] = useState(false); + const authApi = useApi(microsoftAuthApiRef); + + const signIn = useCallback( + async (optional = false) => { + const token = await authApi.getAccessToken('Calendars.Read', { + optional, + instantPopup: !optional, + }); + + setSignedIn(!!token); + setInitialized(true); + }, + [authApi, setSignedIn], + ); + + return { isSignedIn, isInitialized, signIn }; +}; diff --git a/plugins/microsoft-calendar/src/icons/calendar.svg b/plugins/microsoft-calendar/src/icons/calendar.svg new file mode 100644 index 0000000000..79895da87a --- /dev/null +++ b/plugins/microsoft-calendar/src/icons/calendar.svg @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/plugins/microsoft-calendar/src/icons/webcam.svg b/plugins/microsoft-calendar/src/icons/webcam.svg new file mode 100644 index 0000000000..356bb69215 --- /dev/null +++ b/plugins/microsoft-calendar/src/icons/webcam.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/plugins/microsoft-calendar/src/index.ts b/plugins/microsoft-calendar/src/index.ts new file mode 100644 index 0000000000..6867ca31ac --- /dev/null +++ b/plugins/microsoft-calendar/src/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { MicrosoftCalendarCard } from './plugin'; +export { MicrosoftCalendarApiClient, microsoftCalendarApiRef } from './api'; diff --git a/plugins/microsoft-calendar/src/plugin.test.ts b/plugins/microsoft-calendar/src/plugin.test.ts new file mode 100644 index 0000000000..7da86e176b --- /dev/null +++ b/plugins/microsoft-calendar/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { microsoftCalendarPlugin } from './plugin'; + +describe('microsoft-calendar', () => { + it('should export plugin', () => { + expect(microsoftCalendarPlugin).toBeDefined(); + }); +}); diff --git a/plugins/microsoft-calendar/src/plugin.ts b/plugins/microsoft-calendar/src/plugin.ts new file mode 100644 index 0000000000..644d368d97 --- /dev/null +++ b/plugins/microsoft-calendar/src/plugin.ts @@ -0,0 +1,49 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + createPlugin, + microsoftAuthApiRef, + fetchApiRef, + createApiFactory, + createComponentExtension, +} from '@backstage/core-plugin-api'; +import { + microsoftCalendarApiRef, + MicrosoftCalendarApiClient, +} from './api/index'; + +export const microsoftCalendarPlugin = createPlugin({ + id: 'microsoft-calendar', + apis: [ + createApiFactory({ + api: microsoftCalendarApiRef, + deps: { authApi: microsoftAuthApiRef, fetchApi: fetchApiRef }, + factory(deps) { + return new MicrosoftCalendarApiClient(deps); + }, + }), + ], +}); + +/** @public */ +export const MicrosoftCalendarCard = microsoftCalendarPlugin.provide( + createComponentExtension({ + name: 'MicrosoftCalendarCard', + component: { + lazy: () => import('./components').then(m => m.MicrosoftCalendar), + }, + }), +); diff --git a/plugins/microsoft-calendar/src/setupTests.ts b/plugins/microsoft-calendar/src/setupTests.ts new file mode 100644 index 0000000000..73dd8dce47 --- /dev/null +++ b/plugins/microsoft-calendar/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md index 3f9352d0d2..78aadfc390 100644 --- a/plugins/newrelic-dashboard/CHANGELOG.md +++ b/plugins/newrelic-dashboard/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-newrelic-dashboard +## 0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json index 09d4afcd93..5fe39fd8a3 100644 --- a/plugins/newrelic-dashboard/package.json +++ b/plugins/newrelic-dashboard/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic-dashboard", - "version": "0.2.8-next.1", + "version": "0.2.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index 10a7d6131b..8858555c7c 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-newrelic +## 0.3.33-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.3.33-next.0 ### Patch Changes diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 60f6019ccf..e02c690eff 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-newrelic", "description": "A Backstage plugin that integrates towards New Relic", - "version": "0.3.33-next.0", + "version": "0.3.33-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md index 6400654730..6af5e01b9b 100644 --- a/plugins/org-react/CHANGELOG.md +++ b/plugins/org-react/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-org-react +## 0.1.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.4-next.1 ### Patch Changes diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json index bf3fbe4050..6e939b1954 100644 --- a/plugins/org-react/package.json +++ b/plugins/org-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org-react", - "version": "0.1.4-next.1", + "version": "0.1.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index c4cd46928b..31c7843fa4 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-org +## 0.6.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.6.5-next.1 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index ef9106808b..240e473b88 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.6.5-next.1", + "version": "0.6.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx index cfb3d847a4..f183ab4d8e 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx @@ -123,7 +123,7 @@ export const ExtraDetails = () => ( - + diff --git a/plugins/org/src/components/Cards/Meta/LinksGroup.tsx b/plugins/org/src/components/Cards/Meta/LinksGroup.tsx index eeade18411..dfc654c14b 100644 --- a/plugins/org/src/components/Cards/Meta/LinksGroup.tsx +++ b/plugins/org/src/components/Cards/Meta/LinksGroup.tsx @@ -23,6 +23,7 @@ import { Divider, } from '@material-ui/core'; import React from 'react'; +import { Link } from '@backstage/core-components'; const WebLink = ({ href, @@ -33,9 +34,11 @@ const WebLink = ({ text?: string; Icon?: IconComponent; }) => ( - + {Icon ? : } - {text} + + {text} + ); diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index c10c36ee38..f9424baaea 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-pagerduty +## 0.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.5.8-next.1 ### Patch Changes diff --git a/plugins/pagerduty/README.md b/plugins/pagerduty/README.md index 7b3124ab0f..98f769e9f3 100644 --- a/plugins/pagerduty/README.md +++ b/plugins/pagerduty/README.md @@ -6,7 +6,7 @@ # How it Works - The Backstage PagerDuty plugin allows PagerDuty information about a Backstage entity to be displayed within Backstage. This includes active incidents, recent change events, as well as the current on-call responders' names, email addresses, and links to their profiles in PagerDuty. -- Incidents can be manually triggered via the plugin with a user-provided description, which will in turn notify the current on-call responders. +- Incidents can be manually triggered via the plugin with a user-provided description, which will in turn notify the current on-call responders (Alternatively, the plugin can be configured with an optional `readOnly` property to suppress the ability to trigger incidents from the plugin). - _Note: This feature is only available when providing the `pagerduty.com/integration-key` annotation_ - Change events will be displayed in a separate tab. If the change event payload has additional links the first link only will be rendered. @@ -145,6 +145,24 @@ pagerduty: eventsBaseUrl: 'https://events.pagerduty.com/v2' ``` +To suppress the rendering of the actionable incident-creation button, the `PagerDutyCard` can also be instantiated in `readOnly` mode: + +```ts + +``` + +**WARNING**: In current implementation, the PagerDuty plugin requires the `/pagerduty` proxy endpoint be exposed by the Backstage backend as an unprotected endpoint, in effect enabling PagerDuty API access using the configured `PAGERDUTY_TOKEN` for any user or process with access to the `/pagerduty` Backstage backend endpoint. If you regard this as problematic, consider using the plugin in `readOnly` mode (``) using the following proxy configuration: + +```yaml +proxy: + '/pagerduty': + target: https://api.pagerduty.com + headers: + Authorization: Token token=${PAGERDUTY_TOKEN} + # prohibit the `/pagerduty` proxy endpoint from servicing non-GET requests + allowedMethods: ['GET'] +``` + # How to Uninstall 1. Remove any configuration added in Backstage yaml files, such as the proxy configuration in `app-config.yaml` and the integration key in an entity's annotations. diff --git a/plugins/pagerduty/api-report.md b/plugins/pagerduty/api-report.md index 5bfe84e63f..b460086976 100644 --- a/plugins/pagerduty/api-report.md +++ b/plugins/pagerduty/api-report.md @@ -14,7 +14,7 @@ import { FetchApi } from '@backstage/core-plugin-api'; import { ReactNode } from 'react'; // @public (undocumented) -export const EntityPagerDutyCard: () => JSX.Element; +export const EntityPagerDutyCard: (props: PagerDutyCardProps) => JSX.Element; // @public (undocumented) const isPluginApplicableToEntity: (entity: Entity) => boolean; @@ -45,7 +45,12 @@ export type PagerDutyAssignee = { }; // @public (undocumented) -export const PagerDutyCard: () => JSX.Element; +export const PagerDutyCard: (props: PagerDutyCardProps) => JSX.Element; + +// @public (undocumented) +export type PagerDutyCardProps = { + readOnly?: boolean; +}; // @public (undocumented) export type PagerDutyChangeEvent = { diff --git a/plugins/pagerduty/dev/index.tsx b/plugins/pagerduty/dev/index.tsx index 676eb687a4..e45e0ffd8f 100644 --- a/plugins/pagerduty/dev/index.tsx +++ b/plugins/pagerduty/dev/index.tsx @@ -13,7 +13,153 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createDevApp } from '@backstage/dev-utils'; -import { pagerDutyPlugin } from '../src/plugin'; -createDevApp().registerPlugin(pagerDutyPlugin).render(); +import React from 'react'; +import { Entity } from '@backstage/catalog-model'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; +import { createDevApp } from '@backstage/dev-utils'; +import { pagerDutyPlugin, EntityPagerDutyCard } from '../src/plugin'; +import { pagerDutyApiRef } from '../src/api'; +import { PagerDutyApi, PagerDutyTriggerAlarmRequest } from '../src/api/types'; +import { + PagerDutyIncident, + PagerDutyChangeEvent, +} from '../src/components/types'; + +const mockEntity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'backstage', + description: 'backstage.io', + annotations: { + 'github.com/project-slug': 'backstage/backstage', + 'pagerduty.com/service-id': 'foo', + 'pagerduty.com/integration-key': 'foo', + }, + }, + spec: { + lifecycle: 'production', + type: 'website', + owner: 'user:guest', + }, +}; + +const mockPagerDutyApi: PagerDutyApi = { + async getServiceByEntity(entity: Entity) { + return { + service: { + name: entity.metadata.name, + integrationKey: 'key', + id: '123', + html_url: 'http://service', + escalation_policy: { + id: '123', + html_url: 'http://escalationpolicy', + user: { + id: '123', + summary: 'summary', + email: 'email@email.com', + html_url: 'http://user', + name: 'some-user', + }, + }, + }, + }; + }, + + async getIncidentsByServiceId(serviceId: string) { + const incident = (title: string) => { + return { + id: '123', + title: title, + status: 'acknowledged', + html_url: 'http://incident', + assignments: [ + { + assignee: { + id: '123', + summary: 'Jane Doe', + html_url: 'http://assignee', + }, + }, + ], + serviceId: serviceId, + created_at: '2015-10-06T21:30:42Z', + } as PagerDutyIncident; + }; + + return { + incidents: [ + incident('Some Alerting Incident'), + incident('Another Alerting Incident'), + ], + }; + }, + + async getChangeEventsByServiceId(serviceId: string) { + const changeEvent = (description: string) => { + return { + id: serviceId, + source: 'some-source', + html_url: 'http://changeevent', + links: [ + { + href: 'http://link', + text: 'link text', + }, + ], + summary: description, + timestamp: '2018-10-06T21:30:42Z', + } as PagerDutyChangeEvent; + }; + + return { + change_events: [ + changeEvent('us-east-1 deployment'), + changeEvent('us-west-2 deployment'), + ], + }; + }, + + async getOnCallByPolicyId() { + const oncall = (name: string, escalation: number) => { + return { + user: { + id: '123', + name: name, + html_url: 'http://assignee', + summary: 'summary', + email: 'email@email.com', + }, + escalation_level: escalation, + }; + }; + + return { + oncalls: [oncall('Jane Doe', 1), oncall('John Doe', 2)], + }; + }, + + async triggerAlarm(request: PagerDutyTriggerAlarmRequest) { + return new Response(request.description); + }, +}; + +createDevApp() + .registerApi({ + api: pagerDutyApiRef, + deps: {}, + factory: () => mockPagerDutyApi, + }) + .registerPlugin(pagerDutyPlugin) + .addPage({ + path: '/pagerduty', + title: 'PagerDuty', + element: ( + + + + ), + }) + .render(); diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index a26ac1137f..df20b498ed 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-pagerduty", "description": "A Backstage plugin that integrates towards PagerDuty", - "version": "0.5.8-next.1", + "version": "0.5.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx index 85202a9cfc..34c8914fde 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.test.tsx @@ -356,4 +356,27 @@ describe('PageDutyCard', () => { expect(getByText('Empty escalation policy')).toBeInTheDocument(); }); }); + + describe('when entity has all annotations but the plugin has been configured to be "read only"', () => { + it('queries by integration key but does not render the "Create Incident" button', async () => { + mockPagerDutyApi.getServiceByEntity = jest + .fn() + .mockImplementationOnce(async () => ({ service })); + + const { getByText, queryByTestId } = render( + wrapInTestApp( + + + + + , + ), + ); + await waitFor(() => !queryByTestId('progress')); + expect(getByText('Service Directory')).toBeInTheDocument(); + expect(getByText('Nice! No incidents found!')).toBeInTheDocument(); + expect(getByText('Empty escalation policy')).toBeInTheDocument(); + expect(() => getByText('Create Incident')).toThrow(); + }); + }); }); diff --git a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx index 15967fae01..fd23166d88 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard/index.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard/index.tsx @@ -54,7 +54,13 @@ export const isPluginApplicableToEntity = (entity: Entity) => ); /** @public */ -export const PagerDutyCard = () => { +export type PagerDutyCardProps = { + readOnly?: boolean; +}; + +/** @public */ +export const PagerDutyCard = (props: PagerDutyCardProps) => { + const { readOnly } = props; const { entity } = useEntity(); const pagerDutyEntity = getPagerDutyEntity(entity); const api = useApi(pagerDutyApiRef); @@ -156,7 +162,11 @@ export const PagerDutyCard = () => { title="PagerDuty" subheader={ } /> diff --git a/plugins/pagerduty/src/components/index.ts b/plugins/pagerduty/src/components/index.ts index 17bb2c03dc..e7c1ff4e72 100644 --- a/plugins/pagerduty/src/components/index.ts +++ b/plugins/pagerduty/src/components/index.ts @@ -23,6 +23,8 @@ export type { PagerDutyUser, } from './types'; +export type { PagerDutyCardProps } from './PagerDutyCard'; + export { isPluginApplicableToEntity, isPluginApplicableToEntity as isPagerDutyAvailable, diff --git a/plugins/periskop-backend/CHANGELOG.md b/plugins/periskop-backend/CHANGELOG.md index afeec402d4..1e6ff6f307 100644 --- a/plugins/periskop-backend/CHANGELOG.md +++ b/plugins/periskop-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-periskop-backend +## 0.1.13-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/periskop-backend/api-report.md b/plugins/periskop-backend/api-report.md index caaed109f9..7906bc4785 100644 --- a/plugins/periskop-backend/api-report.md +++ b/plugins/periskop-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -10,6 +11,9 @@ import { Logger } from 'winston'; // @public (undocumented) export function createRouter(options: RouterOptions): Promise; +// @alpha +export const periskopPlugin: () => BackendFeature; + // @public (undocumented) export interface RouterOptions { // (undocumented) diff --git a/plugins/periskop-backend/package.json b/plugins/periskop-backend/package.json index aa6d8daec3..419d430fac 100644 --- a/plugins/periskop-backend/package.json +++ b/plugins/periskop-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop-backend", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,11 +11,12 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -24,6 +25,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@types/express": "*", "express": "^4.17.1", @@ -39,6 +41,7 @@ "supertest": "^6.1.6" }, "files": [ - "dist" + "dist", + "alpha" ] } diff --git a/plugins/periskop-backend/src/index.ts b/plugins/periskop-backend/src/index.ts index ca73cb27ba..a053633745 100644 --- a/plugins/periskop-backend/src/index.ts +++ b/plugins/periskop-backend/src/index.ts @@ -15,3 +15,4 @@ */ export * from './service/router'; +export { periskopPlugin } from './plugin'; diff --git a/plugins/periskop-backend/src/plugin.ts b/plugins/periskop-backend/src/plugin.ts new file mode 100644 index 0000000000..933c864d50 --- /dev/null +++ b/plugins/periskop-backend/src/plugin.ts @@ -0,0 +1,45 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + coreServices, + createBackendPlugin, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The Periskop backend plugin. + * + * @alpha + */ +export const periskopPlugin = createBackendPlugin({ + pluginId: 'periskop', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ config, logger, httpRouter }) { + httpRouter.use( + await createRouter({ config, logger: loggerToWinstonLogger(logger) }), + ); + }, + }); + }, +}); diff --git a/plugins/periskop/CHANGELOG.md b/plugins/periskop/CHANGELOG.md index 5d093e566f..eeabe21052 100644 --- a/plugins/periskop/CHANGELOG.md +++ b/plugins/periskop/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-periskop +## 0.1.13-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.13-next.1 ### Patch Changes diff --git a/plugins/periskop/package.json b/plugins/periskop/package.json index a35c7332b2..08d875e51f 100644 --- a/plugins/periskop/package.json +++ b/plugins/periskop/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-periskop", - "version": "0.1.13-next.1", + "version": "0.1.13-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/permission-backend/CHANGELOG.md b/plugins/permission-backend/CHANGELOG.md index 475ec1fa67..f8cb24795b 100644 --- a/plugins/permission-backend/CHANGELOG.md +++ b/plugins/permission-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-permission-backend +## 0.5.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + ## 0.5.17-next.1 ### Patch Changes diff --git a/plugins/permission-backend/package.json b/plugins/permission-backend/package.json index ef319d2e4b..b0b54f6b6e 100644 --- a/plugins/permission-backend/package.json +++ b/plugins/permission-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-permission-backend", - "version": "0.5.17-next.1", + "version": "0.5.17-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md index 2c56e28584..51c27671db 100644 --- a/plugins/permission-node/CHANGELOG.md +++ b/plugins/permission-node/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-permission-node +## 0.7.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + ## 0.7.5-next.1 ### Patch Changes diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json index e2de41927f..c43904fd00 100644 --- a/plugins/permission-node/package.json +++ b/plugins/permission-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-permission-node", "description": "Common permission and authorization utilities for backend plugins", - "version": "0.7.5-next.1", + "version": "0.7.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/playlist-backend/CHANGELOG.md b/plugins/playlist-backend/CHANGELOG.md index ee9c9940b1..c634e3c2e0 100644 --- a/plugins/playlist-backend/CHANGELOG.md +++ b/plugins/playlist-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-playlist-backend +## 0.2.5-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-playlist-common@0.1.4 + ## 0.2.5-next.1 ### Patch Changes diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json index 211ef7f3c9..cc3e62eb5a 100644 --- a/plugins/playlist-backend/package.json +++ b/plugins/playlist-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist-backend", - "version": "0.2.5-next.1", + "version": "0.2.5-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -23,7 +23,6 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", @@ -43,6 +42,7 @@ "zod": "~3.18.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/supertest": "^2.0.8", "msw": "^0.49.0", diff --git a/plugins/playlist-backend/src/permissions/rules.ts b/plugins/playlist-backend/src/permissions/rules.ts index 99f4725972..e29465ed0f 100644 --- a/plugins/playlist-backend/src/permissions/rules.ts +++ b/plugins/playlist-backend/src/permissions/rules.ts @@ -31,10 +31,10 @@ const createPlaylistPermissionRule = makeCreatePermissionRule< const isOwner = createPlaylistPermissionRule({ name: 'IS_OWNER', - description: 'Should allow only if the playlist belongs to the user', + description: 'Allow playlists owned by the given entity refs', resourceType: PLAYLIST_LIST_RESOURCE_TYPE, paramsSchema: z.object({ - owners: z.array(z.string()).describe('List of owner entity refs'), + owners: z.array(z.string()).describe('List of entity refs to match on'), }), apply: (list: PlaylistMetadata, { owners }) => owners.includes(list.owner), toQuery: ({ owners }) => ({ @@ -45,7 +45,7 @@ const isOwner = createPlaylistPermissionRule({ const isPublic = createPlaylistPermissionRule({ name: 'IS_PUBLIC', - description: 'Should allow only if the playlist is public', + description: 'Allow playlists that are set as public', resourceType: PLAYLIST_LIST_RESOURCE_TYPE, apply: (list: PlaylistMetadata) => list.public, toQuery: () => ({ key: 'public', values: [true] }), diff --git a/plugins/playlist/CHANGELOG.md b/plugins/playlist/CHANGELOG.md index 8651137680..ea27ee40f3 100644 --- a/plugins/playlist/CHANGELOG.md +++ b/plugins/playlist/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-playlist +## 0.1.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-playlist-common@0.1.4 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json index 9954d9b45c..624fdbcd75 100644 --- a/plugins/playlist/package.json +++ b/plugins/playlist/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-playlist", - "version": "0.1.6-next.1", + "version": "0.1.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md index 1ccb8ba717..ad235f36e8 100644 --- a/plugins/proxy-backend/CHANGELOG.md +++ b/plugins/proxy-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-proxy-backend +## 0.2.36-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + ## 0.2.36-next.1 ### Patch Changes diff --git a/plugins/proxy-backend/api-report.md b/plugins/proxy-backend/api-report.md index 581e7a904c..741f7c3b54 100644 --- a/plugins/proxy-backend/api-report.md +++ b/plugins/proxy-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import { Config } from '@backstage/config'; import express from 'express'; import { Logger } from 'winston'; @@ -11,6 +12,15 @@ import { PluginEndpointDiscovery } from '@backstage/backend-common'; // @public export function createRouter(options: RouterOptions): Promise; +// @alpha +export const proxyPlugin: ( + options?: + | { + skipInvalidProxies?: boolean | undefined; + } + | undefined, +) => BackendFeature; + // @public (undocumented) export interface RouterOptions { // (undocumented) diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index d145d91454..76440ce0a9 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -1,14 +1,15 @@ { "name": "@backstage/plugin-proxy-backend", "description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend", - "version": "0.2.36-next.1", + "version": "0.2.36-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "backstage": { "role": "backend-plugin" @@ -24,7 +25,7 @@ ], "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -33,6 +34,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/config": "workspace:^", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -56,7 +58,8 @@ }, "files": [ "dist", - "config.d.ts" + "config.d.ts", + "alpha" ], "configSchema": "config.d.ts" } diff --git a/plugins/proxy-backend/src/index.ts b/plugins/proxy-backend/src/index.ts index 8f5d5c79dc..2658a557b7 100644 --- a/plugins/proxy-backend/src/index.ts +++ b/plugins/proxy-backend/src/index.ts @@ -21,3 +21,4 @@ */ export * from './service'; +export { proxyPlugin } from './plugin'; diff --git a/plugins/proxy-backend/src/plugin.ts b/plugins/proxy-backend/src/plugin.ts new file mode 100644 index 0000000000..d095bee527 --- /dev/null +++ b/plugins/proxy-backend/src/plugin.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { loggerToWinstonLogger } from '@backstage/backend-common'; +import { + createBackendPlugin, + coreServices, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The proxy backend plugin. + * + * @alpha + */ +export const proxyPlugin = createBackendPlugin( + (options?: { skipInvalidProxies?: boolean }) => ({ + pluginId: 'proxy', + register(env) { + env.registerInit({ + deps: { + config: coreServices.config, + discovery: coreServices.discovery, + logger: coreServices.logger, + httpRouter: coreServices.httpRouter, + }, + async init({ config, discovery, logger, httpRouter }) { + httpRouter.use( + await createRouter({ + config, + discovery, + logger: loggerToWinstonLogger(logger), + skipInvalidProxies: options?.skipInvalidProxies, + }), + ); + }, + }); + }, + }), +); diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md index 0995c42725..9e6955c033 100644 --- a/plugins/rollbar-backend/CHANGELOG.md +++ b/plugins/rollbar-backend/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-rollbar-backend +## 0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + ## 0.1.39-next.1 ### Patch Changes diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 8aadcde236..876c797a78 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar-backend", "description": "A Backstage backend plugin that integrates towards Rollbar", - "version": "0.1.39-next.1", + "version": "0.1.39-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 8fab9cd0c6..1fe3b3d6ba 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-rollbar +## 0.4.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.4.15-next.1 ### Patch Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index e33ed45d20..2d65565015 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-rollbar", "description": "A Backstage plugin that integrates towards Rollbar", - "version": "0.4.15-next.1", + "version": "0.4.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md index 2ca4243b25..65121ebda8 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-cookiecutter +## 0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + ## 0.2.17-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json index 6f2868d2f6..669f498c1e 100644 --- a/plugins/scaffolder-backend-module-cookiecutter/package.json +++ b/plugins/scaffolder-backend-module-cookiecutter/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-cookiecutter", "description": "A module for the scaffolder backend that lets you template projects using cookiecutter", - "version": "0.2.17-next.1", + "version": "0.2.17-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md index 8fc899ef79..51a6a9b796 100644 --- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-scaffolder-backend-module-rails +## 0.4.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-scaffolder-backend@1.11.0-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + ## 0.4.10-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json index 21e9c1f31c..1e4d881ef0 100644 --- a/plugins/scaffolder-backend-module-rails/package.json +++ b/plugins/scaffolder-backend-module-rails/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-backend-module-rails", "description": "A module for the scaffolder backend that lets you template projects using Rails", - "version": "0.4.10-next.1", + "version": "0.4.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md index 7ad3b35da6..9a7df7e14b 100644 --- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-sentry +## 0.1.2-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + ## 0.1.2-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json index 2136a9055e..903ad54dc1 100644 --- a/plugins/scaffolder-backend-module-sentry/package.json +++ b/plugins/scaffolder-backend-module-sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-sentry", - "version": "0.1.2-next.1", + "version": "0.1.2-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md index d736daf17e..5c4acbd85c 100644 --- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md +++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-scaffolder-backend-module-yeoman +## 0.2.15-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + ## 0.2.15-next.1 ### Patch Changes diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json index eb8598ac8f..7ae88b60e6 100644 --- a/plugins/scaffolder-backend-module-yeoman/package.json +++ b/plugins/scaffolder-backend-module-yeoman/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend-module-yeoman", - "version": "0.2.15-next.1", + "version": "0.2.15-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index b43edb7c73..d6db10f4ae 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-scaffolder-backend +## 1.11.0-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-catalog-backend@1.7.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-catalog-node@1.3.3-next.2 + - @backstage/plugin-scaffolder-node@0.1.0-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + ## 1.11.0-next.1 ### Minor Changes diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index e50ebba1b8..ccc586f55b 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": "1.11.0-next.1", + "version": "1.11.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-backend/scripts/build-nunjucks.js b/plugins/scaffolder-backend/scripts/build-nunjucks.js index 8b98ac4305..8183de77ee 100755 --- a/plugins/scaffolder-backend/scripts/build-nunjucks.js +++ b/plugins/scaffolder-backend/scripts/build-nunjucks.js @@ -16,7 +16,7 @@ */ /* eslint-disable no-restricted-syntax */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const path = require('path'); diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts index 43b8d30f99..3b0d26be21 100644 --- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts +++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts @@ -64,7 +64,7 @@ class ScaffolderActionsExtensionPointImpl */ export const scaffolderPlugin = createBackendPlugin( (options: ScaffolderPluginOptions) => ({ - id: 'scaffolder', + pluginId: 'scaffolder', register(env) { const actionsExtensions = new ScaffolderActionsExtensionPointImpl(); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts index f15747fd6b..a9ef8c3daa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubRepoCreate.test.ts @@ -42,6 +42,7 @@ const mockOctokit = { }, teams: { addOrUpdateRepoPermissionsInOrg: jest.fn(), + getByName: jest.fn(), }, }, }; @@ -96,6 +97,13 @@ describe('github:repo:create', () => { data: { type: 'Organization' }, }); + mockOctokit.rest.teams.getByName.mockResolvedValue({ + data: { + name: 'blam', + id: 42, + }, + }); + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); await action.handler(mockContext); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index dae5e16374..75c8f25d8b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -15,7 +15,7 @@ */ import { Config } from '@backstage/config'; -import { assertError, InputError } from '@backstage/errors'; +import { assertError, InputError, NotFoundError } from '@backstage/errors'; import { DefaultGithubCredentialsProvider, GithubCredentialsProvider, @@ -135,6 +135,10 @@ export async function createGithubRepoWithCollaboratorsAndTopics( username: owner, }); + if (access?.startsWith(`${owner}/`)) { + await validateAccessTeam(client, access); + } + const repoCreationPromise = user.data.type === 'Organization' ? client.rest.repos.createInOrg({ @@ -351,3 +355,22 @@ function extractCollaboratorName( if ('user' in collaborator) return collaborator.user; return collaborator.team; } + +async function validateAccessTeam(client: Octokit, access: string) { + const [org, team_slug] = access.split('/'); + try { + // Below rule disabled because of a 'getByName' check for a different library + // incorrectly triggers here. + // eslint-disable-next-line testing-library/no-await-sync-query + await client.rest.teams.getByName({ + org, + team_slug, + }); + } catch (e) { + if (e.response.data.message === 'Not Found') { + const message = `Received 'Not Found' from the API; one of org: + ${org} or team: ${team_slug} was not found within GitHub.`; + throw new NotFoundError(message); + } + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 8ead489ea4..94b51f3e94 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -45,6 +45,7 @@ const mockOctokit = { replaceAllTopics: jest.fn(), }, teams: { + getByName: jest.fn(), addOrUpdateRepoPermissionsInOrg: jest.fn(), }, }, @@ -96,11 +97,41 @@ describe('publish:github', () => { }); }); + it('should fail to create if the team is not found in the org', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'Organization' }, + }); + + mockOctokit.rest.teams.getByName.mockRejectedValue({ + response: { + status: 404, + data: { + message: 'Not Found', + documentation_url: + 'https://docs.github.com/en/rest/teams/teams#add-or-update-team-repository-permissions', + }, + }, + }); + + await expect(action.handler(mockContext)).rejects.toThrow( + "Received 'Not Found' from the API;", + ); + + expect(mockOctokit.rest.repos.createInOrg).not.toHaveBeenCalled(); + }); + it('should call the githubApis with the correct values for createInOrg', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'Organization' }, }); + mockOctokit.rest.teams.getByName.mockResolvedValue({ + data: { + name: 'blam', + id: 42, + }, + }); + mockOctokit.rest.repos.createInOrg.mockResolvedValue({ data: {} }); await action.handler(mockContext); @@ -518,6 +549,30 @@ describe('publish:github', () => { }); }); + it('should provide an adequate failure message when adding access', async () => { + mockOctokit.rest.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockOctokit.rest.teams.getByName.mockRejectedValue({ + response: { + status: 404, + data: { + message: 'Not Found', + documentation_url: + 'https://docs.github.com/en/rest/teams/teams#add-or-update-team-repository-permissions', + }, + }, + }); + await expect(action.handler(mockContext)).rejects.toThrow( + "Received 'Not Found' from the API;", + ); + + expect( + mockOctokit.rest.repos.createForAuthenticatedUser, + ).not.toHaveBeenCalled(); + }); + it('should add outside collaborators when provided', async () => { mockOctokit.rest.users.getByUsername.mockResolvedValue({ data: { type: 'User' }, diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md index 4b7444994a..e1936292f9 100644 --- a/plugins/scaffolder-common/CHANGELOG.md +++ b/plugins/scaffolder-common/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-scaffolder-common +## 1.2.5-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/types@1.0.2 + ## 1.2.5-next.0 ### Patch Changes diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json index bf8c32d5d1..3ec3528427 100644 --- a/plugins/scaffolder-common/package.json +++ b/plugins/scaffolder-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-common", "description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin", - "version": "1.2.5-next.0", + "version": "1.2.5-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md index 19e00ac8b4..cd2ef0259e 100644 --- a/plugins/scaffolder-node/CHANGELOG.md +++ b/plugins/scaffolder-node/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-scaffolder-node +## 0.1.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/types@1.0.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + ## 0.1.0-next.1 ### Patch Changes diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json index dc93306107..ce82f57f5f 100644 --- a/plugins/scaffolder-node/package.json +++ b/plugins/scaffolder-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-node", "description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend", - "version": "0.1.0-next.1", + "version": "0.1.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md index 384680c672..51fa1abc63 100644 --- a/plugins/scaffolder-react/CHANGELOG.md +++ b/plugins/scaffolder-react/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-scaffolder-react +## 1.1.0-next.2 + +### Minor Changes + +- 5555e17313: refactor `createAsyncValidators` to be recursive to ensure validators are called in nested schemas. + +### Patch Changes + +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + ## 1.1.0-next.1 ### Patch Changes diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md index deead4366c..33246d8bea 100644 --- a/plugins/scaffolder-react/api-report.md +++ b/plugins/scaffolder-react/api-report.md @@ -7,13 +7,14 @@ import { ApiHolder } from '@backstage/core-plugin-api'; import { ApiRef } from '@backstage/core-plugin-api'; +import { ComponentType } from 'react'; import { Dispatch } from 'react'; import { Extension } from '@backstage/core-plugin-api'; import { FieldProps } from '@rjsf/core'; import { FieldProps as FieldProps_2 } from '@rjsf/utils'; import { FieldValidation } from '@rjsf/core'; import { FieldValidation as FieldValidation_2 } from '@rjsf/utils'; -import type { FormProps as FormProps_2 } from '@rjsf/core-v5'; +import { FormProps as FormProps_2 } from '@rjsf/core-v5'; import { IconComponent } from '@backstage/core-plugin-api'; import { JsonObject } from '@backstage/types'; import { JSONSchema7 } from 'json-schema'; @@ -22,6 +23,7 @@ import { Observable } from '@backstage/types'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; +import { RJSFSchema } from '@rjsf/utils'; import { SetStateAction } from 'react'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TaskStep } from '@backstage/plugin-scaffolder-common'; @@ -85,6 +87,11 @@ export type CustomFieldValidator = ( }, ) => void | Promise; +// @alpha +export const DefaultTemplateOutputs: (props: { + output?: ScaffolderTaskOutput; +}) => JSX.Element | null; + // @alpha (undocumented) export const EmbeddableWorkflow: (props: WorkflowProps) => JSX.Element; @@ -121,6 +128,9 @@ export type FieldExtensionOptions< schema?: CustomFieldExtensionSchema; }; +// @alpha (undocumented) +export const Form: ComponentType>; + // @public export type FormProps = Pick< FormProps_2, diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json index e7acbdce05..e26bd973d5 100644 --- a/plugins/scaffolder-react/package.json +++ b/plugins/scaffolder-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder-react", "description": "A frontend library that helps other Backstage plugins interact with the Scaffolder", - "version": "1.1.0-next.1", + "version": "1.1.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -47,11 +47,11 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.57", "@rjsf/core": "^3.2.1", - "@rjsf/core-v5": "npm:@rjsf/core@5.0.1", + "@rjsf/core-v5": "npm:@rjsf/core@5.1.0", "@rjsf/material-ui": "^3.2.1", - "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.0.1", - "@rjsf/utils": "5.0.1", - "@rjsf/validator-ajv8": "5.0.1", + "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.1.0", + "@rjsf/utils": "5.1.0", + "@rjsf/validator-ajv8": "5.1.0", "@types/json-schema": "^7.0.9", "classnames": "^2.2.6", "json-schema": "^0.4.0", diff --git a/plugins/scaffolder-react/src/next/components/Form/Form.tsx b/plugins/scaffolder-react/src/next/components/Form/Form.tsx new file mode 100644 index 0000000000..79bcaa8475 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Form/Form.tsx @@ -0,0 +1,23 @@ +/* + * Copyright 2022 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 { withTheme } from '@rjsf/core-v5'; + +// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly +// which is what we're using here as the default types, it needs to depend on @rjsf/core-v5 because +// of the re-writing we're doing. Once we've migrated, we can import this the exact same as before. +/** @alpha */ +export const Form = withTheme(require('@rjsf/material-ui-v5').Theme); diff --git a/plugins/scaffolder-react/src/next/components/Form/index.ts b/plugins/scaffolder-react/src/next/components/Form/index.ts new file mode 100644 index 0000000000..6f780e92a4 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Form/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { Form } from './Form'; diff --git a/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/DescriptionField.tsx b/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/DescriptionField.tsx new file mode 100644 index 0000000000..eded38beee --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/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/utils'; + +export const DescriptionField = ({ description }: FieldProps) => + description && ; diff --git a/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/index.ts b/plugins/scaffolder-react/src/next/components/Stepper/FieldOverrides/index.ts new file mode 100644 index 0000000000..d6d0bf3499 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/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-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index c97d31c487..bb2844fd52 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -22,12 +22,15 @@ import { Button, makeStyles, } from '@material-ui/core'; -import { type IChangeEvent, withTheme } from '@rjsf/core-v5'; -import { ErrorSchema, FieldValidation } from '@rjsf/utils'; +import { type IChangeEvent } from '@rjsf/core-v5'; +import { ErrorSchema } from '@rjsf/utils'; import React, { useCallback, useMemo, useState, type ReactNode } from 'react'; import { NextFieldExtensionOptions } from '../../extensions'; import { TemplateParameterSchema } from '../../../types'; -import { createAsyncValidators } from './createAsyncValidators'; +import { + createAsyncValidators, + type FormValidation, +} from './createAsyncValidators'; import { ReviewState, type ReviewStateProps } from '../ReviewState'; import { useTemplateSchema } from '../../hooks/useTemplateSchema'; import validator from '@rjsf/validator-ajv8'; @@ -35,6 +38,9 @@ import { useFormDataFromQuery } from '../../hooks'; import { FormProps } from '../../types'; import { LayoutOptions } from '../../../layouts'; import { useTransformSchemaToProps } from '../../hooks/useTransformSchemaToProps'; +import { hasErrors } from './utils'; +import * as FieldOverrides from './FieldOverrides'; +import { Form } from '../Form'; const useStyles = makeStyles(theme => ({ backButton: { @@ -70,11 +76,6 @@ export type StepperProps = { layouts?: LayoutOptions[]; }; -// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly -// which is what we're using here as the default types, it needs to depend on @rjsf/core-v5 because -// of the re-writing we're doing. Once we've migrated, we can import this the exact same as before. -const Form = withTheme(require('@rjsf/material-ui-v5').Theme); - /** * The `Stepper` component is the Wizard that is rendered when a user selects a template * @alpha @@ -92,9 +93,7 @@ export const Stepper = (stepperProps: StepperProps) => { const [activeStep, setActiveStep] = useState(0); const [formState, setFormState] = useFormDataFromQuery(props.initialState); - const [errors, setErrors] = useState< - undefined | Record - >(); + const [errors, setErrors] = useState(); const styles = useStyles(); const extensions = useMemo(() => { @@ -125,6 +124,8 @@ export const Stepper = (stepperProps: StepperProps) => { [setFormState], ); + const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); + const handleNext = async ({ formData = {}, }: { @@ -136,11 +137,7 @@ export const Stepper = (stepperProps: StepperProps) => { const returnedValidation = await validation(formData); - const hasErrors = Object.values(returnedValidation).some( - i => i.__errors?.length, - ); - - if (hasErrors) { + if (hasErrors(returnedValidation)) { setErrors(returnedValidation); } else { setErrors(undefined); @@ -153,8 +150,6 @@ export const Stepper = (stepperProps: StepperProps) => { setFormState(current => ({ ...current, ...formData })); }; - const currentStep = useTransformSchemaToProps(steps[activeStep], { layouts }); - return ( <> @@ -177,7 +172,7 @@ export const Stepper = (stepperProps: StepperProps) => { schema={currentStep.schema} uiSchema={currentStep.uiSchema} onSubmit={handleNext} - fields={extensions} + fields={{ ...FieldOverrides, ...extensions }} showErrorList={false} onChange={handleChange} {...(props.FormProps ?? {})} diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts index 6ad14e3cc4..3c4442afea 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.test.ts @@ -154,4 +154,177 @@ describe('createAsyncValidators', () => { }), }); }); + + it('should run validation on complex nested schemas', async () => { + const schema: JsonObject = { + title: 'Select component', + properties: { + actionType: { + title: 'action type', + type: 'string', + description: 'Select the action type', + enum: ['newThing', 'existingThing'], + enumNames: ['New thing', 'Existing thing'], + default: 'newThing', + }, + }, + required: ['actionType'], + dependencies: { + actionType: { + oneOf: [ + { + properties: { + actionType: { + enum: ['newThing'], + }, + general: { + title: 'General', + type: 'object', + properties: { + address: { + type: 'object', + 'ui:field': 'AddressField', + properties: { + street: { + type: 'string', + }, + postcode: { + type: 'string', + }, + }, + }, + name: { + title: 'Name', + type: 'string', + 'ui:field': 'NameField', + }, + }, + }, + }, + }, + { + properties: { + actionType: { + enum: ['existingThing'], + }, + thingId: { + title: 'Thing id', + type: 'string', + description: 'Enter thing id', + }, + }, + }, + ], + }, + }, + }; + + const AddressField: NextCustomFieldValidator<{ + street?: string; + postcode?: string; + }> = (value, { addError }) => { + if (!value.postcode) { + addError('postcode is missing!'); + } + + if (!value.street) { + addError('street is missing here!'); + } + }; + + const NameField: NextCustomFieldValidator = ( + value, + { addError }, + ) => { + if (!value) { + addError('something is broken here!'); + } + }; + + const validators = { + AddressField: AddressField as NextCustomFieldValidator, + NameField: NameField as NextCustomFieldValidator, + }; + + const validate = createAsyncValidators(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await expect( + validate({ + actionType: 'newThing', + general: { + address: { + street: 'street', + postcode: 'postcode', + }, + name: undefined, + }, + }), + ).resolves.toEqual({ + general: { + address: expect.objectContaining({ + __errors: [], + }), + name: expect.objectContaining({ + __errors: ['something is broken here!'], + }), + }, + }); + }); + + it('should call a validator for array property from a custom field extension', async () => { + const schema: JsonObject = { + type: 'object', + properties: { + tags: { + title: 'Tags', + type: 'array', + items: { + type: 'string', + 'ui:field': 'TagField', + }, + }, + }, + }; + + const validators = { TagField: jest.fn() }; + + const validate = createAsyncValidators(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await validate({ + tags: ['tag-1', 'tag-2'], + }); + + expect(validators.TagField).toHaveBeenCalled(); + }); + + it('should does not call a validator if no ui field specified', async () => { + const schema: JsonObject = { + type: 'object', + properties: { + tags: { + title: 'Tags', + type: 'array', + items: { + type: 'string', + }, + }, + }, + }; + + const validators = { TagField: jest.fn() }; + + const validate = createAsyncValidators(schema, validators, { + apiHolder: { get: jest.fn() }, + }); + + await validate({ + tags: ['asd', 'asd$'], + }); + + expect(validators.TagField).not.toHaveBeenCalled(); + }); }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts index 79466ee602..b524944719 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts +++ b/plugins/scaffolder-react/src/next/components/Stepper/createAsyncValidators.ts @@ -15,11 +15,19 @@ */ import { FieldValidation } from '@rjsf/utils'; -import { JsonObject } from '@backstage/types'; +import type { JsonObject, JsonValue } from '@backstage/types'; import { ApiHolder } from '@backstage/core-plugin-api'; import { Draft07 as JSONSchema } from 'json-schema-library'; import { createFieldValidation } from '../../lib'; import { NextCustomFieldValidator } from '../../extensions'; +import { isObject } from './utils'; + +/** + * @internal + */ +export type FormValidation = { + [name: string]: FieldValidation | FormValidation; +}; export const createAsyncValidators = ( rootSchema: JsonObject, @@ -28,26 +36,49 @@ export const createAsyncValidators = ( apiHolder: ApiHolder; }, ) => { - async function validate(formData: JsonObject, pathPrefix: string = '#') { + async function validate( + formData: JsonObject, + pathPrefix: string = '#', + current: JsonObject = formData, + ): Promise { const parsedSchema = new JSONSchema(rootSchema); - const formValidation: Record = {}; - for (const [key, value] of Object.entries(formData)) { - const definitionInSchema = parsedSchema.getSchema( - `${pathPrefix}/${key}`, - formData, - ); + const formValidation: FormValidation = {}; + + const validateForm = async ( + validatorName: string, + key: string, + value: JsonValue | undefined, + ) => { + const validator = validators[validatorName]; + if (validator) { + const fieldValidation = createFieldValidation(); + try { + await validator(value, fieldValidation, { ...context, formData }); + } catch (ex) { + fieldValidation.addError(ex.message); + } + formValidation[key] = fieldValidation; + } + }; + + for (const [key, value] of Object.entries(current)) { + const path = `${pathPrefix}/${key}`; + const definitionInSchema = parsedSchema.getSchema(path, formData); if (definitionInSchema && 'ui:field' in definitionInSchema) { - const validator = validators[definitionInSchema['ui:field']]; - if (validator) { - const fieldValidation = createFieldValidation(); - try { - await validator(value, fieldValidation, { ...context, formData }); - } catch (ex) { - fieldValidation.addError(ex.message); - } - formValidation[key] = fieldValidation; + if ('ui:field' in definitionInSchema) { + await validateForm(definitionInSchema['ui:field'], key, value); } + } else if ( + definitionInSchema && + definitionInSchema.items && + 'ui:field' in definitionInSchema.items + ) { + if ('ui:field' in definitionInSchema.items) { + await validateForm(definitionInSchema.items['ui:field'], key, value); + } + } else if (isObject(value)) { + formValidation[key] = await validate(formData, path, value); } } diff --git a/plugins/scaffolder-react/src/next/components/Stepper/utils.test.ts b/plugins/scaffolder-react/src/next/components/Stepper/utils.test.ts new file mode 100644 index 0000000000..f6a1b012ac --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/utils.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { hasErrors } from './utils'; + +describe('hasErrors', () => { + it('should return false for empty _errors', () => { + expect( + hasErrors({ + name: { + __errors: [], + addError: jest.fn(), + }, + }), + ).toBe(false); + }); + + it('should return true for a single error', () => { + expect( + hasErrors({ + name: { + __errors: ['an error'], + addError: jest.fn(), + }, + }), + ).toBe(true); + }); + + it('should return true for more than one error', () => { + expect( + hasErrors({ + name: { + __errors: [], + addError: jest.fn(), + }, + general: { + address: { + __errors: [], + addError: jest.fn(), + }, + name: { + __errors: ['something is broken here!'], + addError: jest.fn(), + }, + }, + }), + ).toBe(true); + }); +}); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/utils.ts b/plugins/scaffolder-react/src/next/components/Stepper/utils.ts new file mode 100644 index 0000000000..7d3efa9d94 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/Stepper/utils.ts @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import type { JsonObject, JsonValue } from '@backstage/types'; +import type { FieldValidation } from '@rjsf/utils'; +import { FormValidation } from './createAsyncValidators'; + +function isFieldValidation(error: any): error is FieldValidation { + return !!error && '__errors' in error; +} + +export function hasErrors(errors?: FormValidation): boolean { + if (!errors) { + return false; + } + + for (const error of Object.values(errors)) { + if (isFieldValidation(error)) { + if ((error.__errors ?? []).length > 0) { + return true; + } + + continue; + } + + return hasErrors(error); + } + + return false; +} + +export function isObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx new file mode 100644 index 0000000000..5fbd3a5142 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/DefaultTemplateOutputs.tsx @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { Box, Paper } from '@material-ui/core'; +import { LinkOutputs } from './LinkOutputs'; +import { ScaffolderTaskOutput } from '../../../api'; + +/** + * The DefaultOutputs renderer for the scaffolder task output + * + * @alpha + */ +export const DefaultTemplateOutputs = (props: { + output?: ScaffolderTaskOutput; +}) => { + if (!props.output?.links) { + return null; + } + + return ( + + + + + + + + ); +}; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx b/plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx new file mode 100644 index 0000000000..d172aa9cb3 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/LinkOutputs.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { IconComponent, useApp, useRouteRef } from '@backstage/core-plugin-api'; +import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { Button, makeStyles } from '@material-ui/core'; +import React from 'react'; +import WebIcon from '@material-ui/icons/Web'; +import { parseEntityRef } from '@backstage/catalog-model'; +import { Link } from '@backstage/core-components'; +import { ScaffolderTaskOutput } from '../../../api'; + +const useStyles = makeStyles({ + root: { + '&:hover': { + textDecoration: 'none', + }, + }, +}); + +export const LinkOutputs = (props: { output: ScaffolderTaskOutput }) => { + const { links = [] } = props.output; + const classes = useStyles(); + const app = useApp(); + const entityRoute = useRouteRef(entityRouteRef); + + const iconResolver = (key?: string): IconComponent => + app.getSystemIcon(key!) ?? WebIcon; + + return ( + <> + {links + .filter(({ url, entityRef }) => url || entityRef) + .map(({ url, entityRef, title, icon }) => { + if (entityRef) { + const entityName = parseEntityRef(entityRef); + const target = entityRoute(entityName); + return { title, icon, url: target }; + } + return { title, icon, url: url! }; + }) + .map(({ url, title, icon }, i) => { + const Icon = iconResolver(icon); + return ( + + + + ); + })} + + ); +}; diff --git a/plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts b/plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts new file mode 100644 index 0000000000..1c998eb1b9 --- /dev/null +++ b/plugins/scaffolder-react/src/next/components/TemplateOutputs/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { DefaultTemplateOutputs } from './DefaultTemplateOutputs'; diff --git a/plugins/scaffolder-react/src/next/components/index.ts b/plugins/scaffolder-react/src/next/components/index.ts index babe3b4dcb..60a8fd9a51 100644 --- a/plugins/scaffolder-react/src/next/components/index.ts +++ b/plugins/scaffolder-react/src/next/components/index.ts @@ -18,3 +18,5 @@ export * from './TemplateCard'; export * from './ReviewState'; export * from './TemplateGroup'; export * from './Workflow'; +export * from './TemplateOutputs'; +export * from './Form'; diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 9fc78b0416..37da3b63e1 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-scaffolder +## 1.11.0-next.2 + +### Patch Changes + +- b46f385eff: scaffolder/next: Implementing a simple `OngoingTask` page +- ccbf91051b: bump `@rjsf` `v5` dependencies to 5.1.0 +- Updated dependencies + - @backstage/plugin-scaffolder-react@1.1.0-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-permission-react@0.4.9 + - @backstage/plugin-scaffolder-common@1.2.5-next.1 + ## 1.11.0-next.1 ### Patch Changes diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 1fb72b0fac..43b885b81a 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -197,9 +197,17 @@ export type NextRouterProps = { template: TemplateEntityV1beta3; }>; TaskPageComponent?: React_2.ComponentType<{}>; + TemplateOutputsComponent?: React_2.ComponentType<{ + output?: ScaffolderTaskOutput_2; + }>; }; groups?: TemplateGroupFilter[]; FormProps?: FormProps_2; + contextMenu?: { + editor?: boolean; + actions?: boolean; + tasks?: boolean; + }; }; // @alpha diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 1b0beb09d9..08381732c2 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-scaffolder", "description": "The Backstage plugin that helps you create new things", - "version": "1.11.0-next.1", + "version": "1.11.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -57,7 +57,8 @@ "@react-hookz/web": "^20.0.0", "@rjsf/core": "^3.2.1", "@rjsf/material-ui": "^3.2.1", - "@rjsf/utils": "5.0.1", + "@rjsf/utils": "5.1.0", + "@rjsf/validator-ajv8": "5.1.0", "@uiw/react-codemirror": "^4.9.3", "classnames": "^2.2.6", "git-url-parse": "^13.0.0", diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts index 177697eda0..f5be77811a 100644 --- a/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.test.ts @@ -17,14 +17,32 @@ import { createValidator } from './createValidator'; import { CustomFieldValidator } from '@backstage/plugin-scaffolder-react'; import { ApiHolder } from '@backstage/core-plugin-api'; -import { FormValidation } from '@rjsf/core'; +import { FieldValidation, FormValidation } from '@rjsf/core'; describe('createValidator', () => { const validators: Record> = { - CustomPicker: (value, validation, _context) => { + CustomPicker: ( + value: unknown, + fieldValidation: FieldValidation, + _context: { apiHolder: ApiHolder }, + ) => { if (!value || !(value as { value?: unknown }).value) { - validation.addError('Error !'); + fieldValidation.addError('Error !'); + } + }, + TagPicker: ( + values: unknown, + fieldValidation: FieldValidation, + _context: { apiHolder: ApiHolder }, + ) => { + const input = values as string[]; + for (const item of input) { + if (!/^[a-z0-9-]+$/.test(item)) { + fieldValidation.addError( + 'A tag name can only contain lowercase letters, numeric characters or dashes', + ); + } } }, }; @@ -39,7 +57,7 @@ describe('createValidator', () => { apiHolder: apiHolderMock, }; - it('Should call validator for object property from a custom field extension', () => { + it('should call validator for object property from a custom field extension', () => { /* GIVEN */ const rootSchema = { title: 'Title', @@ -70,4 +88,39 @@ describe('createValidator', () => { expect(result).not.toBeNull(); expect(result.p1.addError).toHaveBeenCalledTimes(1); }); + + it('should call validator for array property from a custom field extension', () => { + /* GIVEN */ + const rootSchema = { + title: 'My form', + properties: { + tags: { + title: 'Tags', + type: 'array', + items: { + type: 'string', + 'ui:field': 'TagPicker', + }, + }, + }, + }; + const validator = createValidator(rootSchema, validators, context); + + const formData = { + tags: ['invalid-tag$$'], + }; + const errors = { + addError: jest.fn(), + tags: { + addError: jest.fn(), + } as unknown as FormValidation, + } as unknown as FormValidation; + + /* WHEN */ + const result = validator(formData, errors); + + /* THEN */ + expect(result).not.toBeNull(); + expect(result.tags.addError).toHaveBeenCalledTimes(1); + }); }); diff --git a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts index 4a64481fc3..123a7e800e 100644 --- a/plugins/scaffolder/src/components/TemplatePage/createValidator.ts +++ b/plugins/scaffolder/src/components/TemplatePage/createValidator.ts @@ -23,6 +23,10 @@ function isObject(obj: unknown): obj is JsonObject { return typeof obj === 'object' && obj !== null && !Array.isArray(obj); } +function isArray(obj: unknown): obj is JsonObject { + return typeof obj === 'object' && obj !== null && Array.isArray(obj); +} + export const createValidator = ( rootSchema: JsonObject, validators: Record>, @@ -46,8 +50,8 @@ export const createValidator = ( for (const [key, propData] of Object.entries(formData)) { const propValidation = errors[key]; + const propSchemaProps = schemaProps[key]; if (isObject(propData)) { - const propSchemaProps = schemaProps[key]; if (isObject(propSchemaProps)) { validate( propSchemaProps, @@ -55,10 +59,24 @@ export const createValidator = ( propValidation as FormValidation, ); } + } else if (isArray(propData)) { + if (isObject(propSchemaProps)) { + const { items } = propSchemaProps; + if (isObject(items)) { + const fieldName = items['ui:field'] as string; + if (fieldName && typeof validators[fieldName] === 'function') { + validators[fieldName]!( + propData as JsonObject[], + propValidation, + context, + ); + } + } + } } else { - const propSchema = schemaProps[key]; const fieldName = - isObject(propSchema) && (propSchema['ui:field'] as string); + isObject(propSchemaProps) && + (propSchemaProps['ui:field'] as string); if (fieldName && typeof validators[fieldName] === 'function') { validators[fieldName]!( propData as JsonValue, diff --git a/plugins/scaffolder/src/components/hooks/useEventStream.ts b/plugins/scaffolder/src/components/hooks/useEventStream.ts index 0b36c85ad8..be0ff922a4 100644 --- a/plugins/scaffolder/src/components/hooks/useEventStream.ts +++ b/plugins/scaffolder/src/components/hooks/useEventStream.ts @@ -25,7 +25,7 @@ import { import { useApi } from '@backstage/core-plugin-api'; import { Subscription } from '@backstage/types'; -type Step = { +export type Step = { id: string; status: ScaffolderTaskStatus; endedAt?: string; diff --git a/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx new file mode 100644 index 0000000000..d4c771a9f8 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx @@ -0,0 +1,87 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { BackstageTheme } from '@backstage/theme'; +import { + IconButton, + ListItemIcon, + ListItemText, + makeStyles, + MenuItem, + MenuList, + Popover, +} from '@material-ui/core'; +import Retry from '@material-ui/icons/Repeat'; +import Toc from '@material-ui/icons/Toc'; +import MoreVert from '@material-ui/icons/MoreVert'; +import React, { useState } from 'react'; + +type ContextMenuProps = { + logsVisible?: boolean; + onToggleLogs?: (state: boolean) => void; + onStartOver?: () => void; +}; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + button: { + color: theme.palette.common.white, + }, +})); + +export const ContextMenu = (props: ContextMenuProps) => { + const { logsVisible, onToggleLogs, onStartOver } = props; + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(); + + return ( + <> + ) => { + setAnchorEl(event.currentTarget); + }} + data-testid="menu-button" + color="inherit" + className={classes.button} + > + + + setAnchorEl(undefined)} + anchorEl={anchorEl} + anchorOrigin={{ vertical: 'bottom', horizontal: 'right' }} + transformOrigin={{ vertical: 'top', horizontal: 'right' }} + > + + onToggleLogs?.(!logsVisible)}> + + + + + + + + + + + + + + + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx new file mode 100644 index 0000000000..a68df01ed5 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx @@ -0,0 +1,160 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useEffect, useMemo, useState, useCallback } from 'react'; +import { Page, Header, Content, ErrorPanel } from '@backstage/core-components'; +import { useTaskEventStream } from '../../components/hooks/useEventStream'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Box, makeStyles, Paper } from '@material-ui/core'; +import { TaskSteps } from './TaskSteps'; +import { TaskBorder } from './TaskBorder'; +import { TaskLogStream } from './TaskLogStream'; +import { nextSelectedTemplateRouteRef } from '../routes'; +import { useRouteRef } from '@backstage/core-plugin-api'; +import qs from 'qs'; +import { ContextMenu } from './ContextMenu'; +import { + DefaultTemplateOutputs, + ScaffolderTaskOutput, +} from '@backstage/plugin-scaffolder-react'; + +const useStyles = makeStyles({ + contentWrapper: { + display: 'flex', + flexDirection: 'column', + }, +}); + +export const OngoingTask = (props: { + TemplateOutputsComponent?: React.ComponentType<{ + output?: ScaffolderTaskOutput; + }>; +}) => { + // todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful. + const { taskId } = useParams(); + const templateRouteRef = useRouteRef(nextSelectedTemplateRouteRef); + const navigate = useNavigate(); + const taskStream = useTaskEventStream(taskId!); + const classes = useStyles(); + const steps = useMemo( + () => + taskStream.task?.spec.steps.map(step => ({ + ...step, + ...taskStream?.steps?.[step.id], + })) ?? [], + [taskStream], + ); + + const [logsVisible, setLogVisibleState] = useState(false); + + useEffect(() => { + if (taskStream.error) { + setLogVisibleState(true); + } + }, [taskStream.error]); + + const activeStep = useMemo(() => { + for (let i = steps.length - 1; i >= 0; i--) { + if (steps[i].status !== 'open') { + return i; + } + } + + return 0; + }, [steps]); + + const startOver = useCallback(() => { + const { namespace, name } = + taskStream.task?.spec.templateInfo?.entity?.metadata ?? {}; + + const formData = taskStream.task?.spec.parameters ?? {}; + + if (!namespace || !name) { + return; + } + + navigate({ + pathname: templateRouteRef({ + namespace, + templateName: name, + }), + search: `?${qs.stringify({ formData: JSON.stringify(formData) })}`, + }); + }, [ + navigate, + taskStream.task?.spec.parameters, + taskStream.task?.spec.templateInfo?.entity?.metadata, + templateRouteRef, + ]); + + const Outputs = props.TemplateOutputsComponent ?? DefaultTemplateOutputs; + + const templateName = + taskStream.task?.spec.templateInfo?.entity?.metadata.name; + + return ( + +
    + Run of {templateName} +
  • + } + subtitle={`Task ${taskId}`} + > + +
    + + {taskStream.error ? ( + + + + ) : null} + + + + + + + + + + + + + {logsVisible ? ( + + + + + + + + ) : null} + + + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskBorder.test.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.test.tsx new file mode 100644 index 0000000000..f3a59ad344 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.test.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TaskBorder } from './TaskBorder'; +import { render } from '@testing-library/react'; + +describe('TaskBorder', () => { + it('should render a pending linear progress if the task is not complete', () => { + const { getByRole } = render( + , + ); + + const progressBar = getByRole('progressbar'); + + expect(progressBar).toBeInTheDocument(); + expect(progressBar).toHaveClass('MuiLinearProgress-indeterminate'); + }); + + it('should render a determinate progress bar if the task is complete', () => { + const { getByRole } = render(); + + const progressBar = getByRole('progressbar'); + + expect(progressBar).toBeInTheDocument(); + expect(progressBar).toHaveClass('MuiLinearProgress-determinate'); + }); +}); diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx new file mode 100644 index 0000000000..fdfab17367 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskBorder.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { LinearProgress, makeStyles } from '@material-ui/core'; +import { BackstageTheme } from '@backstage/theme'; + +const useStyles = makeStyles((theme: BackstageTheme) => ({ + failed: { + backgroundColor: theme.palette.error.main, + }, + success: { + backgroundColor: theme.palette.success.main, + }, +})); + +export const TaskBorder = (props: { + isComplete: boolean; + isError: boolean; +}) => { + const styles = useStyles(); + + if (!props.isComplete) { + return ; + } + + return ( + + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.test.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.test.tsx new file mode 100644 index 0000000000..e8f9e67e96 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderInTestApp } from '@backstage/test-utils'; +import React, { ReactNode } from 'react'; +import { TaskLogStream } from './TaskLogStream'; + +// The inside needs mocking to render in jsdom +jest.mock('react-virtualized-auto-sizer', () => ({ + __esModule: true, + default: (props: { + children: (size: { width: number; height: number }) => ReactNode; + }) => <>{props.children({ width: 400, height: 200 })}, +})); + +describe('TaskLogStream', () => { + it('should render a log stream with the correct log lines', async () => { + const logs = { step: ['line 1', 'line 2'], step2: ['line 3'] }; + + const { findByText } = await renderInTestApp(); + + const logLines = Object.values(logs).flat(); + + for (const line of logLines) { + await expect(findByText(line)).resolves.toBeInTheDocument(); + } + }); + + it('should filter out empty log lines', async () => { + const logs = { step: ['line 1', 'line 2'], step2: [] }; + + const { findAllByRole } = await renderInTestApp( + , + ); + + await expect(findAllByRole('row')).resolves.toHaveLength(2); + }); +}); diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx new file mode 100644 index 0000000000..00e8452b42 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskLogStream.tsx @@ -0,0 +1,40 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { LogViewer } from '@backstage/core-components'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles({ + root: { + width: '100%', + height: '100%', + position: 'relative', + }, +}); + +export const TaskLogStream = (props: { logs: { [k: string]: string[] } }) => { + const styles = useStyles(); + return ( +
    + l.join('\n')) + .filter(Boolean) + .join('\n')} + /> +
    + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx new file mode 100644 index 0000000000..cecf682596 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepIcon.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; + +import { BackstageTheme } from '@backstage/theme'; +import { CircularProgress, makeStyles, StepIconProps } from '@material-ui/core'; +import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline'; +import PanoramaFishEyeIcon from '@material-ui/icons/PanoramaFishEye'; +import classNames from 'classnames'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import ErrorOutline from '@material-ui/icons/ErrorOutline'; + +const useStepIconStyles = makeStyles((theme: BackstageTheme) => ({ + root: { + color: theme.palette.text.disabled, + }, + completed: { + color: theme.palette.status.ok, + }, + error: { + color: theme.palette.status.error, + }, +})); + +export const StepIcon = (props: StepIconProps & { skipped: boolean }) => { + const classes = useStepIconStyles(); + const { active, completed, error, skipped } = props; + + const getMiddle = () => { + if (active) { + return ; + } + if (completed) { + return ; + } + + if (error) { + return ; + } + + if (skipped) { + return ; + } + + return ; + }; + + return ( +
    + {getMiddle()} +
    + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.test.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.test.tsx new file mode 100644 index 0000000000..185d2fa7fb --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.test.tsx @@ -0,0 +1,79 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { StepTime } from './StepTime'; +import { render, act } from '@testing-library/react'; + +describe('StepTime', () => { + it('should return empty when there is no start time', () => { + const step = { + id: 'test', + startedAt: undefined, + endedAt: undefined, + name: 'test', + }; + + const { container } = render(); + + expect(container.querySelector('span')?.textContent).toBe(''); + }); + + it('should format the time between the start and end date properly', async () => { + const step = { + id: 'test', + startedAt: '2021-01-01T00:00:00Z', + endedAt: '2021-01-01T00:00:01Z', + name: 'test', + }; + + const { findByText } = render(); + + await expect(findByText('1 second')).resolves.toBeInTheDocument(); + }); + + describe('updates', () => { + beforeEach(() => { + jest.useFakeTimers({ now: new Date('2021-01-01T00:00:00Z') }); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should update the time every second', async () => { + const step = { + id: 'test', + startedAt: '2021-01-01T00:00:00Z', + endedAt: undefined, + name: 'test', + }; + + const { findByText } = render(); + + await expect(findByText('0 seconds')).resolves.toBeInTheDocument(); + + act(() => jest.advanceTimersByTime(1000)); + + await expect(findByText('1 second')).resolves.toBeInTheDocument(); + + act(() => jest.advanceTimersByTime(1000 * 60)); + + await expect( + findByText('1 minute, 1 second'), + ).resolves.toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx new file mode 100644 index 0000000000..653fe6e942 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/StepTime.tsx @@ -0,0 +1,57 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { useCallback, useState } from 'react'; +import useInterval from 'react-use/lib/useInterval'; +import { DateTime, Interval } from 'luxon'; +import humanizeDuration from 'humanize-duration'; +import { Typography } from '@material-ui/core'; +import { useMountEffect } from '@react-hookz/web'; + +export const StepTime = (props: { + step: { + id: string; + name: string; + startedAt?: string; + endedAt?: string; + }; +}) => { + const [time, setTime] = useState(''); + const { step } = props; + + const calculate = useCallback(() => { + if (!step.startedAt) { + setTime(''); + return; + } + + const end = step.endedAt + ? DateTime.fromISO(step.endedAt) + : DateTime.local(); + + const startedAt = DateTime.fromISO(step.startedAt); + const formatted = Interval.fromDateTimes(startedAt, end) + .toDuration() + .valueOf(); + + setTime(humanizeDuration(formatted, { round: true })); + }, [step.endedAt, step.startedAt]); + + useMountEffect(() => calculate()); + + useInterval(() => !step.endedAt && calculate(), 1000); + + return {time}; +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.test.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.test.tsx new file mode 100644 index 0000000000..afd1f58190 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.test.tsx @@ -0,0 +1,64 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { TaskSteps } from './TaskSteps'; +import { ScaffolderTaskStatus } from '@backstage/plugin-scaffolder-react'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('TaskSteps', () => { + it('should render each of the steps', async () => { + const steps = [ + { + id: '1', + name: 'Step 1', + status: 'processing' as ScaffolderTaskStatus, + startedAt: Date.now().toLocaleString(), + action: 'create', + }, + { + id: '2', + name: 'Step 2', + status: 'failed' as ScaffolderTaskStatus, + + startedAt: Date.now().toLocaleString(), + endedAt: Date.now().toLocaleString(), + action: 'create', + }, + { + id: '3', + name: 'Step 3', + status: 'completed' as ScaffolderTaskStatus, + startedAt: Date.now().toLocaleString(), + endedAt: Date.now().toLocaleString(), + action: 'create', + }, + { + id: '4', + name: 'Step 4', + status: 'skipped' as ScaffolderTaskStatus, + startedAt: Date.now().toLocaleString(), + endedAt: Date.now().toLocaleString(), + action: 'create', + }, + ]; + + const { getByText } = await renderInTestApp(); + + for (const step of steps) { + expect(getByText(step.name)).toBeInTheDocument(); + } + }); +}); diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx new file mode 100644 index 0000000000..7cf5ed71bf --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/TaskSteps.tsx @@ -0,0 +1,70 @@ +/* + * Copyright 2022 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 { + Stepper as MuiStepper, + Step as MuiStep, + StepButton as MuiStepButton, + StepLabel as MuiStepLabel, + StepIconProps, + Box, +} from '@material-ui/core'; +import { TaskStep } from '@backstage/plugin-scaffolder-common'; +import { Step } from '../../../components/hooks/useEventStream'; +import { StepIcon } from './StepIcon'; +import { StepTime } from './StepTime'; + +interface StepperProps { + steps: (TaskStep & Step)[]; + activeStep?: number; +} + +export const TaskSteps = (props: StepperProps) => { + return ( + + {props.steps.map((step, index) => { + const isCompleted = step.status === 'completed'; + const isFailed = step.status === 'failed'; + const isActive = step.status === 'processing'; + const isSkipped = step.status === 'skipped'; + const stepIconProps: Partial = { + completed: isCompleted, + error: isFailed, + active: isActive, + skipped: isSkipped, + }; + + return ( + + + + {step.name} + + + + + ); + })} + + ); +}; diff --git a/plugins/scaffolder/src/next/OngoingTask/TaskSteps/index.ts b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/index.ts new file mode 100644 index 0000000000..28724ba512 --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/TaskSteps/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TaskSteps } from './TaskSteps'; diff --git a/plugins/scaffolder/src/next/OngoingTask/index.ts b/plugins/scaffolder/src/next/OngoingTask/index.ts new file mode 100644 index 0000000000..f0c7e2a6cc --- /dev/null +++ b/plugins/scaffolder/src/next/OngoingTask/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { OngoingTask } from './OngoingTask'; diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index 8dd47b478e..ded344b048 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -19,6 +19,7 @@ import { TemplateListPage } from '../TemplateListPage'; import { TemplateWizardPage } from '../TemplateWizardPage'; import { NextFieldExtensionOptions, + ScaffolderTaskOutput, SecretsContextProvider, useCustomFieldExtensions, useCustomLayouts, @@ -28,7 +29,19 @@ import { import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups'; import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default'; -import { nextSelectedTemplateRouteRef } from '../routes'; + +import { + nextActionsRouteRef, + nextEditRouteRef, + nextScaffolderListTaskRouteRef, + nextScaffolderTaskRouteRef, + nextSelectedTemplateRouteRef, +} from '../routes'; +import { ErrorPage } from '@backstage/core-components'; +import { OngoingTask } from '../OngoingTask'; +import { ActionsPage } from '../../components/ActionsPage'; +import { ListTasksPage } from '../../components/ListTasksPage'; +import { TemplateEditorPage } from '../TemplateEditorPage'; /** * The Props for the Scaffolder Router @@ -41,9 +54,21 @@ export type NextRouterProps = { template: TemplateEntityV1beta3; }>; TaskPageComponent?: React.ComponentType<{}>; + TemplateOutputsComponent?: React.ComponentType<{ + output?: ScaffolderTaskOutput; + }>; }; groups?: TemplateGroupFilter[]; + // todo(blam): rename this to formProps FormProps?: FormProps; + contextMenu?: { + /** Whether to show a link to the template editor */ + editor?: boolean; + /** Whether to show a link to the actions documentation */ + actions?: boolean; + /** Whether to show a link to the tasks page */ + tasks?: boolean; + }; }; /** @@ -52,10 +77,17 @@ export type NextRouterProps = { * @alpha */ export const Router = (props: PropsWithChildren) => { - const { components: { TemplateCardComponent } = {} } = props; + const { + components: { + TemplateCardComponent, + TemplateOutputsComponent, + TaskPageComponent = OngoingTask, + } = {}, + } = props; const outlet = useOutlet() || props.children; const customFieldExtensions = useCustomFieldExtensions(outlet); + const fieldExtensions = [ ...customFieldExtensions, ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( @@ -75,6 +107,7 @@ export const Router = (props: PropsWithChildren) => { element={ } @@ -91,6 +124,35 @@ export const Router = (props: PropsWithChildren) => { } /> + + } + /> + + + + } + /> + + } /> + } + /> + } + /> ); }; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/CustomFieldExplorer.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/CustomFieldExplorer.tsx new file mode 100644 index 0000000000..d0852a6a31 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/CustomFieldExplorer.tsx @@ -0,0 +1,196 @@ +/* + * Copyright 2022 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 { StreamLanguage } from '@codemirror/language'; +import { yaml as yamlSupport } from '@codemirror/legacy-modes/mode/yaml'; +import { + Button, + Card, + CardContent, + CardHeader, + FormControl, + IconButton, + InputLabel, + makeStyles, + MenuItem, + Select, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import CodeMirror from '@uiw/react-codemirror'; +import React, { useCallback, useMemo, useState } from 'react'; +import yaml from 'yaml'; +import { + NextFieldExtensionOptions, + Form, +} from '@backstage/plugin-scaffolder-react'; +import { TemplateEditorForm } from './TemplateEditorForm'; +import validator from '@rjsf/validator-ajv8'; + +const useStyles = makeStyles(theme => ({ + root: { + gridArea: 'pageContent', + display: 'grid', + gridTemplateAreas: ` + "controls controls" + "fieldForm preview" + `, + gridTemplateRows: 'auto 1fr', + gridTemplateColumns: '1fr 1fr', + }, + controls: { + gridArea: 'controls', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + margin: theme.spacing(1), + }, + fieldForm: { + gridArea: 'fieldForm', + }, + preview: { + gridArea: 'preview', + }, +})); + +export const CustomFieldExplorer = ({ + customFieldExtensions = [], + onClose, +}: { + customFieldExtensions?: NextFieldExtensionOptions[]; + onClose?: () => void; +}) => { + const classes = useStyles(); + const fieldOptions = customFieldExtensions.filter(field => !!field.schema); + const [selectedField, setSelectedField] = useState(fieldOptions[0]); + const [fieldFormState, setFieldFormState] = useState({}); + const [refreshKey, setRefreshKey] = useState(Date.now()); + const sampleFieldTemplate = useMemo( + () => + yaml.stringify({ + parameters: [ + { + title: `${selectedField.name} Example`, + properties: { + [selectedField.name]: { + type: selectedField.schema?.returnValue?.type, + 'ui:field': selectedField.name, + 'ui:options': fieldFormState, + }, + }, + }, + ], + }), + [fieldFormState, selectedField], + ); + + const fieldComponents = useMemo(() => { + return Object.fromEntries( + customFieldExtensions.map(({ name, component }) => [name, component]), + ); + }, [customFieldExtensions]); + + const handleSelectionChange = useCallback( + selection => { + setSelectedField(selection); + setFieldFormState({}); + }, + [setFieldFormState, setSelectedField], + ); + + const handleFieldConfigChange = useCallback( + state => { + setFieldFormState(state); + // Force TemplateEditorForm to re-render since some fields + // may not be responsive to ui:option changes + setRefreshKey(Date.now()); + }, + [setFieldFormState, setRefreshKey], + ); + + return ( +
    +
    + + + Choose Custom Field Extension + + + + + + + +
    +
    + + + +
    handleFieldConfigChange(e.formData)} + validator={validator} + schema={selectedField.schema?.uiOptions || {}} + > + +
    +
    +
    +
    +
    + + + + + + + null} + /> +
    +
    + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditor.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditor.tsx new file mode 100644 index 0000000000..4cdd383f83 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditor.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2022 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 { makeStyles } from '@material-ui/core'; +import React, { useState } from 'react'; +import type { + LayoutOptions, + NextFieldExtensionOptions, +} from '@backstage/plugin-scaffolder-react'; +import { TemplateDirectoryAccess } from '../../lib/filesystem'; +import { DirectoryEditorProvider } from '../../components/TemplateEditorPage/DirectoryEditorContext'; +import { DryRunProvider } from '../../components/TemplateEditorPage/DryRunContext'; +import { DryRunResults } from '../../components/TemplateEditorPage/DryRunResults'; +import { TemplateEditorBrowser } from '../../components/TemplateEditorPage/TemplateEditorBrowser'; +import { TemplateEditorForm } from './TemplateEditorForm'; +import { TemplateEditorTextArea } from '../../components/TemplateEditorPage/TemplateEditorTextArea'; + +const useStyles = makeStyles({ + // Reset and fix sizing to make sure scrolling behaves correctly + root: { + gridArea: 'pageContent', + + display: 'grid', + gridTemplateAreas: ` + "browser editor preview" + "results results results" + `, + gridTemplateColumns: '1fr 3fr 2fr', + gridTemplateRows: '1fr auto', + }, + browser: { + gridArea: 'browser', + overflow: 'auto', + }, + editor: { + gridArea: 'editor', + overflow: 'auto', + }, + preview: { + gridArea: 'preview', + overflow: 'auto', + }, + results: { + gridArea: 'results', + }, +}); + +export const TemplateEditor = (props: { + directory: TemplateDirectoryAccess; + fieldExtensions?: NextFieldExtensionOptions[]; + layouts?: LayoutOptions[]; + onClose?: () => void; +}) => { + const classes = useStyles(); + + const [errorText, setErrorText] = useState(); + + return ( + + +
    +
    + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorForm.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorForm.tsx new file mode 100644 index 0000000000..5dc841cf20 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorForm.tsx @@ -0,0 +1,233 @@ +/* + * Copyright 2022 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 { useApiHolder } from '@backstage/core-plugin-api'; +import { JsonObject, JsonValue } from '@backstage/types'; +import { makeStyles } from '@material-ui/core/styles'; +import React, { Component, ReactNode, useState } from 'react'; +import useDebounce from 'react-use/lib/useDebounce'; +import yaml from 'yaml'; +import { + LayoutOptions, + NextFieldExtensionOptions, + Stepper, + TemplateParameterSchema, +} from '@backstage/plugin-scaffolder-react'; +import { useDryRun } from '../../components/TemplateEditorPage/DryRunContext'; +import { useDirectoryEditor } from '../../components/TemplateEditorPage/DirectoryEditorContext'; + +const useStyles = makeStyles({ + containerWrapper: { + position: 'relative', + width: '100%', + height: '100%', + }, + container: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + right: 0, + overflow: 'auto', + }, +}); + +interface ErrorBoundaryProps { + invalidator: unknown; + setErrorText(errorText: string | undefined): void; + children: ReactNode; +} + +interface ErrorBoundaryState { + shouldRender: boolean; +} + +class ErrorBoundary extends Component { + state = { + shouldRender: true, + }; + + componentDidUpdate(prevProps: { invalidator: unknown }) { + if (prevProps.invalidator !== this.props.invalidator) { + this.setState({ shouldRender: true }); + } + } + + componentDidCatch(error: Error) { + this.props.setErrorText(error.message); + this.setState({ shouldRender: false }); + } + + render() { + return this.state.shouldRender ? this.props.children : null; + } +} + +interface TemplateEditorFormProps { + content?: string; + /** Setting this to true will cause the content to be parsed as if it is the template entity spec */ + contentIsSpec?: boolean; + setErrorText: (errorText?: string) => void; + onDryRun?: (data: JsonObject) => Promise; + fieldExtensions?: NextFieldExtensionOptions[]; + layouts?: LayoutOptions[]; +} + +function isJsonObject(value: JsonValue | undefined): value is JsonObject { + return typeof value === 'object' && value !== null && !Array.isArray(value); +} + +/** Shows the a template form that is parsed from the provided content */ +export function TemplateEditorForm(props: TemplateEditorFormProps) { + const { + content, + contentIsSpec, + onDryRun, + setErrorText, + fieldExtensions = [], + layouts = [], + } = props; + const classes = useStyles(); + const apiHolder = useApiHolder(); + + const [steps, setSteps] = useState(); + + useDebounce( + () => { + try { + if (!content) { + setSteps(undefined); + return; + } + const parsed: JsonValue = yaml.parse(content); + + if (!isJsonObject(parsed)) { + setSteps(undefined); + return; + } + + let rootObj = parsed; + if (!contentIsSpec) { + const isTemplate = + String(parsed.kind).toLocaleLowerCase('en-US') === 'template'; + if (!isTemplate) { + setSteps(undefined); + return; + } + + rootObj = isJsonObject(parsed.spec) ? parsed.spec : {}; + } + + const { parameters } = rootObj; + + if (!Array.isArray(parameters)) { + setErrorText('Template parameters must be an array'); + setSteps(undefined); + return; + } + + setErrorText(); + setSteps( + parameters.flatMap(param => + isJsonObject(param) + ? [ + { + title: String(param.title), + schema: param, + }, + ] + : [], + ), + ); + } catch (e) { + setErrorText(e.message); + } + }, + 250, + [contentIsSpec, content, apiHolder], + ); + + if (!steps) { + return null; + } + + return ( +
    +
    + + { + await onDryRun?.(data); + }} + layouts={layouts} + components={{ createButtonText: onDryRun && 'Try It' }} + /> + +
    +
    + ); +} + +/** A version of the TemplateEditorForm that is connected to the DirectoryEditor and DryRun contexts */ +export function TemplateEditorFormDirectoryEditorDryRun( + props: Pick< + TemplateEditorFormProps, + 'setErrorText' | 'fieldExtensions' | 'layouts' + >, +) { + const { setErrorText, fieldExtensions = [], layouts } = props; + const dryRun = useDryRun(); + + const directoryEditor = useDirectoryEditor(); + const { selectedFile } = directoryEditor; + + const handleDryRun = async (values: JsonObject) => { + if (!selectedFile) { + return; + } + + try { + await dryRun.execute({ + templateContent: selectedFile.content, + values, + files: directoryEditor.files, + }); + setErrorText(); + } catch (e) { + setErrorText(String(e.cause || e)); + throw e; + } + }; + + const content = + selectedFile && selectedFile.path.match(/\.ya?ml$/) + ? selectedFile.content + : undefined; + + return ( + + ); +} + +TemplateEditorForm.DirectoryEditorDryRun = + TemplateEditorFormDirectoryEditorDryRun; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx new file mode 100644 index 0000000000..92387fd69d --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateEditorPage.tsx @@ -0,0 +1,107 @@ +/* + * Copyright 2022 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 { Content, Header, Page } from '@backstage/core-components'; +import { + TemplateDirectoryAccess, + WebFileSystemAccess, +} from '../../lib/filesystem'; +import { CustomFieldExplorer } from './CustomFieldExplorer'; +import { TemplateEditor } from './TemplateEditor'; +import { TemplateFormPreviewer } from './TemplateFormPreviewer'; +import { + NextFieldExtensionOptions, + type LayoutOptions, +} from '@backstage/plugin-scaffolder-react'; +import { TemplateEditorIntro } from '../../components/TemplateEditorPage/TemplateEditorIntro'; + +type Selection = + | { + type: 'local'; + directory: TemplateDirectoryAccess; + } + | { + type: 'form'; + } + | { + type: 'field-explorer'; + }; + +interface TemplateEditorPageProps { + defaultPreviewTemplate?: string; + customFieldExtensions?: NextFieldExtensionOptions[]; + layouts?: LayoutOptions[]; +} + +export function TemplateEditorPage(props: TemplateEditorPageProps) { + const [selection, setSelection] = useState(); + + let content: JSX.Element | null = null; + if (selection?.type === 'local') { + content = ( + setSelection(undefined)} + layouts={props.layouts} + /> + ); + } else if (selection?.type === 'form') { + content = ( + setSelection(undefined)} + layouts={props.layouts} + /> + ); + } else if (selection?.type === 'field-explorer') { + content = ( + setSelection(undefined)} + /> + ); + } else { + content = ( + + { + if (option === 'local') { + WebFileSystemAccess.requestDirectoryAccess() + .then(directory => setSelection({ type: 'local', directory })) + .catch(() => {}); + } else if (option === 'form') { + setSelection({ type: 'form' }); + } else if (option === 'field-explorer') { + setSelection({ type: 'field-explorer' }); + } + }} + /> + + ); + } + + return ( + +
    + {content} + + ); +} diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx new file mode 100644 index 0000000000..1476cda2d0 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -0,0 +1,219 @@ +/* + * Copyright 2022 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 { alertApiRef, useApi } from '@backstage/core-plugin-api'; +import { + catalogApiRef, + humanizeEntityRef, +} from '@backstage/plugin-catalog-react'; +import { + FormControl, + IconButton, + InputLabel, + LinearProgress, + makeStyles, + MenuItem, + Select, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import React, { useCallback, useState } from 'react'; +import useAsync from 'react-use/lib/useAsync'; +import yaml from 'yaml'; +import { + NextFieldExtensionOptions, + type LayoutOptions, +} from '@backstage/plugin-scaffolder-react'; +import { TemplateEditorForm } from './TemplateEditorForm'; +import { TemplateEditorTextArea } from '../../components/TemplateEditorPage/TemplateEditorTextArea'; + +const EXAMPLE_TEMPLATE_PARAMS_YAML = `# Edit the template parameters below to see how they will render in the scaffolder form UI +parameters: + - title: Fill in some steps + required: + - name + properties: + name: + title: Name + type: string + description: Unique name of the component + owner: + title: Owner + type: string + description: Owner of the component + ui:field: OwnerPicker + ui:options: + catalogFilter: + kind: Group + - title: Choose a location + required: + - repoUrl + properties: + repoUrl: + title: Repository Location + type: string + ui:field: RepoUrlPicker + ui:options: + allowedHosts: + - github.com +steps: + - id: fetch-base + name: Fetch Base + action: fetch:template + input: + url: ./template + values: + name: \${{parameters.name}} +`; + +type TemplateOption = { + label: string; + value: Entity; +}; + +const useStyles = makeStyles(theme => ({ + root: { + gridArea: 'pageContent', + display: 'grid', + gridTemplateAreas: ` + "controls controls" + "textArea preview" + `, + gridTemplateRows: 'auto 1fr', + gridTemplateColumns: '1fr 1fr', + }, + controls: { + gridArea: 'controls', + display: 'flex', + flexFlow: 'row nowrap', + alignItems: 'center', + margin: theme.spacing(1), + }, + textArea: { + gridArea: 'textArea', + }, + preview: { + gridArea: 'preview', + }, +})); + +export const TemplateFormPreviewer = ({ + defaultPreviewTemplate = EXAMPLE_TEMPLATE_PARAMS_YAML, + customFieldExtensions = [], + onClose, + layouts = [], +}: { + defaultPreviewTemplate?: string; + customFieldExtensions?: NextFieldExtensionOptions[]; + onClose?: () => void; + layouts?: LayoutOptions[]; +}) => { + const classes = useStyles(); + const alertApi = useApi(alertApiRef); + const catalogApi = useApi(catalogApiRef); + const [selectedTemplate, setSelectedTemplate] = useState(''); + const [errorText, setErrorText] = useState(); + const [templateOptions, setTemplateOptions] = useState([]); + const [templateYaml, setTemplateYaml] = useState(defaultPreviewTemplate); + + const { loading } = useAsync( + () => + catalogApi + .getEntities({ + filter: { kind: 'template' }, + fields: [ + 'kind', + 'metadata.namespace', + 'metadata.name', + 'metadata.title', + 'spec.parameters', + 'spec.steps', + 'spec.output', + ], + }) + .then(({ items }) => + setTemplateOptions( + items.map(template => ({ + label: + template.metadata.title ?? + humanizeEntityRef(template, { defaultKind: 'template' }), + value: template, + })), + ), + ) + .catch(e => + alertApi.post({ + message: `Error loading exisiting templates: ${e.message}`, + severity: 'error', + }), + ), + [catalogApi], + ); + + const handleSelectChange = useCallback( + selected => { + setSelectedTemplate(selected); + setTemplateYaml(yaml.stringify(selected.spec)); + }, + [setTemplateYaml], + ); + + return ( + <> + {loading && } +
    +
    + + + Load Existing Template + + + + + + + +
    +
    + +
    +
    + +
    +
    + + ); +}; diff --git a/plugins/scaffolder/src/next/TemplateEditorPage/index.ts b/plugins/scaffolder/src/next/TemplateEditorPage/index.ts new file mode 100644 index 0000000000..7ec6bddb64 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateEditorPage/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2022 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 { TemplateEditorPage } from './TemplateEditorPage'; diff --git a/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx b/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx new file mode 100644 index 0000000000..3bca46af2a --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx @@ -0,0 +1,123 @@ +/* + * 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 { useRouteRef } from '@backstage/core-plugin-api'; +import IconButton from '@material-ui/core/IconButton'; +import ListItemIcon from '@material-ui/core/ListItemIcon'; +import ListItemText from '@material-ui/core/ListItemText'; +import MenuItem from '@material-ui/core/MenuItem'; +import MenuList from '@material-ui/core/MenuList'; +import Popover from '@material-ui/core/Popover'; +import { makeStyles } from '@material-ui/core/styles'; +import Description from '@material-ui/icons/Description'; +import Edit from '@material-ui/icons/Edit'; +import List from '@material-ui/icons/List'; +import MoreVert from '@material-ui/icons/MoreVert'; +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + nextActionsRouteRef, + nextEditRouteRef, + nextScaffolderListTaskRouteRef, +} from '../routes'; + +const useStyles = makeStyles({ + button: { + color: 'white', + }, +}); + +export type ScaffolderPageContextMenuProps = { + editor?: boolean; + actions?: boolean; + tasks?: boolean; +}; + +export function ContextMenu(props: ScaffolderPageContextMenuProps) { + const classes = useStyles(); + const [anchorEl, setAnchorEl] = useState(); + const editLink = useRouteRef(nextEditRouteRef); + const actionsLink = useRouteRef(nextActionsRouteRef); + const tasksLink = useRouteRef(nextScaffolderListTaskRouteRef); + + const navigate = useNavigate(); + + const showEditor = props.editor !== false; + const showActions = props.actions !== false; + const showTasks = props.tasks !== false; + + if (!showEditor && !showActions) { + return null; + } + + const onOpen = (event: React.SyntheticEvent) => { + setAnchorEl(event.currentTarget); + }; + + const onClose = () => { + setAnchorEl(undefined); + }; + + return ( + <> + + + + + + {showEditor && ( + navigate(editLink())}> + + + + + + )} + {showActions && ( + navigate(actionsLink())}> + + + + + + )} + {showTasks && ( + navigate(tasksLink())}> + + + + + + )} + + + + ); +} diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx index 4eae84b866..b701ceb3c4 100644 --- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx +++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx @@ -37,12 +37,18 @@ import { RegisterExistingButton } from './RegisterExistingButton'; import { useRouteRef } from '@backstage/core-plugin-api'; import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups'; import { registerComponentRouteRef } from '../../routes'; +import { ContextMenu } from './ContextMenu'; export type TemplateListPageProps = { TemplateCardComponent?: React.ComponentType<{ template: TemplateEntityV1beta3; }>; groups?: TemplateGroupFilter[]; + contextMenu?: { + editor?: boolean; + actions?: boolean; + tasks?: boolean; + }; }; const defaultGroup: TemplateGroupFilter = { @@ -61,7 +67,9 @@ export const TemplateListPage = (props: TemplateListPageProps) => { pageTitleOverride="Create a new component" title="Create a new component" subtitle="Create new software components using standard templates in your organization" - /> + > + +
    []; @@ -43,12 +48,12 @@ export type TemplateWizardPageProps = { export const TemplateWizardPage = (props: TemplateWizardPageProps) => { const rootRef = useRouteRef(nextRouteRef); - const taskRoute = useRouteRef(scaffolderTaskRouteRef); + const taskRoute = useRouteRef(nextScaffolderTaskRouteRef); const { secrets } = useTemplateSecrets(); const scaffolderApi = useApi(scaffolderApiRef); const navigate = useNavigate(); const { templateName, namespace } = useRouteRefParams( - selectedTemplateRouteRef, + nextSelectedTemplateRouteRef, ); const templateRef = stringifyEntityRef({ diff --git a/plugins/scaffolder/src/next/routes.ts b/plugins/scaffolder/src/next/routes.ts index 620122ee60..12fad4210a 100644 --- a/plugins/scaffolder/src/next/routes.ts +++ b/plugins/scaffolder/src/next/routes.ts @@ -34,3 +34,24 @@ export const nextScaffolderTaskRouteRef = createSubRouteRef({ parent: nextRouteRef, path: '/tasks/:taskId', }); + +/** @alpha */ +export const nextScaffolderListTaskRouteRef = createSubRouteRef({ + id: 'scaffolder/next/list-tasks', + parent: nextRouteRef, + path: '/tasks', +}); + +/** @alpha */ +export const nextActionsRouteRef = createSubRouteRef({ + id: 'scaffolder/next/actions', + parent: nextRouteRef, + path: '/actions', +}); + +/** @alpha */ +export const nextEditRouteRef = createSubRouteRef({ + id: 'scaffolder/next/edit', + parent: nextRouteRef, + path: '/edit', +}); diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md index dddb776afc..71dccfef4f 100644 --- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md +++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-search-backend-module-elasticsearch +## 1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + ## 1.1.3-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-elasticsearch/api-report.md b/plugins/search-backend-module-elasticsearch/api-report.md index 9e3ca40ec7..8545e3fd8f 100644 --- a/plugins/search-backend-module-elasticsearch/api-report.md +++ b/plugins/search-backend-module-elasticsearch/api-report.md @@ -67,6 +67,11 @@ export interface BaseElasticSearchClientOptions { Transport?: ElasticSearchTransportConstructor; } +// @public (undocumented) +export function decodeElasticSearchPageCursor(pageCursor?: string): { + page: number; +}; + // @public (undocumented) export interface ElasticSearchAgentOptions { // (undocumented) diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json index 05395fbdb2..2eb85f5f2c 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": "1.1.3-next.1", + "version": "1.1.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts index 9edad973de..c2f4224db3 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/ElasticSearchSearchEngine.ts @@ -408,6 +408,9 @@ export class ElasticSearchSearchEngine implements SearchEngine { } } +/** + * @public + */ export function decodePageCursor(pageCursor?: string): { page: number } { if (!pageCursor) { return { page: 0 }; diff --git a/plugins/search-backend-module-elasticsearch/src/engines/index.ts b/plugins/search-backend-module-elasticsearch/src/engines/index.ts index 8c3b9e3c6d..cb24bdc56a 100644 --- a/plugins/search-backend-module-elasticsearch/src/engines/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/engines/index.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -export { ElasticSearchSearchEngine } from './ElasticSearchSearchEngine'; +export { + decodePageCursor as decodeElasticSearchPageCursor, + ElasticSearchSearchEngine, +} from './ElasticSearchSearchEngine'; export { isOpenSearchCompatible } from './ElasticSearchClientOptions'; export type { BaseElasticSearchClientOptions, diff --git a/plugins/search-backend-module-elasticsearch/src/index.ts b/plugins/search-backend-module-elasticsearch/src/index.ts index cb4f62098a..e0b05014cc 100644 --- a/plugins/search-backend-module-elasticsearch/src/index.ts +++ b/plugins/search-backend-module-elasticsearch/src/index.ts @@ -20,7 +20,11 @@ * @packageDocumentation */ -export { ElasticSearchSearchEngine, isOpenSearchCompatible } from './engines'; +export { + decodeElasticSearchPageCursor, + ElasticSearchSearchEngine, + isOpenSearchCompatible, +} from './engines'; export type { BaseElasticSearchClientOptions, ElasticSearchAgentOptions, diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md index 907235f958..1d17c6707c 100644 --- a/plugins/search-backend-module-pg/CHANGELOG.md +++ b/plugins/search-backend-module-pg/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-search-backend-module-pg +## 0.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + ## 0.5.3-next.1 ### Patch Changes diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json index 299c66a0bd..7883475f34 100644 --- a/plugins/search-backend-module-pg/package.json +++ b/plugins/search-backend-module-pg/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-module-pg", "description": "A module for the search backend that implements search using PostgreSQL", - "version": "0.5.3-next.1", + "version": "0.5.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md index 9a5b76131d..bf9306cb1d 100644 --- a/plugins/search-backend-node/CHANGELOG.md +++ b/plugins/search-backend-node/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-search-backend-node +## 1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + ## 1.1.3-next.1 ### Patch Changes diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json index 9c4f3842ed..1bf73971d1 100644 --- a/plugins/search-backend-node/package.json +++ b/plugins/search-backend-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend-node", "description": "A library for Backstage backend plugins that want to interact with the search backend plugin", - "version": "1.1.3-next.1", + "version": "1.1.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md index 33fa09cb39..c31930c867 100644 --- a/plugins/search-backend/CHANGELOG.md +++ b/plugins/search-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-search-backend +## 1.2.3-next.2 + +### Patch Changes + +- 1b30c9a0f7: Change the router's response to include the error message instead of its object type in case it fails during a search query because the messages have more info. +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/plugin-permission-node@0.7.5-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-backend-node@1.1.3-next.2 + - @backstage/plugin-search-common@1.2.1 + ## 1.2.3-next.1 ### Patch Changes diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json index cc903a5b8d..31594bc8af 100644 --- a/plugins/search-backend/package.json +++ b/plugins/search-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search-backend", "description": "The Backstage backend plugin that provides your backstage app with search", - "version": "1.2.3-next.1", + "version": "1.2.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-backend/src/service/router.test.ts b/plugins/search-backend/src/service/router.test.ts index 1dbd145bce..b4772dd62d 100644 --- a/plugins/search-backend/src/service/router.test.ts +++ b/plugins/search-backend/src/service/router.test.ts @@ -74,6 +74,23 @@ describe('createRouter', () => { }); describe('GET /query', () => { + it('throws meaningful query errors', async () => { + const error = new Error('Query error message'); + mockSearchEngine.query.mockRejectedValueOnce(error); + + const response = await request(app).get('/query'); + + expect(response.status).toEqual(500); + expect(response.body).toMatchObject( + expect.objectContaining({ + error: { + name: 'Error', + message: `There was a problem performing the search query: ${error.message}`, + }, + }), + ); + }); + it('returns empty results array', async () => { const response = await request(app).get('/query'); diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index b51dcead14..10f47d180f 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -184,7 +184,7 @@ export async function createRouter( } throw new Error( - `There was a problem performing the search query. ${error}`, + `There was a problem performing the search query: ${error.message}`, ); } }, diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md index f5074bee18..fa58979796 100644 --- a/plugins/search-react/CHANGELOG.md +++ b/plugins/search-react/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-search-react +## 1.5.0-next.1 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-search-common@1.2.1 + ## 1.5.0-next.0 ### Minor Changes diff --git a/plugins/search-react/api-report.md b/plugins/search-react/api-report.md index ccf7a72e33..375af20d5c 100644 --- a/plugins/search-react/api-report.md +++ b/plugins/search-react/api-report.md @@ -14,6 +14,7 @@ import { InputBaseProps } from '@material-ui/core'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; import { LinkProps } from '@backstage/core-components'; +import { ListItemProps } from '@material-ui/core'; import { ListItemTextProps } from '@material-ui/core'; import { ListProps } from '@material-ui/core'; import { PropsWithChildren } from 'react'; @@ -55,6 +56,7 @@ export type DefaultResultListItemProps = { highlight?: ResultHighlight; rank?: number; lineClamp?: number; + toggleModal?: () => void; }; // @public (undocumented) @@ -391,6 +393,16 @@ export type SearchResultListItemExtensionOptions< predicate?: (result: SearchResult_2) => boolean; }; +// @public +export type SearchResultListItemExtensionProps = Props & + PropsWithChildren< + { + rank?: number; + result?: SearchDocument; + noTrack?: boolean; + } & Omit + >; + // @public export const SearchResultListItemExtensions: ( props: SearchResultListItemExtensionsProps, diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json index 4e9dd292ba..d353189add 100644 --- a/plugins/search-react/package.json +++ b/plugins/search-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search-react", - "version": "1.5.0-next.0", + "version": "1.5.0-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx index e94d394639..aab105df48 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.test.tsx @@ -35,9 +35,10 @@ describe('DefaultResultListItem', () => { it('Includes primary/secondary text (title / text)', async () => { await renderInTestApp(); - expect(screen.getByRole('listitem')).toHaveTextContent( - result.title + result.text, - ); + expect( + screen.getByRole('link', { name: result.title }), + ).toBeInTheDocument(); + expect(screen.getByText(result.text)).toBeInTheDocument(); }); it('should render icon if prop is specified', async () => { diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx index 6ea5e4d1a2..cf89b6a9cd 100644 --- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx +++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx @@ -21,13 +21,7 @@ import { SearchDocument, } from '@backstage/plugin-search-common'; import { HighlightedSearchResultText } from '../HighlightedSearchResultText'; -import { - ListItem, - ListItemIcon, - ListItemText, - Box, - Divider, -} from '@material-ui/core'; +import { ListItemIcon, ListItemText, Box } from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import { Link } from '@backstage/core-components'; @@ -43,6 +37,7 @@ export type DefaultResultListItemProps = { highlight?: ResultHighlight; rank?: number; lineClamp?: number; + toggleModal?: () => void; }; /** @@ -61,48 +56,45 @@ export const DefaultResultListItemComponent = ({ return ( <> - - {icon && {icon}} - - {highlight?.fields.title ? ( - - ) : ( - result.title - )} - - } - secondary={ - - {highlight?.fields.text ? ( - - ) : ( - result.text - )} - - } - /> - {secondaryAction && {secondaryAction}} - - + {icon && {icon}} + + {highlight?.fields.title ? ( + + ) : ( + result.title + )} + + } + secondary={ + + {highlight?.fields.text ? ( + + ) : ( + result.text + )} + + } + /> + {secondaryAction && {secondaryAction}} ); }; diff --git a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx index 6a5a683295..4b8caa2780 100644 --- a/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx +++ b/plugins/search-react/src/components/SearchResult/SearchResult.test.tsx @@ -16,8 +16,6 @@ import React from 'react'; import { waitFor } from '@testing-library/react'; - -import { ListItem } from '@material-ui/core'; import { wrapInTestApp, renderInTestApp, @@ -217,8 +215,7 @@ describe('SearchResult', () => { }).provide( createSearchResultListItemExtension({ name: 'SearchResultExtension', - component: async () => props => - Result: {props.result?.title}, + component: async () => props => <>Result: {props.result?.title}, }), ); diff --git a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx index 728bd9113a..8a9e8986b8 100644 --- a/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx +++ b/plugins/search-react/src/components/SearchResultGroup/SearchResultGroup.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ListItem, MenuItem } from '@material-ui/core'; +import { MenuItem } from '@material-ui/core'; import DocsIcon from '@material-ui/icons/InsertDriveFile'; import { @@ -143,8 +143,7 @@ describe('SearchResultGroup', () => { }).provide( createSearchResultListItemExtension({ name: 'SearchResultGroupItemExtension', - component: async () => props => - Result: {props.result?.title}, + component: async () => props => <>Result: {props.result?.title}, }), ); diff --git a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx index 7e8aa7bb33..a0f5bc7830 100644 --- a/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx +++ b/plugins/search-react/src/components/SearchResultList/SearchResultList.test.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { screen, waitFor } from '@testing-library/react'; -import { ListItem } from '@material-ui/core'; import { TestApiProvider, renderWithEffects, @@ -226,8 +225,7 @@ describe('SearchResultList', () => { }).provide( createSearchResultListItemExtension({ name: 'SearchResultListItemExtension', - component: async () => props => - Result: {props.result?.title}, + component: async () => props => <>Result: {props.result?.title}, }), ); diff --git a/plugins/search-react/src/extensions.test.tsx b/plugins/search-react/src/extensions.test.tsx index 24c115e572..7e5070c738 100644 --- a/plugins/search-react/src/extensions.test.tsx +++ b/plugins/search-react/src/extensions.test.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { ListItem, ListItemText } from '@material-ui/core'; +import { ListItemText } from '@material-ui/core'; import { wrapInTestApp, @@ -75,9 +75,9 @@ const createExtension = ( predicate, component = async () => (props: { result?: SearchDocument }) => ( - + <> - + ), } = options; return plugin.provide( @@ -120,7 +120,7 @@ describe('extensions', () => { ); await userEvent.click( - screen.getByRole('button', { name: /Search Result 1/ }), + screen.getByRole('link', { name: /Search Result 1/ }), ); expect(analyticsApiMock.getEvents()[0]).toMatchObject({ @@ -146,9 +146,7 @@ describe('extensions', () => { expect(screen.getByText('Default')).toBeInTheDocument(); expect(screen.getByText('Search Result 1')).toBeInTheDocument(); - await userEvent.click( - screen.getByRole('button', { name: /Search Result 1/ }), - ); + await userEvent.click(screen.getByRole('listitem')); expect(analyticsApiMock.getEvents()[0]).toMatchObject({ action: 'discover', @@ -184,9 +182,9 @@ describe('extensions', () => { predicate: (result: SearchResult) => result.type === 'explore', component: async () => (props: { result?: SearchDocument }) => ( - + <> - + ), }); diff --git a/plugins/search-react/src/extensions.tsx b/plugins/search-react/src/extensions.tsx index 1e0619b14c..6c854e24e0 100644 --- a/plugins/search-react/src/extensions.tsx +++ b/plugins/search-react/src/extensions.tsx @@ -33,7 +33,7 @@ import { } from '@backstage/core-plugin-api'; import { SearchDocument, SearchResult } from '@backstage/plugin-search-common'; -import { Box, List, ListProps } from '@material-ui/core'; +import { ListItem, List, ListProps, ListItemProps } from '@material-ui/core'; import { DefaultResultListItem } from './components'; @@ -73,26 +73,34 @@ const findSearchResultListItemExtensionElement = ( }; /** - * @internal - * Props for {@link SearchResultListItemExtension}. + * @public + * Extends props for any search result list item extension */ -type SearchResultListItemExtensionProps = PropsWithChildren<{ - rank?: number; - result?: SearchDocument; - noTrack?: boolean; -}>; +export type SearchResultListItemExtensionProps = Props & + PropsWithChildren< + { + rank?: number; + result?: SearchDocument; + noTrack?: boolean; + } & Omit + >; /** * @internal * Extends children with extension capabilities. * @param props - see {@link SearchResultListItemExtensionProps}. */ -const SearchResultListItemExtension = ({ - rank, - result, - noTrack, - children, -}: SearchResultListItemExtensionProps) => { +const SearchResultListItemExtension = ( + props: SearchResultListItemExtensionProps, +) => { + const { + rank, + result, + noTrack, + children, + alignItems = 'flex-start', + ...rest + } = props; const analytics = useAnalytics(); const handleClickCapture = useCallback(() => { @@ -105,9 +113,14 @@ const SearchResultListItemExtension = ({ }, [rank, result, noTrack, analytics]); return ( - + {children} - + ); }; diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 4e4d5de406..7853a44abb 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-search +## 1.1.0-next.2 + +### Patch Changes + +- 66e2aab4c4: Updated colors for each tab used on search modal, to improve color contrast. Aria label added to tabs wrapper component. `disableRipple` property on the `Tab` component removed to improve keyboard navigation indicator. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/version-bridge@1.0.3 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + ## 1.1.0-next.1 ### Minor Changes diff --git a/plugins/search/api-report.md b/plugins/search/api-report.md index cdbdc4c095..09c12cd776 100644 --- a/plugins/search/api-report.md +++ b/plugins/search/api-report.md @@ -97,6 +97,7 @@ export type SearchTypeAccordionProps = { icon: JSX.Element; }>; defaultValue?: string; + showCounts?: boolean; }; // @public diff --git a/plugins/search/package.json b/plugins/search/package.json index 6acb85af9b..c2f635f762 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-search", "description": "The Backstage plugin that provides your backstage app with search", - "version": "1.1.0-next.1", + "version": "1.1.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/search/src/components/SearchModal/SearchModal.tsx b/plugins/search/src/components/SearchModal/SearchModal.tsx index d17cf0f6ba..c9621c5940 100644 --- a/plugins/search/src/components/SearchModal/SearchModal.tsx +++ b/plugins/search/src/components/SearchModal/SearchModal.tsx @@ -42,7 +42,7 @@ import { rootRouteRef } from '../../plugin'; /** * @public - **/ + */ export interface SearchModalChildrenProps { /** * A function that should be invoked when navigating away from the modal. @@ -52,7 +52,7 @@ export interface SearchModalChildrenProps { /** * @public - **/ + */ export interface SearchModalProps { /** * If true, it renders the modal. diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx index da1cae46b9..e0acee025a 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.test.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { TestApiProvider } from '@backstage/test-utils'; -import { act, render } from '@testing-library/react'; +import { act, render, waitFor } from '@testing-library/react'; import user from '@testing-library/user-event'; import { searchApiRef, @@ -34,6 +34,8 @@ jest.mock('@backstage/plugin-search-react', () => ({ setTypes: (types: any) => setTypesMock(types), pageCursor: '', setPageCursor: (pageCursor: any) => setPageCursorMock(pageCursor), + term: 'abc', + filters: { foo: 'bar' }, }), })); @@ -48,7 +50,7 @@ describe('SearchType.Accordion', () => { }; beforeEach(() => { - query.mockResolvedValue({ results: [] }); + query.mockResolvedValue({ results: [], numberOfResults: 1234 }); }); const Wrapper = ({ children }: { children: React.ReactNode }) => { @@ -132,4 +134,34 @@ describe('SearchType.Accordion', () => { expect(queryByText('Collapse')).not.toBeInTheDocument(); }); + + it('should show result counts if enabled', async () => { + const { getAllByText } = render( + + + , + ); + + expect(query).toHaveBeenCalledWith({ + term: 'abc', + types: [], + filters: { foo: 'bar' }, + pageLimit: 0, + }); + expect(query).toHaveBeenCalledWith({ + term: 'abc', + types: [expectedType.value], + filters: {}, + pageLimit: 0, + }); + await waitFor(() => { + const countLabels = getAllByText('1234 results'); + expect(countLabels.length).toEqual(2); + expect(countLabels[0]).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx index 49f5cf74ca..67785b89cb 100644 --- a/plugins/search/src/components/SearchType/SearchType.Accordion.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Accordion.tsx @@ -15,7 +15,8 @@ */ import React, { cloneElement, Fragment, useEffect, useState } from 'react'; -import { useSearch } from '@backstage/plugin-search-react'; +import { useApi } from '@backstage/core-plugin-api'; +import { searchApiRef, useSearch } from '@backstage/plugin-search-react'; import { Accordion, AccordionSummary, @@ -32,6 +33,7 @@ import { } from '@material-ui/core'; import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import AllIcon from '@material-ui/icons/FontDownload'; +import useAsync from 'react-use/lib/useAsync'; const useStyles = makeStyles(theme => ({ card: { @@ -81,13 +83,15 @@ export type SearchTypeAccordionProps = { icon: JSX.Element; }>; defaultValue?: string; + showCounts?: boolean; }; export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { const classes = useStyles(); - const { setPageCursor, setTypes, types } = useSearch(); + const { filters, setPageCursor, setTypes, term, types } = useSearch(); + const searchApi = useApi(searchApiRef); const [expanded, setExpanded] = useState(true); - const { defaultValue, name, types: givenTypes } = props; + const { defaultValue, name, showCounts, types: givenTypes } = props; const toggleExpanded = () => setExpanded(prevState => !prevState); const handleClick = (type: string) => { @@ -116,6 +120,37 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { ]; const selected = types[0] || ''; + const { value: resultCounts } = useAsync(async () => { + if (!showCounts) { + return {}; + } + + const counts = await Promise.all( + definedTypes + .map(t => t.value) + .map(async type => { + const { numberOfResults } = await searchApi.query({ + term, + types: type ? [type] : [], + filters: + types.includes(type) || (!types.length && !type) ? filters : {}, + pageLimit: 0, + }); + + return [ + type, + numberOfResults !== undefined + ? `${ + numberOfResults >= 10000 ? `>10000` : numberOfResults + } results` + : ' -- ', + ]; + }), + ); + + return Object.fromEntries(counts); + }, [filters, showCounts, term, types]); + return ( @@ -161,7 +196,10 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => { className: classes.listItemIcon, })} - + ))} diff --git a/plugins/search/src/components/SearchType/SearchType.Tabs.tsx b/plugins/search/src/components/SearchType/SearchType.Tabs.tsx index fc0380e23a..57a311b00c 100644 --- a/plugins/search/src/components/SearchType/SearchType.Tabs.tsx +++ b/plugins/search/src/components/SearchType/SearchType.Tabs.tsx @@ -22,13 +22,12 @@ import { makeStyles, Tab, Tabs } from '@material-ui/core'; const useStyles = makeStyles((theme: BackstageTheme) => ({ tabs: { borderBottom: `1px solid ${theme.palette.textVerySubtle}`, - padding: theme.spacing(0, 4), }, tab: { height: '50px', fontWeight: theme.typography.fontWeightBold, fontSize: theme.typography.pxToRem(13), - color: theme.palette.textSubtle, + color: theme.palette.text.primary, minWidth: '130px', }, })); @@ -72,6 +71,7 @@ export const SearchTypeTabs = (props: SearchTypeTabsProps) => { return ( { diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index b19a4b29d9..922b06c8d8 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-sentry +## 0.5.0-next.2 + +### Minor Changes + +- 6a8d6b9a0c: Add option to filter issues based on the time it was triggered + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.4.8-next.1 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index f9b5dad4b8..e757ff25a4 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sentry", "description": "A Backstage plugin that integrates towards Sentry", - "version": "0.4.8-next.1", + "version": "0.5.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx index 647208f141..5bf6b1e92b 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.test.tsx @@ -19,6 +19,7 @@ import SentryIssuesTable from './SentryIssuesTable'; import { SentryIssue } from '../../api'; import mockIssue from '../../api/mock/sentry-issue-mock.json'; import { renderInTestApp } from '@backstage/test-utils'; +import { DateTime } from 'luxon'; describe('SentryIssuesTable', () => { it('should render headers in a table', async () => { @@ -62,6 +63,7 @@ describe('SentryIssuesTable', () => { }, count: '101', userCount: 202, + lastSeen: DateTime.now().toISO(), }, ]; const table = await renderInTestApp( @@ -105,6 +107,6 @@ describe('SentryIssuesTable', () => { }} />, ); - expect(await table.findByText('Last 24h')).toBeInTheDocument(); + expect(await table.findByText('Stats for 24h')).toBeInTheDocument(); }); }); diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index abe29efd7f..60ee6b7dbf 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -14,13 +14,19 @@ * limitations under the License. */ -import React from 'react'; +import React, { useCallback, useState } from 'react'; import { SentryIssue } from '../../api'; -import { DateTime } from 'luxon'; +import { DateTime, Duration } from 'luxon'; import { ErrorCell } from '../ErrorCell/ErrorCell'; import { ErrorGraph } from '../ErrorGraph/ErrorGraph'; import { Table, TableColumn } from '@backstage/core-components'; +import Select from '@material-ui/core/Select'; import { Options } from '@material-table/core'; +import { FormControl, Grid, MenuItem } from '@material-ui/core'; + +const ONE_DAY_IN_MILLIS = 86400000; +const SEVEN_DAYS_IN_MILLIS = ONE_DAY_IN_MILLIS * 7; +const FOURTEEN_DAYS_IN_MILLIS = ONE_DAY_IN_MILLIS * 14; const columns: TableColumn[] = [ { @@ -66,14 +72,55 @@ type SentryIssuesTableProps = { const SentryIssuesTable = (props: SentryIssuesTableProps) => { const { sentryIssues, statsFor, tableOptions } = props; + const [selected, setSelected] = useState(ONE_DAY_IN_MILLIS); + + const filterByDate = useCallback((issue, selectedFilter) => { + return ( + DateTime.fromISO(issue.lastSeen) > + DateTime.now().minus(Duration.fromMillis(selectedFilter)) + ); + }, []); + const [filteredIssues, setFilteredIssues] = useState( + sentryIssues.filter(i => filterByDate(i, selected)), + ); + + const handleFilterChange = ( + event: React.ChangeEvent<{ name?: string; value: unknown }>, + ) => { + const item = event.target.value; + if (typeof item === 'number') { + setSelected(item); + if (item === Number.NEGATIVE_INFINITY) { + setFilteredIssues(sentryIssues); + return; + } + setFilteredIssues(sentryIssues.filter(i => filterByDate(i, item))); + } + }; return ( - + <> +
    + Sentry Issues + + + + + + + } + subtitle={statsFor ? `Stats for ${statsFor}` : undefined} + data={filteredIssues} + /> + ); }; diff --git a/plugins/shortcuts/CHANGELOG.md b/plugins/shortcuts/CHANGELOG.md index 224ad0a78d..94baec18aa 100644 --- a/plugins/shortcuts/CHANGELOG.md +++ b/plugins/shortcuts/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-shortcuts +## 0.3.7-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + ## 0.3.7-next.0 ### Patch Changes diff --git a/plugins/shortcuts/README.md b/plugins/shortcuts/README.md index d7f8f19aec..cd79901656 100644 --- a/plugins/shortcuts/README.md +++ b/plugins/shortcuts/README.md @@ -7,7 +7,8 @@ The shortcuts plugin allows a user to have easy access to pages within a Backsta ### Install the package: ```bash -yarn add @backstage/plugin-shortcuts +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-shortcuts ``` ### Register plugin: diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json index 3a955b95e0..61b48b1c55 100644 --- a/plugins/shortcuts/package.json +++ b/plugins/shortcuts/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-shortcuts", "description": "A Backstage plugin that provides a shortcuts feature to the sidebar", - "version": "0.3.7-next.0", + "version": "0.3.7-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/sonarqube-backend/CHANGELOG.md b/plugins/sonarqube-backend/CHANGELOG.md index 4f568d7442..6fb7414acb 100644 --- a/plugins/sonarqube-backend/CHANGELOG.md +++ b/plugins/sonarqube-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-sonarqube-backend +## 0.1.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + ## 0.1.7-next.1 ### Patch Changes diff --git a/plugins/sonarqube-backend/package.json b/plugins/sonarqube-backend/package.json index 785911f6c0..99c0a8b486 100644 --- a/plugins/sonarqube-backend/package.json +++ b/plugins/sonarqube-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube-backend", - "version": "0.1.7-next.1", + "version": "0.1.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/sonarqube-react/CHANGELOG.md b/plugins/sonarqube-react/CHANGELOG.md index 41b5d5b371..b938ce8b37 100644 --- a/plugins/sonarqube-react/CHANGELOG.md +++ b/plugins/sonarqube-react/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/plugin-sonarqube-react +## 0.1.2-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-plugin-api@1.3.0 + ## 0.1.2-next.0 ### Patch Changes diff --git a/plugins/sonarqube-react/package.json b/plugins/sonarqube-react/package.json index ebf75572ec..53837a47cf 100644 --- a/plugins/sonarqube-react/package.json +++ b/plugins/sonarqube-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube-react", - "version": "0.1.2-next.0", + "version": "0.1.2-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index a419f321e3..e51e0a65ac 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-sonarqube +## 0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-sonarqube-react@0.1.2-next.1 + ## 0.6.3-next.1 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index 8c5d706db2..80dd578523 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-sonarqube", "description": "", - "version": "0.6.3-next.1", + "version": "0.6.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md index 401f927bd9..c5de65a3d3 100644 --- a/plugins/splunk-on-call/CHANGELOG.md +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-splunk-on-call +## 0.4.4-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.4.4-next.1 ### Patch Changes diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index 0700dacf92..097cc1c6fd 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-splunk-on-call", "description": "A Backstage plugin that integrates towards Splunk On-Call", - "version": "0.4.4-next.1", + "version": "0.4.4-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts index 764ac94e2c..9beb4e150e 100644 --- a/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts +++ b/plugins/splunk-on-call/src/components/TriggerDialog/testUtils.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -// eslint-disable-next-line import/no-extraneous-dependencies +// eslint-disable-next-line @backstage/no-undeclared-imports import { act, fireEvent, diff --git a/plugins/stack-overflow-backend/CHANGELOG.md b/plugins/stack-overflow-backend/CHANGELOG.md index 5a8f26be5f..aac07808c9 100644 --- a/plugins/stack-overflow-backend/CHANGELOG.md +++ b/plugins/stack-overflow-backend/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/plugin-stack-overflow-backend +## 0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/plugin-search-common@1.2.1 + ## 0.1.11-next.1 ### Patch Changes diff --git a/plugins/stack-overflow-backend/package.json b/plugins/stack-overflow-backend/package.json index ca8e4a2a55..e40e3e846a 100644 --- a/plugins/stack-overflow-backend/package.json +++ b/plugins/stack-overflow-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow-backend", - "version": "0.1.11-next.1", + "version": "0.1.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/stack-overflow/CHANGELOG.md b/plugins/stack-overflow/CHANGELOG.md index 00ed8a363c..f80e350a03 100644 --- a/plugins/stack-overflow/CHANGELOG.md +++ b/plugins/stack-overflow/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-stack-overflow +## 0.1.11-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-home@0.4.31-next.2 + - @backstage/plugin-search-common@1.2.1 + ## 0.1.11-next.1 ### Patch Changes diff --git a/plugins/stack-overflow/package.json b/plugins/stack-overflow/package.json index d073d519c1..c9fbe7e7a3 100644 --- a/plugins/stack-overflow/package.json +++ b/plugins/stack-overflow/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-stack-overflow", - "version": "0.1.11-next.1", + "version": "0.1.11-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/stack-overflow/src/api/StackOverflowClient.ts b/plugins/stack-overflow/src/api/StackOverflowClient.ts index f4ff5bf190..f598f912bd 100644 --- a/plugins/stack-overflow/src/api/StackOverflowClient.ts +++ b/plugins/stack-overflow/src/api/StackOverflowClient.ts @@ -39,9 +39,8 @@ export class StackOverflowClient implements StackOverflowApi { } /** - * List Questions in the StackOverflow instance - * - * */ + * List questions in the StackOverflow instance + */ async listQuestions(options: { requestParams: StackOverflowQuestionsRequestParams; }): Promise { diff --git a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md index 64ceb662d2..28a8ae4e05 100644 --- a/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md +++ b/plugins/tech-insights-backend-module-jsonfc/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-tech-insights-backend-module-jsonfc +## 0.1.26-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + ## 0.1.26-next.1 ### Patch Changes diff --git a/plugins/tech-insights-backend-module-jsonfc/package.json b/plugins/tech-insights-backend-module-jsonfc/package.json index f7c1b6563b..d3a3260f87 100644 --- a/plugins/tech-insights-backend-module-jsonfc/package.json +++ b/plugins/tech-insights-backend-module-jsonfc/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend-module-jsonfc", - "version": "0.1.26-next.1", + "version": "0.1.26-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-backend/CHANGELOG.md b/plugins/tech-insights-backend/CHANGELOG.md index 42e4c273a0..bef4e4c472 100644 --- a/plugins/tech-insights-backend/CHANGELOG.md +++ b/plugins/tech-insights-backend/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-tech-insights-backend +## 0.5.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + - @backstage/plugin-tech-insights-node@0.4.0-next.2 + ## 0.5.8-next.1 ### Patch Changes diff --git a/plugins/tech-insights-backend/package.json b/plugins/tech-insights-backend/package.json index c04ce8f483..2b32f72763 100644 --- a/plugins/tech-insights-backend/package.json +++ b/plugins/tech-insights-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-backend", - "version": "0.5.8-next.1", + "version": "0.5.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-node/CHANGELOG.md b/plugins/tech-insights-node/CHANGELOG.md index 867a242826..aa058fdc3b 100644 --- a/plugins/tech-insights-node/CHANGELOG.md +++ b/plugins/tech-insights-node/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-tech-insights-node +## 0.4.0-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/types@1.0.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + ## 0.4.0-next.1 ### Minor Changes diff --git a/plugins/tech-insights-node/package.json b/plugins/tech-insights-node/package.json index 79f8846f51..7f49679030 100644 --- a/plugins/tech-insights-node/package.json +++ b/plugins/tech-insights-node/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights-node", - "version": "0.4.0-next.1", + "version": "0.4.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-insights-node/src/facts.ts b/plugins/tech-insights-node/src/facts.ts index 3619f8b66f..b35ad756ed 100644 --- a/plugins/tech-insights-node/src/facts.ts +++ b/plugins/tech-insights-node/src/facts.ts @@ -162,7 +162,7 @@ export interface FactRetriever { * \{ timeToLive: 1209600000 \} * \{ timeToLive: \{ weeks: 4 \} \} * - **/ + */ export type TTL = { timeToLive: DurationLike }; /** @@ -172,7 +172,7 @@ export type TTL = { timeToLive: DurationLike }; * @example * \{ maxItems: 10 \} * - **/ + */ export type MaxItems = { maxItems: number }; /** diff --git a/plugins/tech-insights/CHANGELOG.md b/plugins/tech-insights/CHANGELOG.md index 907fc50495..37e46a4b5a 100644 --- a/plugins/tech-insights/CHANGELOG.md +++ b/plugins/tech-insights/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-tech-insights +## 0.3.7-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-tech-insights-common@0.2.10-next.0 + ## 0.3.7-next.1 ### Patch Changes diff --git a/plugins/tech-insights/package.json b/plugins/tech-insights/package.json index f4515d94b0..e874d2ba30 100644 --- a/plugins/tech-insights/package.json +++ b/plugins/tech-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-insights", - "version": "0.3.7-next.1", + "version": "0.3.7-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index f6983f4875..37aa1e8336 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,30 @@ # @backstage/plugin-tech-radar +## 0.6.1-next.1 + +### Patch Changes + +- acf6c8c10b: Deprecate `RadarEntry.url` - use `RadarEntry.links` instead + + ```diff + - url: 'https://www.javascript.com/', + key: 'javascript', + id: 'javascript', + title: 'JavaScript', + quadrant: 'languages', + links: [ + + { + + url: 'https://www.javascript.com/', + + title: 'Learn more', + + }, + ], + ``` + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/theme@0.2.16 + ## 0.6.1-next.0 ### Patch Changes diff --git a/plugins/tech-radar/api-report.md b/plugins/tech-radar/api-report.md index e2744872d8..3b84990d85 100644 --- a/plugins/tech-radar/api-report.md +++ b/plugins/tech-radar/api-report.md @@ -25,7 +25,8 @@ export interface RadarEntry { quadrant: string; timeline: Array; title: string; - url: string; + // @deprecated + url?: string; } // @public diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 78b0afd963..2aad00f224 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-tech-radar", "description": "A Backstage plugin that lets you display a Tech Radar for your organization", - "version": "0.6.1-next.0", + "version": "0.6.1-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/tech-radar/src/api.ts b/plugins/tech-radar/src/api.ts index 0fbc88255a..c0b8fb8852 100644 --- a/plugins/tech-radar/src/api.ts +++ b/plugins/tech-radar/src/api.ts @@ -133,8 +133,10 @@ export interface RadarEntry { * @remarks * * You can use `#` if you don't want to provide any other url + * + * @deprecated Use {@link RadarEntry.links} instead */ - url: string; + url?: string; /** * History of the Entry moving through {@link RadarRing} */ diff --git a/plugins/tech-radar/src/sample.ts b/plugins/tech-radar/src/sample.ts index 1c4f7a1a01..395b4f2ff5 100644 --- a/plugins/tech-radar/src/sample.ts +++ b/plugins/tech-radar/src/sample.ts @@ -45,12 +45,15 @@ entries.push({ 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua', }, ], - url: 'https://www.javascript.com/', key: 'javascript', id: 'javascript', title: 'JavaScript', quadrant: 'languages', links: [ + { + url: 'https://www.javascript.com/', + title: 'Learn more', + }, { url: 'https://www.typescriptlang.org/', title: 'TypeScript', @@ -69,7 +72,6 @@ entries.push({ 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat', }, ], - url: '#', key: 'typescript', id: 'typescript', title: 'TypeScript', @@ -87,7 +89,12 @@ entries.push({ 'Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur', }, ], - url: 'https://webpack.js.org/', + links: [ + { + url: 'https://webpack.js.org/', + title: 'Learn more', + }, + ], key: 'webpack', id: 'webpack', title: 'Webpack', @@ -101,7 +108,12 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: 'https://reactjs.org/', + links: [ + { + url: 'https://reactjs.org/', + title: 'Learn more', + }, + ], key: 'react', id: 'react', title: 'React', @@ -115,7 +127,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'code-reviews', id: 'code-reviews', title: 'Code Reviews', @@ -129,7 +140,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'mob-programming', id: 'mob-programming', title: 'Mob Programming', @@ -143,7 +153,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'docs-like-code', id: 'docs-like-code', title: 'Docs-like-code', @@ -156,7 +165,6 @@ entries.push({ date: new Date('2020-08-06'), }, ], - url: '#', key: 'force-push', id: 'force-push', title: 'Force push to master', @@ -170,7 +178,12 @@ entries.push({ description: 'long description', }, ], - url: 'https://github.com', + links: [ + { + url: 'https://github.com', + title: 'Learn more', + }, + ], key: 'github-actions', id: 'github-actions', title: 'GitHub Actions', diff --git a/plugins/techdocs-addons-test-utils/CHANGELOG.md b/plugins/techdocs-addons-test-utils/CHANGELOG.md index bba46a23c2..9afcf3aace 100644 --- a/plugins/techdocs-addons-test-utils/CHANGELOG.md +++ b/plugins/techdocs-addons-test-utils/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-techdocs-addons-test-utils +## 1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/plugin-techdocs@1.5.0-next.2 + - @backstage/plugin-catalog@1.8.0-next.2 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/test-utils@1.2.5-next.0 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + ## 1.0.10-next.1 ### Patch Changes diff --git a/plugins/techdocs-addons-test-utils/package.json b/plugins/techdocs-addons-test-utils/package.json index 82d5913a2e..1ad762a09d 100644 --- a/plugins/techdocs-addons-test-utils/package.json +++ b/plugins/techdocs-addons-test-utils/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-addons-test-utils", - "version": "1.0.10-next.1", + "version": "1.0.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 04c7a54f20..dbad0b17b1 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-techdocs-backend +## 1.5.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/plugin-catalog-common@1.0.11-next.1 + - @backstage/plugin-permission-common@0.7.3 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-node@1.4.6-next.2 + ## 1.5.3-next.1 ### Patch Changes diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index dd16050e5d..9c8ce22bef 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-backend", "description": "The Backstage backend plugin that renders technical documentation for your components", - "version": "1.5.3-next.1", + "version": "1.5.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 09eee1fc12..b4c2d2ee26 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -186,6 +186,9 @@ export class DocsBuilder { etag: newEtag, logger: this.logger, logStream: this.logStream, + siteOptions: { + name: this.entity.metadata.title ?? this.entity.metadata.name, + }, }); // Remove Prepared directory since it is no longer needed. diff --git a/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts b/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts index 039f1db09f..fa4c81682c 100644 --- a/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts +++ b/plugins/techdocs-backend/src/cache/TechDocsCache.test.ts @@ -31,6 +31,7 @@ describe('TechDocsCache', () => { get: jest.fn(), set: jest.fn(), delete: jest.fn(), + withOptions: jest.fn(), }; CacheUnderTest = TechDocsCache.fromConfig(new ConfigReader({}), { cache: MockClient, diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts index 1445079d12..52a3b82651 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.test.ts @@ -32,6 +32,7 @@ import { DocsBuilder, shouldCheckForUpdate } from '../DocsBuilder'; import { DocsSynchronizer, DocsSynchronizerSyncOpts } from './DocsSynchronizer'; jest.mock('../DocsBuilder'); +jest.useFakeTimers(); jest.mock('node-fetch', () => ({ __esModule: true, @@ -130,6 +131,8 @@ describe('DocsSynchronizer', () => { const logger = MockedDocsBuilder.mock.calls[0][0].logger; + jest.advanceTimersByTime(10001); + logger.info('Some more log'); return true; @@ -144,13 +147,19 @@ describe('DocsSynchronizer', () => { generators, }); - expect(mockResponseHandler.log).toHaveBeenCalledTimes(3); + expect(mockResponseHandler.log).toHaveBeenCalledTimes(4); expect(mockResponseHandler.log).toHaveBeenCalledWith('Some log'); expect(mockResponseHandler.log).toHaveBeenCalledWith('Another log'); expect(mockResponseHandler.log).toHaveBeenCalledWith( expect.stringMatching(/info.*Some more log/), ); + expect(mockResponseHandler.log).toHaveBeenCalledWith( + expect.stringMatching( + /info.*The docs building process is taking a little bit longer to process this entity. Please bear with us/, + ), + ); + expect(mockResponseHandler.finish).toHaveBeenCalledWith({ updated: true, }); diff --git a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts index 647447a612..3d89d1647f 100644 --- a/plugins/techdocs-backend/src/service/DocsSynchronizer.ts +++ b/plugins/techdocs-backend/src/service/DocsSynchronizer.ts @@ -128,7 +128,13 @@ export class DocsSynchronizer { cache: this.cache, }); + const interval = setInterval(() => { + taskLogger.info( + 'The docs building process is taking a little bit longer to process this entity. Please bear with us.', + ); + }, 10000); const updated = await this.buildLimiter(() => docsBuilder.build()); + clearInterval(interval); if (!updated) { finish({ updated: false }); diff --git a/plugins/techdocs-module-addons-contrib/CHANGELOG.md b/plugins/techdocs-module-addons-contrib/CHANGELOG.md index aab943c78f..22630158f1 100644 --- a/plugins/techdocs-module-addons-contrib/CHANGELOG.md +++ b/plugins/techdocs-module-addons-contrib/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-techdocs-module-addons-contrib +## 1.0.10-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + ## 1.0.10-next.1 ### Patch Changes diff --git a/plugins/techdocs-module-addons-contrib/api-report.md b/plugins/techdocs-module-addons-contrib/api-report.md index 68fad9121d..c242bb02f2 100644 --- a/plugins/techdocs-module-addons-contrib/api-report.md +++ b/plugins/techdocs-module-addons-contrib/api-report.md @@ -10,6 +10,9 @@ import { BackstagePlugin } from '@backstage/core-plugin-api'; // @public export const ExpandableNavigation: () => JSX.Element | null; +// @public +export const LightBox: () => JSX.Element | null; + // @public export const ReportIssue: (props: ReportIssueProps) => JSX.Element | null; diff --git a/plugins/techdocs-module-addons-contrib/package.json b/plugins/techdocs-module-addons-contrib/package.json index 1b92e15c16..c077ccdd6c 100644 --- a/plugins/techdocs-module-addons-contrib/package.json +++ b/plugins/techdocs-module-addons-contrib/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-module-addons-contrib", "description": "Plugin module for contributed TechDocs Addons", - "version": "1.0.10-next.1", + "version": "1.0.10-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -44,6 +44,7 @@ "@material-ui/lab": "4.0.0-alpha.57", "@react-hookz/web": "^20.0.0", "git-url-parse": "^13.0.0", + "photoswipe": "^5.3.5", "react-use": "^17.2.4" }, "peerDependencies": { diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.test.tsx b/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.test.tsx new file mode 100644 index 0000000000..b63aed8777 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.test.tsx @@ -0,0 +1,50 @@ +/* + * Copyright 2022 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 { TechDocsAddonTester } from '@backstage/plugin-techdocs-addons-test-utils'; + +import React from 'react'; + +import { LightBox } from '../plugin'; + +describe('LightBox', () => { + it('renders without exploding', async () => { + const { getByText } = await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom(TEST_CONTENT) + .renderWithEffects(); + + expect(getByText('TEST_CONTENT')).toBeInTheDocument(); + }); + + it('Add onclick event to images', async () => { + const { getByTestId } = await TechDocsAddonTester.buildAddonsInTechDocs([ + , + ]) + .withDom( + dog, + ) + .renderWithEffects(); + + expect(getByTestId('fixture').onclick).not.toBeUndefined(); + expect(getByTestId('fixture').onclick).toEqual(expect.any(Function)); + }); +}); diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.tsx b/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.tsx new file mode 100644 index 0000000000..b74c2f9659 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/LightBox.tsx @@ -0,0 +1,71 @@ +/* + * Copyright 2022 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 { useEffect } from 'react'; +import { useShadowRootElements } from '@backstage/plugin-techdocs-react'; +// @ts-ignore +import PhotoSwipeLightbox, { DataSource } from 'photoswipe/lightbox'; +import PhotoSwipe from 'photoswipe'; +import 'photoswipe/style.css'; +import './lightbox.css'; + +export const LightBoxAddon = () => { + const images = useShadowRootElements(['img']); + + useEffect(() => { + let dataSourceImages: DataSource | null = null; + + let lightbox = new PhotoSwipeLightbox({ + pswpModule: PhotoSwipe, + wheelToZoom: true, + arrowPrevSVG: + '', + arrowNextSVG: + '', + closeSVG: + '', + zoomSVG: + '', + }); + + images.forEach((image, index) => { + image.onclick = () => { + if (dataSourceImages === null) { + dataSourceImages = images.map(dataSourceImage => { + return { + element: dataSourceImage, + src: dataSourceImage.src, + msrc: dataSourceImage.src, + alt: dataSourceImage.alt, + width: dataSourceImage.clientWidth, + height: dataSourceImage.clientHeight, + }; + }); + } + lightbox.loadAndOpen(index, dataSourceImages); + return false; + }; + }); + lightbox.init(); + + return () => { + lightbox.destroy(); + lightbox = null; + }; + }, [images]); + + return null; +}; diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/index.ts b/plugins/techdocs-module-addons-contrib/src/LigthBox/index.ts new file mode 100644 index 0000000000..144359abb7 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2022 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 './LightBox'; diff --git a/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css b/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css new file mode 100644 index 0000000000..63170792f1 --- /dev/null +++ b/plugins/techdocs-module-addons-contrib/src/LigthBox/lightbox.css @@ -0,0 +1,7 @@ +.pswp { + --pswp-bg: #f8f8f8; +} +.pswp__counter { + color: rgba(0, 0, 0, 0.87); + text-shadow: none; +} diff --git a/plugins/techdocs-module-addons-contrib/src/index.ts b/plugins/techdocs-module-addons-contrib/src/index.ts index 528afe5c0d..bad7701486 100644 --- a/plugins/techdocs-module-addons-contrib/src/index.ts +++ b/plugins/techdocs-module-addons-contrib/src/index.ts @@ -25,6 +25,7 @@ export { ExpandableNavigation, ReportIssue, TextSize, + LightBox, } from './plugin'; export type { ReportIssueProps, diff --git a/plugins/techdocs-module-addons-contrib/src/plugin.ts b/plugins/techdocs-module-addons-contrib/src/plugin.ts index 6136f176d0..c4023a72af 100644 --- a/plugins/techdocs-module-addons-contrib/src/plugin.ts +++ b/plugins/techdocs-module-addons-contrib/src/plugin.ts @@ -22,6 +22,7 @@ import { import { ExpandableNavigationAddon } from './ExpandableNavigation'; import { ReportIssueAddon, ReportIssueProps } from './ReportIssue'; import { TextSizeAddon } from './TextSize'; +import { LightBoxAddon } from './LigthBox'; /** * The TechDocs addons contrib plugin @@ -203,3 +204,49 @@ export const TextSize = techdocsModuleAddonsContribPlugin.provide( component: TextSizeAddon, }), ); + +/** + * This TechDocs addon allows users to open images in a lightbox on documentation pages, they can navigate between images if there are several on one page. + * + * @remarks + * The image size of the lightbox image is the same as the image size on the document page. + * + * @example + * Here's a simple example: + * ``` + * import { + * DefaultTechDocsHome, + * TechDocsIndexPage, + * TechDocsReaderPage, + * } from '@backstage/plugin-techdocs'; + * import { TechDocsAddons } from '@backstage/plugin-techdocs-react'; + * import { LightBox } from '@backstage/plugin-techdocs-module-addons-contrib'; + * + * + * const AppRoutes = () => { + * + * // other plugin routes + * }> + * + * + * } + * > + * + * + * + * + * ; + * }; + * ``` + * + * @public + */ +export const LightBox = techdocsModuleAddonsContribPlugin.provide( + createTechDocsAddonExtension({ + name: 'LightBox', + location: TechDocsAddonLocations.Content, + component: LightBoxAddon, + }), +); diff --git a/plugins/techdocs-node/CHANGELOG.md b/plugins/techdocs-node/CHANGELOG.md index 6547d5db59..a6d32d8d9f 100644 --- a/plugins/techdocs-node/CHANGELOG.md +++ b/plugins/techdocs-node/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-techdocs-node +## 1.4.6-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-aws-node@0.1.1 + - @backstage/plugin-search-common@1.2.1 + ## 1.4.6-next.1 ### Patch Changes diff --git a/plugins/techdocs-node/api-report.md b/plugins/techdocs-node/api-report.md index e27b406478..11ff53514b 100644 --- a/plugins/techdocs-node/api-report.md +++ b/plugins/techdocs-node/api-report.md @@ -54,6 +54,9 @@ export type GeneratorRunOptions = { etag?: string; logger: Logger; logStream?: Writable; + siteOptions?: { + name?: string; + }; }; // @public @@ -85,6 +88,32 @@ export const getLocationForEntity: ( scmIntegration: ScmIntegrationRegistry, ) => ParsedLocationAnnotation; +// @public @deprecated (undocumented) +export const getMkDocsYml: ( + inputDir: string, + siteOptions?: + | { + name?: string | undefined; + } + | undefined, +) => Promise<{ + path: string; + content: string; + configIsTemporary: boolean; +}>; + +// @public +export const getMkdocsYml: ( + inputDir: string, + siteOptions?: { + name?: string; + }, +) => Promise<{ + path: string; + content: string; + configIsTemporary: boolean; +}>; + // @public export type MigrateRequest = { removeOriginal?: boolean; diff --git a/plugins/techdocs-node/package.json b/plugins/techdocs-node/package.json index d9b7b72d91..0a6ffd721b 100644 --- a/plugins/techdocs-node/package.json +++ b/plugins/techdocs-node/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-node", "description": "Common node.js functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "1.4.6-next.1", + "version": "1.4.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "publishConfig": { diff --git a/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_default.yml b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_default.yml new file mode 100644 index 0000000000..460e5c0e1d --- /dev/null +++ b/plugins/techdocs-node/src/stages/generate/__fixtures__/mkdocs_default.yml @@ -0,0 +1,4 @@ +site_name: Default Test site name +docs_dir: docs +plugins: + - techdocs-core diff --git a/plugins/techdocs-node/src/stages/generate/helpers.test.ts b/plugins/techdocs-node/src/stages/generate/helpers.test.ts index e894b0602e..b24d85430b 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.test.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.test.ts @@ -34,7 +34,7 @@ import { import { patchMkdocsYmlPreBuild, pathMkdocsYmlWithTechdocsPlugin, -} from './mkDocsPatchers'; +} from './mkdocsPatchers'; import yaml from 'js-yaml'; const mockEntity = { @@ -42,12 +42,16 @@ const mockEntity = { kind: 'TestKind', metadata: { name: 'testName', + title: 'Test site name', }, }; const mkdocsYml = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs.yml'), ); +const mkdocsDefaultYml = fs.readFileSync( + resolvePath(__filename, '../__fixtures__/mkdocs_default.yml'), +); const mkdocsYmlWithExtensions = fs.readFileSync( resolvePath(__filename, '../__fixtures__/mkdocs_with_extensions.yml'), ); @@ -186,6 +190,7 @@ describe('helpers', () => { beforeEach(() => { mockFs({ '/mkdocs.yml': mkdocsYml, + '/mkdocs_default.yml': mkdocsDefaultYml, '/mkdocs_with_repo_url.yml': mkdocsYmlWithRepoUrl, '/mkdocs_with_edit_uri.yml': mkdocsYmlWithEditUri, '/mkdocs_with_extensions.yml': mkdocsYmlWithExtensions, @@ -514,28 +519,60 @@ describe('helpers', () => { describe('getMkdocsYml', () => { const inputDir = resolvePath(__filename, '../__fixtures__/'); + const siteOptions = { + name: mockEntity.metadata.title, + }; it('returns expected contents when .yml file is present', async () => { const key = path.join(inputDir, 'mkdocs.yml'); mockFs({ [key]: mkdocsYml }); - const { path: mkdocsPath, content } = await getMkdocsYml(inputDir); + const { + path: mkdocsPath, + content, + configIsTemporary, + } = await getMkdocsYml(inputDir, siteOptions); expect(mkdocsPath).toBe(key); expect(content).toBe(mkdocsYml.toString()); + expect(configIsTemporary).toBe(false); }); it('returns expected contents when .yaml file is present', async () => { const key = path.join(inputDir, 'mkdocs.yaml'); mockFs({ [key]: mkdocsYml }); - const { path: mkdocsPath, content } = await getMkdocsYml(inputDir); + const { + path: mkdocsPath, + content, + configIsTemporary, + } = await getMkdocsYml(inputDir, siteOptions); expect(mkdocsPath).toBe(key); expect(content).toBe(mkdocsYml.toString()); + expect(configIsTemporary).toBe(false); }); - it('throws when neither .yml nor .yaml file is present', async () => { + it('returns expected contents when default file is present', async () => { + const defaultSiteOptions = { + name: 'Default Test site name', + }; + const key = path.join(inputDir, 'mkdocs.yml'); + const mockPathExists = jest.spyOn(fs, 'pathExists'); + mockPathExists.mockImplementation(() => Promise.resolve(false)); + mockFs({ [key]: mkdocsDefaultYml }); + const { + path: mkdocsPath, + content, + configIsTemporary, + } = await getMkdocsYml(inputDir, defaultSiteOptions); + + expect(mkdocsPath).toBe(key); + expect(content).toBe(mkdocsDefaultYml.toString()); + expect(configIsTemporary).toBe(true); + }); + + it('throws when neither .yml nor .yaml nor default file is present', async () => { const invalidInputDir = resolvePath(__filename); - await expect(getMkdocsYml(invalidInputDir)).rejects.toThrow( - /Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml for validation/, + await expect(getMkdocsYml(invalidInputDir, siteOptions)).rejects.toThrow( + /Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml or default for validation/, ); }); }); diff --git a/plugins/techdocs-node/src/stages/generate/helpers.ts b/plugins/techdocs-node/src/stages/generate/helpers.ts index 50db0eb86a..e16293f906 100644 --- a/plugins/techdocs-node/src/stages/generate/helpers.ts +++ b/plugins/techdocs-node/src/stages/generate/helpers.ts @@ -26,7 +26,7 @@ import path, { resolve as resolvePath } from 'path'; import { PassThrough, Writable } from 'stream'; import { Logger } from 'winston'; import { ParsedLocationAnnotation } from '../../helpers'; -import { SupportedGeneratorKey } from './types'; +import { DefaultMkdocsContent, SupportedGeneratorKey } from './types'; import { getFileTreeRecursively } from '../publish/helpers'; // TODO: Implement proper support for more generators. @@ -150,36 +150,90 @@ export const MKDOCS_SCHEMA = DEFAULT_SCHEMA.extend([ }), ]); +/** + * Generates a mkdocs.yml configuration file + * + * @param inputDir - base dir to where the mkdocs.yml file will be created + * @param siteOptions - options for the site: `name` property will be used in mkdocs.yml for the + * required `site_name` property, default value is "Documentation Site" + */ +export const generateMkdocsYml = async ( + inputDir: string, + siteOptions?: { name?: string }, +) => { + try { + // TODO(awanlin): Use a provided default mkdocs.yml + // from config or some specified location. If this is + // not provided then fall back to generating bare + // minimum mkdocs.yml file + + const mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); + const defaultSiteName = siteOptions?.name ?? 'Documentation Site'; + const defaultMkdocsContent: DefaultMkdocsContent = { + site_name: defaultSiteName, + docs_dir: 'docs', + plugins: ['techdocs-core'], + }; + + await fs.writeFile( + mkdocsYmlPath, + yaml.dump(defaultMkdocsContent, { schema: MKDOCS_SCHEMA }), + ); + } catch (error) { + throw new ForwardedError('Could not generate mkdocs.yml file', error); + } +}; + /** * Finds and loads the contents of either an mkdocs.yml or mkdocs.yaml file, * depending on which is present (MkDocs supports both as of v1.2.2). + * @public * - * @param inputDir - base dir to be searched for either an mkdocs.yml or - * mkdocs.yaml file. + * @param inputDir - base dir to be searched for either an mkdocs.yml or mkdocs.yaml file. + * @param siteOptions - options for the site: `name` property will be used in mkdocs.yml for the + * required `site_name` property, default value is "Documentation Site" */ export const getMkdocsYml = async ( inputDir: string, -): Promise<{ path: string; content: string }> => { + siteOptions?: { name?: string }, +): Promise<{ path: string; content: string; configIsTemporary: boolean }> => { let mkdocsYmlPath: string; let mkdocsYmlFileString: string; try { mkdocsYmlPath = path.join(inputDir, 'mkdocs.yaml'); - mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); - } catch { - try { - mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); + if (await fs.pathExists(mkdocsYmlPath)) { mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); - } catch (error) { - throw new ForwardedError( - 'Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml for validation', - error, - ); + return { + path: mkdocsYmlPath, + content: mkdocsYmlFileString, + configIsTemporary: false, + }; } + + mkdocsYmlPath = path.join(inputDir, 'mkdocs.yml'); + if (await fs.pathExists(mkdocsYmlPath)) { + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + return { + path: mkdocsYmlPath, + content: mkdocsYmlFileString, + configIsTemporary: false, + }; + } + + // No mkdocs file, generate it + await generateMkdocsYml(inputDir, siteOptions); + mkdocsYmlFileString = await fs.readFile(mkdocsYmlPath, 'utf8'); + } catch (error) { + throw new ForwardedError( + 'Could not read MkDocs YAML config file mkdocs.yml or mkdocs.yaml or default for validation', + error, + ); } return { path: mkdocsYmlPath, content: mkdocsYmlFileString, + configIsTemporary: true, }; }; diff --git a/plugins/techdocs-node/src/stages/generate/index.ts b/plugins/techdocs-node/src/stages/generate/index.ts index 1c20c58887..aaf27026c2 100644 --- a/plugins/techdocs-node/src/stages/generate/index.ts +++ b/plugins/techdocs-node/src/stages/generate/index.ts @@ -15,6 +15,7 @@ */ export { TechdocsGenerator } from './techdocs'; export { Generators } from './generators'; +export { getMkdocsYml } from './helpers'; export type { GeneratorBase, GeneratorOptions, @@ -22,3 +23,10 @@ export type { GeneratorRunOptions, SupportedGeneratorKey, } from './types'; +import { getMkdocsYml } from './helpers'; +/** + * @public + * @deprecated + * Deprecated in favor of getMkdocsYml (lowercase 'd') + */ +export const getMkDocsYml = getMkdocsYml; diff --git a/plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts b/plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts similarity index 100% rename from plugins/techdocs-node/src/stages/generate/mkDocsPatchers.ts rename to plugins/techdocs-node/src/stages/generate/mkdocsPatchers.ts diff --git a/plugins/techdocs-node/src/stages/generate/techdocs.ts b/plugins/techdocs-node/src/stages/generate/techdocs.ts index 9bbc58ebdc..2f88e1a8ad 100644 --- a/plugins/techdocs-node/src/stages/generate/techdocs.ts +++ b/plugins/techdocs-node/src/stages/generate/techdocs.ts @@ -34,7 +34,7 @@ import { import { patchMkdocsYmlPreBuild, pathMkdocsYmlWithTechdocsPlugin, -} from './mkDocsPatchers'; +} from './mkdocsPatchers'; import { GeneratorBase, GeneratorConfig, @@ -96,10 +96,14 @@ export class TechdocsGenerator implements GeneratorBase { etag, logger: childLogger, logStream, + siteOptions, } = options; // Do some updates to mkdocs.yml before generating docs e.g. adding repo_url - const { path: mkdocsYmlPath, content } = await getMkdocsYml(inputDir); + const { path: mkdocsYmlPath, content } = await getMkdocsYml( + inputDir, + siteOptions, + ); // validate the docs_dir first const docsDir = await validateMkdocsYaml(inputDir, content); diff --git a/plugins/techdocs-node/src/stages/generate/types.ts b/plugins/techdocs-node/src/stages/generate/types.ts index 2b2bbb21c9..70c332344a 100644 --- a/plugins/techdocs-node/src/stages/generate/types.ts +++ b/plugins/techdocs-node/src/stages/generate/types.ts @@ -53,6 +53,7 @@ export type GeneratorConfig = { * @param etag - A unique identifier for the prepared tree e.g. commit SHA. If provided it will be stored in techdocs_metadata.json. * @param logger - A logger that forwards the messages to the caller to be displayed outside of the backend. * @param logStream - A log stream that can send raw log messages to the caller to be displayed outside of the backend. + * @param siteOptions - Options for the site: The `name` property will be used in mkdocs.yml config for the required `site_name` property, default value is "Documentation Site" */ export type GeneratorRunOptions = { inputDir: string; @@ -61,6 +62,7 @@ export type GeneratorRunOptions = { etag?: string; logger: Logger; logStream?: Writable; + siteOptions?: { name?: string }; }; /** @@ -89,3 +91,9 @@ export type GeneratorBuilder = { register(protocol: SupportedGeneratorKey, generator: GeneratorBase): void; get(entity: Entity): GeneratorBase; }; + +export type DefaultMkdocsContent = { + site_name: string; + docs_dir: string; + plugins: String[]; +}; diff --git a/plugins/techdocs-react/CHANGELOG.md b/plugins/techdocs-react/CHANGELOG.md index 0403406407..34617eb3f5 100644 --- a/plugins/techdocs-react/CHANGELOG.md +++ b/plugins/techdocs-react/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-techdocs-react +## 1.1.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/version-bridge@1.0.3 + ## 1.1.3-next.1 ### Patch Changes diff --git a/plugins/techdocs-react/package.json b/plugins/techdocs-react/package.json index 01bf2ceec4..29587c3e1e 100644 --- a/plugins/techdocs-react/package.json +++ b/plugins/techdocs-react/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-techdocs-react", "description": "Shared frontend utilities for TechDocs and Addons", - "version": "1.1.3-next.1", + "version": "1.1.3-next.2", "publishConfig": { "access": "public", "alphaTypes": "dist/index.alpha.d.ts", diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 01ad074f1c..352114d77d 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,27 @@ # @backstage/plugin-techdocs +## 1.5.0-next.2 + +### Patch Changes + +- 66e2aab4c4: `ListItem` wrapper component moved to `SearchResultListItemExtension` for all `*SearchResultListItems` that are exported as extensions. This is to make sure the list only contains list elements. + + Note: If you have implemented a custom result list item, we recommend you to remove the list item wrapper to avoid nested `
  • ` elements. + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-search-react@1.5.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/config@1.0.6 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + - @backstage/integration-react@1.1.10-next.1 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + - @backstage/plugin-search-common@1.2.1 + - @backstage/plugin-techdocs-react@1.1.3-next.2 + ## 1.5.0-next.1 ### Minor Changes diff --git a/plugins/techdocs/api-report.md b/plugins/techdocs/api-report.md index ddc90d07bb..9452b38feb 100644 --- a/plugins/techdocs/api-report.md +++ b/plugins/techdocs/api-report.md @@ -19,6 +19,7 @@ import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; +import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; import { TableProps } from '@backstage/core-components'; @@ -400,7 +401,7 @@ export type TechDocsSearchProps = { // @public export const TechDocsSearchResultListItem: ( - props: TechDocsSearchResultListItemProps, + props: SearchResultListItemExtensionProps, ) => JSX.Element | null; // @public diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 788b82cfa5..5e113f3e7f 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": "1.5.0-next.1", + "version": "1.5.0-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 331eefe121..bdd4814898 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -33,7 +33,10 @@ import { fetchApiRef, identityApiRef, } from '@backstage/core-plugin-api'; -import { createSearchResultListItemExtension } from '@backstage/plugin-search-react'; +import { + createSearchResultListItemExtension, + SearchResultListItemExtensionProps, +} from '@backstage/plugin-search-react'; import { TechDocsSearchResultListItemProps } from './search/components/TechDocsSearchResultListItem'; /** @@ -162,7 +165,7 @@ export const TechDocsReaderPage = techdocsPlugin.provide( * @public */ export const TechDocsSearchResultListItem: ( - props: TechDocsSearchResultListItemProps, + props: SearchResultListItemExtensionProps, ) => JSX.Element | null = techdocsPlugin.provide( createSearchResultListItemExtension({ name: 'TechDocsSearchResultListItem', diff --git a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx index ffe95e0b7e..ea7e4ed77b 100644 --- a/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsReaderPage/TechDocsReaderPage.tsx @@ -102,6 +102,7 @@ CONFIGURATION 4: and provided content in + diff --git a/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx index f058fd0a9e..9bf8b76092 100644 --- a/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx +++ b/plugins/techdocs/src/search/components/TechDocsSearchResultListItem.tsx @@ -15,13 +15,7 @@ */ import React, { PropsWithChildren, ReactNode } from 'react'; -import { - Divider, - ListItem, - ListItemIcon, - ListItemText, - makeStyles, -} from '@material-ui/core'; +import { ListItemIcon, ListItemText, makeStyles } from '@material-ui/core'; import Typography from '@material-ui/core/Typography'; import { Link } from '@backstage/core-components'; import { ResultHighlight } from '@backstage/plugin-search-common'; @@ -157,11 +151,8 @@ export const TechDocsSearchResultListItem = ( const ListItemWrapper = ({ children }: PropsWithChildren<{}>) => asListItem ? ( <> - - {icon && {icon}} -
    {children}
    -
    - + {icon && {icon}} +
    {children}
    ) : ( <>{children} diff --git a/plugins/todo-backend/CHANGELOG.md b/plugins/todo-backend/CHANGELOG.md index 5009bb66b1..9f44d33d18 100644 --- a/plugins/todo-backend/CHANGELOG.md +++ b/plugins/todo-backend/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-todo-backend +## 0.1.39-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/catalog-client@1.3.1-next.1 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + - @backstage/integration@1.4.2 + ## 0.1.39-next.1 ### Patch Changes diff --git a/plugins/todo-backend/package.json b/plugins/todo-backend/package.json index b7eae2bf52..6120e76a5e 100644 --- a/plugins/todo-backend/package.json +++ b/plugins/todo-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo-backend", "description": "A Backstage backend plugin that lets you browse TODO comments in your source code", - "version": "0.1.39-next.1", + "version": "0.1.39-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/todo/CHANGELOG.md b/plugins/todo/CHANGELOG.md index c5ef6bddfa..49a8241e30 100644 --- a/plugins/todo/CHANGELOG.md +++ b/plugins/todo/CHANGELOG.md @@ -1,5 +1,17 @@ # @backstage/plugin-todo +## 0.2.17-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.2.17-next.1 ### Patch Changes diff --git a/plugins/todo/package.json b/plugins/todo/package.json index a6ed7e591e..d337834872 100644 --- a/plugins/todo/package.json +++ b/plugins/todo/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-todo", "description": "A Backstage plugin that lets you browse TODO comments in your source code", - "version": "0.2.17-next.1", + "version": "0.2.17-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/user-settings-backend/CHANGELOG.md b/plugins/user-settings-backend/CHANGELOG.md index 25d3b6b935..20a7f0c6e5 100644 --- a/plugins/user-settings-backend/CHANGELOG.md +++ b/plugins/user-settings-backend/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-user-settings-backend +## 0.1.6-next.2 + +### Patch Changes + +- 0ff03319be: Updated usage of `createBackendPlugin`. +- 4a6f38a535: Added a Backend System plugin feature +- Updated dependencies + - @backstage/backend-plugin-api@0.4.0-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/plugin-auth-node@0.2.11-next.2 + - @backstage/errors@1.1.4 + - @backstage/types@1.0.2 + ## 0.1.6-next.1 ### Patch Changes diff --git a/plugins/user-settings-backend/api-report.md b/plugins/user-settings-backend/api-report.md index 1bc37bbec7..b015915723 100644 --- a/plugins/user-settings-backend/api-report.md +++ b/plugins/user-settings-backend/api-report.md @@ -3,6 +3,7 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { BackendFeature } from '@backstage/backend-plugin-api'; import express from 'express'; import { IdentityApi } from '@backstage/plugin-auth-node'; import { PluginDatabaseManager } from '@backstage/backend-common'; @@ -18,5 +19,8 @@ export interface RouterOptions { identity: IdentityApi; } +// @alpha +export const userSettingsPlugin: () => BackendFeature; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/user-settings-backend/package.json b/plugins/user-settings-backend/package.json index 8197078d74..b77050a5af 100644 --- a/plugins/user-settings-backend/package.json +++ b/plugins/user-settings-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings-backend", "description": "The Backstage backend plugin to manage user settings", - "version": "0.1.6-next.1", + "version": "0.1.6-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -11,7 +11,8 @@ "publishConfig": { "access": "public", "main": "dist/index.cjs.js", - "types": "dist/index.d.ts" + "types": "dist/index.d.ts", + "alphaTypes": "dist/index.alpha.d.ts" }, "repository": { "type": "git", @@ -20,7 +21,7 @@ }, "scripts": { "start": "backstage-cli package start", - "build": "backstage-cli package build", + "build": "backstage-cli package build --experimental-type-build", "lint": "backstage-cli package lint", "test": "backstage-cli package test", "prepack": "backstage-cli package prepack", @@ -29,6 +30,7 @@ }, "dependencies": { "@backstage/backend-common": "workspace:^", + "@backstage/backend-plugin-api": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/errors": "workspace:^", "@backstage/plugin-auth-node": "workspace:^", @@ -48,6 +50,7 @@ }, "files": [ "dist", - "migrations" + "migrations", + "alpha" ] } diff --git a/plugins/user-settings-backend/src/index.ts b/plugins/user-settings-backend/src/index.ts index 04d8accdaf..5504aaa564 100644 --- a/plugins/user-settings-backend/src/index.ts +++ b/plugins/user-settings-backend/src/index.ts @@ -16,3 +16,4 @@ export * from './service'; export * from './database'; +export { userSettingsPlugin } from './plugin'; diff --git a/plugins/user-settings-backend/src/plugin.ts b/plugins/user-settings-backend/src/plugin.ts new file mode 100644 index 0000000000..e200d22ffe --- /dev/null +++ b/plugins/user-settings-backend/src/plugin.ts @@ -0,0 +1,42 @@ +/* + * Copyright 2023 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createBackendPlugin, + coreServices, +} from '@backstage/backend-plugin-api'; +import { createRouter } from './service/router'; + +/** + * The user settings backend plugin. + * + * @alpha + */ +export const userSettingsPlugin = createBackendPlugin({ + pluginId: 'userSettings', + register(env) { + env.registerInit({ + deps: { + database: coreServices.database, + identity: coreServices.identity, + httpRouter: coreServices.httpRouter, + }, + async init({ database, identity, httpRouter }) { + httpRouter.use(await createRouter({ database, identity })); + }, + }); + }, +}); diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 51de164e53..478b60b6da 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-user-settings +## 0.6.3-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-app-api@1.4.1-next.0 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/types@1.0.2 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.6.3-next.1 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index ad046124c4..319ce98fee 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-user-settings", "description": "A Backstage plugin that provides a settings page", - "version": "0.6.3-next.1", + "version": "0.6.3-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx index f8e1fb190c..772ec80982 100644 --- a/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx +++ b/plugins/user-settings/src/components/AuthProviders/DefaultProviderSettings.tsx @@ -24,6 +24,7 @@ import { oktaAuthApiRef, microsoftAuthApiRef, bitbucketAuthApiRef, + bitbucketServerAuthApiRef, atlassianAuthApiRef, oneloginAuthApiRef, } from '@backstage/core-plugin-api'; @@ -99,6 +100,14 @@ export const DefaultProviderSettings = (props: { icon={Star} /> )} + {configuredProviders.includes('bitbucketServer') && ( + + )} ); }; diff --git a/plugins/vault-backend/CHANGELOG.md b/plugins/vault-backend/CHANGELOG.md index d81f582d2c..2c7715cd73 100644 --- a/plugins/vault-backend/CHANGELOG.md +++ b/plugins/vault-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-vault-backend +## 0.2.8-next.2 + +### Patch Changes + +- Updated dependencies + - @backstage/backend-test-utils@0.1.34-next.2 + - @backstage/backend-common@0.18.2-next.2 + - @backstage/backend-tasks@0.4.3-next.2 + - @backstage/config@1.0.6 + - @backstage/errors@1.1.4 + ## 0.2.8-next.1 ### Patch Changes diff --git a/plugins/vault-backend/package.json b/plugins/vault-backend/package.json index 58a16853e6..ae7c89a61c 100644 --- a/plugins/vault-backend/package.json +++ b/plugins/vault-backend/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-vault-backend", "description": "A Backstage backend plugin that integrates towards Vault", - "version": "0.2.8-next.1", + "version": "0.2.8-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -35,7 +35,6 @@ "dependencies": { "@backstage/backend-common": "workspace:^", "@backstage/backend-tasks": "workspace:^", - "@backstage/backend-test-utils": "workspace:^", "@backstage/config": "workspace:^", "@backstage/errors": "workspace:^", "@types/express": "*", @@ -50,6 +49,7 @@ "yn": "^5.0.0" }, "devDependencies": { + "@backstage/backend-test-utils": "workspace:^", "@backstage/cli": "workspace:^", "@types/compression": "^1.7.2", "@types/supertest": "^2.0.8", diff --git a/plugins/vault/CHANGELOG.md b/plugins/vault/CHANGELOG.md index 7e661f0a8a..60fe5c4c8b 100644 --- a/plugins/vault/CHANGELOG.md +++ b/plugins/vault/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-vault +## 0.1.9-next.2 + +### Patch Changes + +- fd7a77b9c4: Surface additional context and details to the Backstage UI when the Vault plugin encounters non-successful HTTP responses from the Vault API. +- Updated dependencies + - @backstage/catalog-model@1.2.0-next.1 + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + - @backstage/plugin-catalog-react@1.3.0-next.2 + ## 0.1.9-next.1 ### Patch Changes diff --git a/plugins/vault/package.json b/plugins/vault/package.json index 9d59cba50a..15bae8c3ad 100644 --- a/plugins/vault/package.json +++ b/plugins/vault/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-vault", "description": "A Backstage plugin that integrates towards Vault", - "version": "0.1.9-next.1", + "version": "0.1.9-next.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/plugins/vault/src/api.test.ts b/plugins/vault/src/api.test.ts index 4ce6f95c41..471cadc87e 100644 --- a/plugins/vault/src/api.test.ts +++ b/plugins/vault/src/api.test.ts @@ -50,8 +50,10 @@ describe('api', () => { const { path } = req.params; if (path === 'test/success') { return res(ctx.json(mockSecretsResult)); - } else if (path === 'test/error') { + } else if (path === 'test/empty') { return res(ctx.json({ items: [] })); + } else if (path === 'test/not-found') { + return res(ctx.status(404)); } return res(ctx.status(400)); }), @@ -72,7 +74,7 @@ describe('api', () => { it('should return empty secret list', async () => { setupHandlers(); const api = new VaultClient({ discoveryApi }); - expect(await api.listSecrets('test/error')).toEqual([]); + expect(await api.listSecrets('test/empty')).toEqual([]); }); it('should return all the secrets if no path defined', async () => { @@ -80,4 +82,20 @@ describe('api', () => { const api = new VaultClient({ discoveryApi }); expect(await api.listSecrets('')).toEqual(mockSecretsResult.items); }); + + it('should throw an error if the Vault API responds with an HTTP 404', async () => { + setupHandlers(); + const api = new VaultClient({ discoveryApi }); + await expect(api.listSecrets('test/not-found')).rejects.toThrow( + "No secrets found in path 'v1/secrets/test%2Fnot-found'", + ); + }); + + it('should throw an error if the Vault API responds with a non-successful HTTP status code', async () => { + setupHandlers(); + const api = new VaultClient({ discoveryApi }); + await expect(api.listSecrets('test/error')).rejects.toThrow( + 'Request failed with 400 Error', + ); + }); }); diff --git a/plugins/vault/src/api.ts b/plugins/vault/src/api.ts index bdd881e08c..84843c9176 100644 --- a/plugins/vault/src/api.ts +++ b/plugins/vault/src/api.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { DiscoveryApi, createApiRef } from '@backstage/core-plugin-api'; -import { NotFoundError } from '@backstage/errors'; +import { NotFoundError, ResponseError } from '@backstage/errors'; /** * @public @@ -75,9 +75,7 @@ export class VaultClient implements VaultApi { } else if (response.status === 404) { throw new NotFoundError(`No secrets found in path '${path}'`); } - throw new Error( - `Unexpected error while fetching secrets from path '${path}'`, - ); + throw await ResponseError.fromResponse(response); } async listSecrets(secretPath: string): Promise { diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx index 82a90f23e1..27cc0ed91e 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.test.tsx @@ -33,40 +33,28 @@ describe('EntityVaultTable', () => { let apis: TestApiRegistry; const mockBaseUrl = 'https://api-vault.com/api/vault'; const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - - const entityOk: ComponentEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'test', - description: 'This is the description', - annotations: { - 'vault.io/secrets-path': 'test/success', + const entity = (secretPath: string) => { + return { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + description: 'This is the description', + annotations: { + 'vault.io/secrets-path': secretPath, + }, }, - }, - spec: { - lifecycle: 'production', - owner: 'owner', - type: 'service', - }, + spec: { + lifecycle: 'production', + owner: 'owner', + type: 'service', + }, + } as ComponentEntity; }; - const entityNotOk: ComponentEntity = { - apiVersion: 'backstage.io/v1alpha1', - kind: 'Component', - metadata: { - name: 'test', - description: 'This is the description', - annotations: { - 'vault.io/secrets-path': 'test/error', - }, - }, - spec: { - lifecycle: 'production', - owner: 'owner', - type: 'service', - }, - }; + const entityOk = entity('test/success'); + const entityEmpty = entity('test/empty'); + const entityNotOk = entity('test/error'); const mockSecretsResult: { items: VaultSecret[] } = { items: [ @@ -91,7 +79,7 @@ describe('EntityVaultTable', () => { const { path } = req.params; if (path === 'test/success') { return res(ctx.json(mockSecretsResult)); - } else if (path === 'test/error') { + } else if (path === 'test/empty') { return res(ctx.json([])); } return res(ctx.status(400)); @@ -122,10 +110,25 @@ describe('EntityVaultTable', () => { setupHandlers(); const rendered = await renderInTestApp( - + , ); expect(rendered.getByText(/No secrets found/)).toBeInTheDocument(); }); + + it('should surface an appropriate error when the Vault API responds unsuccessfully', async () => { + setupHandlers(); + const rendered = await renderInTestApp( + + + , + ); + + expect( + rendered.getByText( + /Unexpected error while fetching secrets from path \'test\/error\'\: Request failed with 400 Error/, + ), + ).toBeInTheDocument(); + }); }); diff --git a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx index efc345b07f..9191d54b0b 100644 --- a/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx +++ b/plugins/vault/src/components/EntityVaultTable/EntityVaultTable.tsx @@ -81,7 +81,12 @@ export const EntityVaultTable = ({ entity }: { entity: Entity }) => { }); if (error) { - return {error.message}; + return ( + + Unexpected error while fetching secrets from path '{secretPath}':{' '} + {error.message} + + ); } return ( diff --git a/plugins/xcmetrics/CHANGELOG.md b/plugins/xcmetrics/CHANGELOG.md index e73dbc7e64..af4665e980 100644 --- a/plugins/xcmetrics/CHANGELOG.md +++ b/plugins/xcmetrics/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-xcmetrics +## 0.2.35-next.1 + +### Patch Changes + +- Updated dependencies + - @backstage/core-components@0.12.4-next.1 + - @backstage/core-plugin-api@1.3.0 + - @backstage/errors@1.1.4 + - @backstage/theme@0.2.16 + ## 0.2.35-next.0 ### Patch Changes diff --git a/plugins/xcmetrics/README.md b/plugins/xcmetrics/README.md index a3bb93d2af..7c003722a0 100644 --- a/plugins/xcmetrics/README.md +++ b/plugins/xcmetrics/README.md @@ -7,10 +7,9 @@ With this plugin, you can view data from XCMetrics directly in Backstage. ## Getting started -From `packages/app`: - ```bash -yarn add @backstage/plugin-xcmetrics +# From your Backstage root directory +yarn add --cwd packages/app @backstage/plugin-xcmetrics ``` In `packages/app/src/App.tsx`, add the following: diff --git a/plugins/xcmetrics/package.json b/plugins/xcmetrics/package.json index fc85d7b050..775e884f4b 100644 --- a/plugins/xcmetrics/package.json +++ b/plugins/xcmetrics/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/plugin-xcmetrics", "description": "A Backstage plugin that shows XCode build metrics for your components", - "version": "0.2.35-next.0", + "version": "0.2.35-next.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", diff --git a/scripts/assemble-manifest.js b/scripts/assemble-manifest.js index f68a83447d..f01bc7fd9b 100755 --- a/scripts/assemble-manifest.js +++ b/scripts/assemble-manifest.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2022 The Backstage Authors * diff --git a/scripts/check-docs-quality.js b/scripts/check-docs-quality.js index 10c4b99976..3dcbd6ef81 100755 --- a/scripts/check-docs-quality.js +++ b/scripts/check-docs-quality.js @@ -56,7 +56,7 @@ async function listFiles(dir = '') { // caused by the script. In CI, we want to ensure vale linter is run. async function exitIfMissingVale() { try { - // eslint-disable-next-line import/no-extraneous-dependencies + // eslint-disable-next-line @backstage/no-undeclared-imports await require('command-exists')('vale'); } catch (e) { if (process.env.CI) { diff --git a/scripts/check-if-release.js b/scripts/check-if-release.js index e7dabe14a4..c12b27b0ef 100755 --- a/scripts/check-if-release.js +++ b/scripts/check-if-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/create-github-release.js b/scripts/create-github-release.js index 4b3891c5ce..b64d7a9cb5 100755 --- a/scripts/create-github-release.js +++ b/scripts/create-github-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/create-release-tag.js b/scripts/create-release-tag.js index 28417024fd..311b46d4e5 100755 --- a/scripts/create-release-tag.js +++ b/scripts/create-release-tag.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2020 The Backstage Authors * diff --git a/scripts/list-deprecations.js b/scripts/list-deprecations.js index 8047158138..22aff96d67 100755 --- a/scripts/list-deprecations.js +++ b/scripts/list-deprecations.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const _ = require('lodash'); const fs = require('fs-extra'); diff --git a/scripts/prepare-release.js b/scripts/prepare-release.js index 973f4fad58..4019a71cf3 100755 --- a/scripts/prepare-release.js +++ b/scripts/prepare-release.js @@ -1,5 +1,5 @@ #!/usr/bin/env node -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ /* * Copyright 2022 The Backstage Authors * diff --git a/scripts/snyk-github-issue-sync.ts b/scripts/snyk-github-issue-sync.ts index da64e2ea82..ea7d42a2a4 100755 --- a/scripts/snyk-github-issue-sync.ts +++ b/scripts/snyk-github-issue-sync.ts @@ -14,7 +14,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ import { Octokit } from '@octokit/rest'; import minimist from 'minimist'; // Generated by GitHub workflow .github/workflows/snyk-github-issue-creator diff --git a/scripts/verify-api-reference.js b/scripts/verify-api-reference.js index 84a526604d..091e803c33 100755 --- a/scripts/verify-api-reference.js +++ b/scripts/verify-api-reference.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath } = require('path'); const { promises: fs } = require('fs'); diff --git a/scripts/verify-changesets.js b/scripts/verify-changesets.js index 9e3064eeac..ae3f07840a 100755 --- a/scripts/verify-changesets.js +++ b/scripts/verify-changesets.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath } = require('path'); const fs = require('fs-extra'); diff --git a/scripts/verify-links.js b/scripts/verify-links.js index 05ad387c37..7664cadad5 100755 --- a/scripts/verify-links.js +++ b/scripts/verify-links.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { resolve: resolvePath, join: joinPath, dirname } = require('path'); const fs = require('fs').promises; diff --git a/scripts/verify-lockfile-duplicates.js b/scripts/verify-lockfile-duplicates.js index cc22664219..820fba2ea5 100644 --- a/scripts/verify-lockfile-duplicates.js +++ b/scripts/verify-lockfile-duplicates.js @@ -15,7 +15,7 @@ * limitations under the License. */ -/* eslint-disable import/no-extraneous-dependencies */ +/* eslint-disable @backstage/no-undeclared-imports */ const { execFile: execFileCb } = require('child_process'); const { resolve: resolvePath, dirname: dirnamePath } = require('path'); diff --git a/storybook/yarn.lock b/storybook/yarn.lock index 7c73f5d0cc..f2e0f1c96a 100644 --- a/storybook/yarn.lock +++ b/storybook/yarn.lock @@ -2966,90 +2966,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-darwin-arm64@npm:1.3.30" +"@swc/core-darwin-arm64@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-darwin-arm64@npm:1.3.34" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-darwin-x64@npm:1.3.30" +"@swc/core-darwin-x64@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-darwin-x64@npm:1.3.34" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.30" +"@swc/core-linux-arm-gnueabihf@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.34" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.30" +"@swc/core-linux-arm64-gnu@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.34" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.30" +"@swc/core-linux-arm64-musl@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.34" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.30" +"@swc/core-linux-x64-gnu@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.34" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-x64-musl@npm:1.3.30" +"@swc/core-linux-x64-musl@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-x64-musl@npm:1.3.34" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.30" +"@swc/core-win32-arm64-msvc@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.34" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.30" +"@swc/core-win32-ia32-msvc@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.34" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.30" +"@swc/core-win32-x64-msvc@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.34" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.9": - version: 1.3.30 - resolution: "@swc/core@npm:1.3.30" + version: 1.3.34 + resolution: "@swc/core@npm:1.3.34" dependencies: - "@swc/core-darwin-arm64": 1.3.30 - "@swc/core-darwin-x64": 1.3.30 - "@swc/core-linux-arm-gnueabihf": 1.3.30 - "@swc/core-linux-arm64-gnu": 1.3.30 - "@swc/core-linux-arm64-musl": 1.3.30 - "@swc/core-linux-x64-gnu": 1.3.30 - "@swc/core-linux-x64-musl": 1.3.30 - "@swc/core-win32-arm64-msvc": 1.3.30 - "@swc/core-win32-ia32-msvc": 1.3.30 - "@swc/core-win32-x64-msvc": 1.3.30 + "@swc/core-darwin-arm64": 1.3.34 + "@swc/core-darwin-x64": 1.3.34 + "@swc/core-linux-arm-gnueabihf": 1.3.34 + "@swc/core-linux-arm64-gnu": 1.3.34 + "@swc/core-linux-arm64-musl": 1.3.34 + "@swc/core-linux-x64-gnu": 1.3.34 + "@swc/core-linux-x64-musl": 1.3.34 + "@swc/core-win32-arm64-msvc": 1.3.34 + "@swc/core-win32-ia32-msvc": 1.3.34 + "@swc/core-win32-x64-msvc": 1.3.34 dependenciesMeta: "@swc/core-darwin-arm64": optional: true @@ -3071,7 +3071,7 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - checksum: 157a8ae43ebc73fa28d53e913fd7fc879289d5479ba5ea0c5e9abad0b379ca7e292d45f4f07bfd3ec0ff1998b55f300737724dfe463f1f7b361661e083db950c + checksum: a77c286b6b34d40f634ae882aac66f882cf806c165453878c31311070c46f020c607925bd454f64959504f08852b204cc37feba93fc6a3c1a64b295a9b5007d6 languageName: node linkType: hard @@ -6783,9 +6783,9 @@ __metadata: linkType: hard "http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 languageName: node linkType: hard diff --git a/yarn.lock b/yarn.lock index 7adadefb11..ac66cab7ca 100644 --- a/yarn.lock +++ b/yarn.lock @@ -217,14 +217,16 @@ __metadata: languageName: node linkType: hard -"@asyncapi/avro-schema-parser@npm:^0.3.0": - version: 0.3.0 - resolution: "@asyncapi/avro-schema-parser@npm:0.3.0" - checksum: 3086bc16a634600fe4d7c83f52f9558684c0d79193958fcb9275cacfb550700eaa718aa9ed1d3c89d1d77253f67e0f33ea91e137ef08aa722d67ded9373bf142 +"@asyncapi/avro-schema-parser@npm:^1.1.0": + version: 1.1.0 + resolution: "@asyncapi/avro-schema-parser@npm:1.1.0" + dependencies: + avsc: ^5.7.3 + checksum: 31bb6285a49f255e809088164ac4638103b2ec3e761c9c85be116af8abd9fe429626ba79ba6b3899d3823a77216741d9d793841e600145f2956328f3ce45a0e0 languageName: node linkType: hard -"@asyncapi/openapi-schema-parser@npm:^2.0.0": +"@asyncapi/openapi-schema-parser@npm:^2.0.1": version: 2.0.1 resolution: "@asyncapi/openapi-schema-parser@npm:2.0.1" dependencies: @@ -233,12 +235,12 @@ __metadata: languageName: node linkType: hard -"@asyncapi/parser@npm:^1.17.0": - version: 1.17.0 - resolution: "@asyncapi/parser@npm:1.17.0" +"@asyncapi/parser@npm:^1.18.0": + version: 1.18.0 + resolution: "@asyncapi/parser@npm:1.18.0" dependencies: "@apidevtools/json-schema-ref-parser": ^9.0.6 - "@asyncapi/specs": ^3.2.0 + "@asyncapi/specs": ^4.1.0 "@fmvilas/pseudo-yaml-ast": ^0.3.1 ajv: ^6.10.1 js-yaml: ^3.13.1 @@ -246,34 +248,35 @@ __metadata: lodash.clonedeep: ^4.5.0 node-fetch: ^2.6.0 tiny-merge-patch: ^0.1.2 - checksum: 6ac0bd3c9c90abf331cfdfb054dab047cf0324ee803633c2abc6dad96b12c64d033e4fb94907906adfb521ba16e9b23083b2a6acc7719ccd22154a7867046a95 + checksum: 7e08cae52990ce97299170faf99aaf27a8e2e02e6b03df28518d256f5687f394c26fd0a4aba235514ed4e3c2fd081a79321a489c20987ed22122afdb0b9ea0dd languageName: node linkType: hard -"@asyncapi/react-component@npm:1.0.0-next.44": - version: 1.0.0-next.44 - resolution: "@asyncapi/react-component@npm:1.0.0-next.44" +"@asyncapi/react-component@npm:1.0.0-next.47": + version: 1.0.0-next.47 + resolution: "@asyncapi/react-component@npm:1.0.0-next.47" dependencies: - "@asyncapi/avro-schema-parser": ^0.3.0 - "@asyncapi/openapi-schema-parser": ^2.0.0 - "@asyncapi/parser": ^1.17.0 + "@asyncapi/avro-schema-parser": ^1.1.0 + "@asyncapi/openapi-schema-parser": ^2.0.1 + "@asyncapi/parser": ^1.18.0 highlight.js: ^10.7.2 isomorphic-dompurify: ^0.13.0 marked: ^4.0.14 openapi-sampler: ^1.2.1 - react-icons: ^4.4.0 use-resize-observer: ^8.0.0 peerDependencies: react: ">=16.8.0" react-dom: ">=16.8.0" - checksum: 1d96db3bc6a4655884969a7639467836376c028df1a00f8930adf4e1b6ecb43e6a2d61df5c68e63d6c105d319fecb3f2ac0e4146274a81a874fbde17f4dbac1c + checksum: 837b6378f1188945a0e92d93efe71691a8dad120fad5624b58b325ec86c51435b874464ec0c47308ffacda9ee4d77ad8217881584bd74081f6db2211f09cc60c languageName: node linkType: hard -"@asyncapi/specs@npm:^3.2.0": - version: 3.2.0 - resolution: "@asyncapi/specs@npm:3.2.0" - checksum: 09971262aefc8844ab3e7c0c3652711862ac562dd5d614f23b496185690430a81df8e50eddba657f4141e0fd9548ef622fe6c20f4e3dec8054be23f774798335 +"@asyncapi/specs@npm:^4.1.0": + version: 4.1.0 + resolution: "@asyncapi/specs@npm:4.1.0" + dependencies: + "@types/json-schema": ^7.0.11 + checksum: 6e95f3c1ef7267480cdfc69f5a015f63b9101874289e31843a629346a3ea07490e3043b296a089bf3458e877c664d83d4b4738dcb53d37d7e13b75c7bc08c879 languageName: node linkType: hard @@ -3295,12 +3298,12 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": - version: 7.20.7 - resolution: "@babel/runtime@npm:7.20.7" +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.1, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.14.6, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.18.6, @babel/runtime@npm:^7.2.0, @babel/runtime@npm:^7.20.1, @babel/runtime@npm:^7.20.7, @babel/runtime@npm:^7.3.1, @babel/runtime@npm:^7.4.4, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.0, @babel/runtime@npm:^7.7.6, @babel/runtime@npm:^7.8.3, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.8.7, @babel/runtime@npm:^7.9.2": + version: 7.20.13 + resolution: "@babel/runtime@npm:7.20.13" dependencies: regenerator-runtime: ^0.13.11 - checksum: 4629ce5c46f06cca9cfb9b7fc00d48003335a809888e2b91ec2069a2dcfbfef738480cff32ba81e0b7c290f8918e5c22ddcf2b710001464ee84ba62c7e32a3a3 + checksum: 09b7a97a05c80540db6c9e4ddf8c5d2ebb06cae5caf3a87e33c33f27f8c4d49d9c67a2d72f1570e796045288fad569f98a26ceba0c4f5fad2af84b6ad855c4fb languageName: node linkType: hard @@ -3420,6 +3423,7 @@ __metadata: resolution: "@backstage/backend-common@workspace:packages/backend-common" dependencies: "@backstage/backend-app-api": "workspace:^" + "@backstage/backend-dev-utils": "workspace:^" "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" @@ -3516,6 +3520,14 @@ __metadata: languageName: unknown linkType: soft +"@backstage/backend-dev-utils@workspace:^, @backstage/backend-dev-utils@workspace:packages/backend-dev-utils": + version: 0.0.0-use.local + resolution: "@backstage/backend-dev-utils@workspace:packages/backend-dev-utils" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + "@backstage/backend-plugin-api@workspace:^, @backstage/backend-plugin-api@workspace:packages/backend-plugin-api": version: 0.0.0-use.local resolution: "@backstage/backend-plugin-api@workspace:packages/backend-plugin-api" @@ -3657,10 +3669,12 @@ __metadata: "@backstage/core-plugin-api": "workspace:^" "@backstage/dev-utils": "workspace:^" "@backstage/errors": "workspace:^" + "@backstage/eslint-plugin": "workspace:^" "@backstage/release-manifests": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/theme": "workspace:^" "@backstage/types": "workspace:^" + "@esbuild-kit/cjs-loader": ^2.4.1 "@manypkg/get-packages": ^1.1.3 "@octokit/request": ^6.0.0 "@pmmmwh/react-refresh-webpack-plugin": ^0.5.7 @@ -3679,6 +3693,7 @@ __metadata: "@swc/core": ^1.3.9 "@swc/helpers": ^0.4.7 "@swc/jest": ^0.2.22 + "@types/cross-spawn": ^6.0.2 "@types/diff": ^5.0.0 "@types/express": ^4.17.6 "@types/fs-extra": ^9.0.1 @@ -3706,6 +3721,7 @@ __metadata: chalk: ^4.0.0 chokidar: ^3.3.1 commander: ^9.1.0 + cross-spawn: ^7.0.3 css-loader: ^6.5.1 del: ^6.0.0 diff: ^5.0.0 @@ -3718,7 +3734,6 @@ __metadata: eslint-plugin-import: ^2.25.4 eslint-plugin-jest: ^27.0.0 eslint-plugin-jsx-a11y: ^6.5.1 - eslint-plugin-monorepo: ^0.3.2 eslint-plugin-react: ^7.28.0 eslint-plugin-react-hooks: ^4.3.0 eslint-webpack-plugin: ^3.1.1 @@ -4092,6 +4107,17 @@ __metadata: languageName: unknown linkType: soft +"@backstage/eslint-plugin@workspace:^, @backstage/eslint-plugin@workspace:packages/eslint-plugin": + version: 0.0.0-use.local + resolution: "@backstage/eslint-plugin@workspace:packages/eslint-plugin" + dependencies: + "@backstage/cli": "workspace:^" + "@manypkg/get-packages": ^1.1.3 + eslint: ^8.33.0 + minimatch: ^5.1.2 + languageName: unknown + linkType: soft + "@backstage/integration-aws-node@workspace:^, @backstage/integration-aws-node@workspace:packages/integration-aws-node": version: 0.0.0-use.local resolution: "@backstage/integration-aws-node@workspace:packages/integration-aws-node" @@ -4247,6 +4273,7 @@ __metadata: react-use: ^17.2.4 remark-gfm: ^3.0.1 peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 react: ^16.13.1 || ^17.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 languageName: unknown @@ -4401,7 +4428,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-api-docs@workspace:plugins/api-docs" dependencies: - "@asyncapi/react-component": 1.0.0-next.44 + "@asyncapi/react-component": 1.0.0-next.47 "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" @@ -4758,6 +4785,7 @@ __metadata: resolution: "@backstage/plugin-bazaar-backend@workspace:plugins/bazaar-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -5816,6 +5844,110 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-entity-feedback-backend@workspace:^, @backstage/plugin-entity-feedback-backend@workspace:plugins/entity-feedback-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-entity-feedback-backend@workspace:plugins/entity-feedback-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-test-utils": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-entity-feedback-common": "workspace:^" + "@types/express": "*" + "@types/supertest": ^2.0.12 + express: ^4.18.1 + express-promise-router: ^4.1.0 + knex: ^2.0.0 + msw: ^0.49.0 + node-fetch: ^2.6.7 + supertest: ^6.2.4 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-entity-feedback-common@workspace:^, @backstage/plugin-entity-feedback-common@workspace:plugins/entity-feedback-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-entity-feedback-common@workspace:plugins/entity-feedback-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/plugin-entity-feedback@workspace:^, @backstage/plugin-entity-feedback@workspace:plugins/entity-feedback": + version: 0.0.0-use.local + resolution: "@backstage/plugin-entity-feedback@workspace:plugins/entity-feedback" + dependencies: + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-entity-feedback-common": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/react-hooks": ^8.0.1 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + msw: ^0.49.0 + react-use: ^17.2.4 + peerDependencies: + "@types/react": ^16.13.1 || ^17.0.0 + react: ^16.13.1 || ^17.0.0 + react-router-dom: 6.0.0-beta.0 || ^6.3.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-entity-validation@workspace:plugins/entity-validation": + version: 0.0.0-use.local + resolution: "@backstage/plugin-entity-validation@workspace:plugins/entity-validation" + dependencies: + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-common": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@codemirror/language": ^6.0.0 + "@codemirror/legacy-modes": ^6.1.0 + "@codemirror/view": ^6.0.0 + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@react-hookz/web": ^20.0.0 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + "@uiw/react-codemirror": ^4.9.3 + cross-fetch: ^3.1.5 + lodash: ^4.17.21 + msw: ^0.49.0 + react-use: ^17.2.4 + yaml: ^2.0.0 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs": version: 0.0.0-use.local resolution: "@backstage/plugin-events-backend-module-aws-sqs@workspace:plugins/events-backend-module-aws-sqs" @@ -6439,6 +6571,33 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-graphql-voyager@workspace:plugins/graphql-voyager": + version: 0.0.0-use.local + resolution: "@backstage/plugin-graphql-voyager@workspace:plugins/graphql-voyager" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": 4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + graphql-voyager: ^1.0.0-rc.31 + msw: ^0.49.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-home@npm:^0.4.30": version: 0.4.30 resolution: "@backstage/plugin-home@npm:0.4.30" @@ -6602,6 +6761,7 @@ __metadata: resolution: "@backstage/plugin-kafka-backend@workspace:plugins/kafka-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -6801,6 +6961,110 @@ __metadata: languageName: unknown linkType: soft +"@backstage/plugin-linguist-backend@workspace:^, @backstage/plugin-linguist-backend@workspace:plugins/linguist-backend": + version: 0.0.0-use.local + resolution: "@backstage/plugin-linguist-backend@workspace:plugins/linguist-backend" + dependencies: + "@backstage/backend-common": "workspace:^" + "@backstage/backend-tasks": "workspace:^" + "@backstage/catalog-client": "workspace:^" + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/config": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-auth-node": "workspace:^" + "@backstage/plugin-linguist-common": "workspace:^" + "@backstage/types": "workspace:^" + "@types/express": "*" + "@types/supertest": ^2.0.8 + express: ^4.18.1 + express-promise-router: ^4.1.0 + fs-extra: ^10.0.0 + knex: ^2.0.0 + linguist-js: ^2.5.3 + luxon: ^2.0.2 + msw: ^0.49.0 + node-fetch: ^2.6.7 + supertest: ^6.2.4 + uuid: ^8.3.2 + winston: ^3.2.1 + yn: ^4.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-linguist-common@workspace:^, @backstage/plugin-linguist-common@workspace:plugins/linguist-common": + version: 0.0.0-use.local + resolution: "@backstage/plugin-linguist-common@workspace:plugins/linguist-common" + dependencies: + "@backstage/cli": "workspace:^" + languageName: unknown + linkType: soft + +"@backstage/plugin-linguist@workspace:^, @backstage/plugin-linguist@workspace:plugins/linguist": + version: 0.0.0-use.local + resolution: "@backstage/plugin-linguist@workspace:plugins/linguist" + dependencies: + "@backstage/catalog-model": "workspace:^" + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/plugin-catalog-react": "workspace:^" + "@backstage/plugin-linguist-common": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.9.13 + "@material-ui/icons": ^4.9.1 + "@material-ui/lab": ^4.0.0-alpha.57 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": "*" + cross-fetch: ^3.1.5 + luxon: ^2.0.2 + msw: ^0.49.0 + react-use: ^17.2.4 + slugify: ^1.6.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + +"@backstage/plugin-microsoft-calendar@workspace:^, @backstage/plugin-microsoft-calendar@workspace:plugins/microsoft-calendar": + version: 0.0.0-use.local + resolution: "@backstage/plugin-microsoft-calendar@workspace:plugins/microsoft-calendar" + dependencies: + "@backstage/cli": "workspace:^" + "@backstage/core-app-api": "workspace:^" + "@backstage/core-components": "workspace:^" + "@backstage/core-plugin-api": "workspace:^" + "@backstage/dev-utils": "workspace:^" + "@backstage/errors": "workspace:^" + "@backstage/test-utils": "workspace:^" + "@backstage/theme": "workspace:^" + "@material-ui/core": ^4.12.2 + "@material-ui/icons": ^4.9.1 + "@microsoft/microsoft-graph-types": ^2.25.0 + "@tanstack/react-query": ^4.1.3 + "@testing-library/jest-dom": ^5.10.1 + "@testing-library/react": ^12.1.3 + "@testing-library/user-event": ^14.0.0 + "@types/node": ^16.11.26 + classnames: ^2.3.1 + cross-fetch: ^3.1.5 + dompurify: ^2.3.6 + lodash: ^4.17.21 + luxon: ^3.0.0 + material-ui-popup-state: ^1.9.3 + msw: ^0.49.0 + react-use: ^17.2.4 + peerDependencies: + react: ^16.13.1 || ^17.0.0 + languageName: unknown + linkType: soft + "@backstage/plugin-newrelic-dashboard@workspace:^, @backstage/plugin-newrelic-dashboard@workspace:plugins/newrelic-dashboard": version: 0.0.0-use.local resolution: "@backstage/plugin-newrelic-dashboard@workspace:plugins/newrelic-dashboard" @@ -6950,6 +7214,7 @@ __metadata: resolution: "@backstage/plugin-periskop-backend@workspace:plugins/periskop-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@types/express": "*" @@ -7165,6 +7430,7 @@ __metadata: resolution: "@backstage/plugin-proxy-backend@workspace:plugins/proxy-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@types/express": ^4.17.6 @@ -7432,11 +7698,11 @@ __metadata: "@material-ui/icons": ^4.9.1 "@material-ui/lab": 4.0.0-alpha.57 "@rjsf/core": ^3.2.1 - "@rjsf/core-v5": "npm:@rjsf/core@5.0.1" + "@rjsf/core-v5": "npm:@rjsf/core@5.1.0" "@rjsf/material-ui": ^3.2.1 - "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.0.1" - "@rjsf/utils": 5.0.1 - "@rjsf/validator-ajv8": 5.0.1 + "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.1.0" + "@rjsf/utils": 5.1.0 + "@rjsf/validator-ajv8": 5.1.0 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 @@ -7491,7 +7757,8 @@ __metadata: "@react-hookz/web": ^20.0.0 "@rjsf/core": ^3.2.1 "@rjsf/material-ui": ^3.2.1 - "@rjsf/utils": 5.0.1 + "@rjsf/utils": 5.1.0 + "@rjsf/validator-ajv8": 5.1.0 "@testing-library/jest-dom": ^5.10.1 "@testing-library/react": ^12.1.3 "@testing-library/react-hooks": ^8.0.0 @@ -8190,6 +8457,7 @@ __metadata: cross-fetch: ^3.1.5 git-url-parse: ^13.0.0 msw: ^0.49.0 + photoswipe: ^5.3.5 react-use: ^17.2.4 peerDependencies: react: ^16.13.1 || ^17.0.0 @@ -8375,6 +8643,7 @@ __metadata: resolution: "@backstage/plugin-user-settings-backend@workspace:plugins/user-settings-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/backend-test-utils": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" @@ -9152,9 +9421,29 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/android-arm64@npm:0.17.5" +"@esbuild-kit/cjs-loader@npm:^2.4.1": + version: 2.4.2 + resolution: "@esbuild-kit/cjs-loader@npm:2.4.2" + dependencies: + "@esbuild-kit/core-utils": ^3.0.0 + get-tsconfig: ^4.4.0 + checksum: e346e339bfc7eff5c52c270fd0ec06a7f2341b624adfb69f84b7d83f119c35070420906f2761a0b4604e0a0ec90e35eaf12544585476c428ed6d6ee3b250c0fe + languageName: node + linkType: hard + +"@esbuild-kit/core-utils@npm:^3.0.0": + version: 3.0.0 + resolution: "@esbuild-kit/core-utils@npm:3.0.0" + dependencies: + esbuild: ~0.15.10 + source-map-support: ^0.5.21 + checksum: 0e89ec718e2211bf95c48a8085aaef88e8e416f42abd1c62d488d5458eecd3fbc144179a0c5570ad36fa7e2d3bbc411f8d3fb28802c37ced2154dc2c6ded9dfe + languageName: node + linkType: hard + +"@esbuild/android-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/android-arm64@npm:0.17.6" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -9166,65 +9455,65 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/android-arm@npm:0.17.5" +"@esbuild/android-arm@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/android-arm@npm:0.17.6" conditions: os=android & cpu=arm languageName: node linkType: hard -"@esbuild/android-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/android-x64@npm:0.17.5" +"@esbuild/android-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/android-x64@npm:0.17.6" conditions: os=android & cpu=x64 languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/darwin-arm64@npm:0.17.5" +"@esbuild/darwin-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/darwin-arm64@npm:0.17.6" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/darwin-x64@npm:0.17.5" +"@esbuild/darwin-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/darwin-x64@npm:0.17.6" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/freebsd-arm64@npm:0.17.5" +"@esbuild/freebsd-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/freebsd-arm64@npm:0.17.6" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/freebsd-x64@npm:0.17.5" +"@esbuild/freebsd-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/freebsd-x64@npm:0.17.6" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-arm64@npm:0.17.5" +"@esbuild/linux-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-arm64@npm:0.17.6" conditions: os=linux & cpu=arm64 languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-arm@npm:0.17.5" +"@esbuild/linux-arm@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-arm@npm:0.17.6" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-ia32@npm:0.17.5" +"@esbuild/linux-ia32@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-ia32@npm:0.17.6" conditions: os=linux & cpu=ia32 languageName: node linkType: hard @@ -9236,86 +9525,86 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-loong64@npm:0.17.5" +"@esbuild/linux-loong64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-loong64@npm:0.17.6" conditions: os=linux & cpu=loong64 languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-mips64el@npm:0.17.5" +"@esbuild/linux-mips64el@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-mips64el@npm:0.17.6" conditions: os=linux & cpu=mips64el languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-ppc64@npm:0.17.5" +"@esbuild/linux-ppc64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-ppc64@npm:0.17.6" conditions: os=linux & cpu=ppc64 languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-riscv64@npm:0.17.5" +"@esbuild/linux-riscv64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-riscv64@npm:0.17.6" conditions: os=linux & cpu=riscv64 languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-s390x@npm:0.17.5" +"@esbuild/linux-s390x@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-s390x@npm:0.17.6" conditions: os=linux & cpu=s390x languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/linux-x64@npm:0.17.5" +"@esbuild/linux-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/linux-x64@npm:0.17.6" conditions: os=linux & cpu=x64 languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/netbsd-x64@npm:0.17.5" +"@esbuild/netbsd-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/netbsd-x64@npm:0.17.6" conditions: os=netbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/openbsd-x64@npm:0.17.5" +"@esbuild/openbsd-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/openbsd-x64@npm:0.17.6" conditions: os=openbsd & cpu=x64 languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/sunos-x64@npm:0.17.5" +"@esbuild/sunos-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/sunos-x64@npm:0.17.6" conditions: os=sunos & cpu=x64 languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/win32-arm64@npm:0.17.5" +"@esbuild/win32-arm64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/win32-arm64@npm:0.17.6" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/win32-ia32@npm:0.17.5" +"@esbuild/win32-ia32@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/win32-ia32@npm:0.17.6" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.17.5": - version: 0.17.5 - resolution: "@esbuild/win32-x64@npm:0.17.5" +"@esbuild/win32-x64@npm:0.17.6": + version: 0.17.6 + resolution: "@esbuild/win32-x64@npm:0.17.6" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -9337,6 +9626,56 @@ __metadata: languageName: node linkType: hard +"@f/animate@npm:^1.0.1": + version: 1.0.1 + resolution: "@f/animate@npm:1.0.1" + dependencies: + "@f/elapsed-time": ^1.0.0 + "@f/raf": ^1.0.0 + "@f/tween": ^1.0.0 + checksum: 660b44d297b1a6e5e28c48438633e00ded40f8e51adb4bfb26fc809b60c8c056f567f2e61f4f842c8fd2cf8a85dffaa1c2502bd0963eb926135f2d66d9f997db + languageName: node + linkType: hard + +"@f/elapsed-time@npm:^1.0.0": + version: 1.0.0 + resolution: "@f/elapsed-time@npm:1.0.0" + dependencies: + "@f/timestamp": ^1.0.0 + checksum: 5934d24a667bae4e0089408aa362c0ee1503945adf73ca0ff0d09fed33d62d070f0323eb9a54f42c37bd0545330a0f63da13319f66cc89fef59b8917a3278b5f + languageName: node + linkType: hard + +"@f/map-obj@npm:^1.2.2": + version: 1.2.2 + resolution: "@f/map-obj@npm:1.2.2" + checksum: d67db6074e1daa7a3c0f8c2e54094bff48b54d11817fddc886631dd8a17586d0040e7116282a374cc1d3d643d05862d49a1062ea5b2d03b9069507c09ae888a4 + languageName: node + linkType: hard + +"@f/raf@npm:^1.0.0": + version: 1.0.3 + resolution: "@f/raf@npm:1.0.3" + checksum: 800344caf4c649c0bcdefd4febbf3f0eed96bb9b7e54b5bc6eebd7d781c9e6d98ba22fb39da41474080d97ac21916bd1e82051558b43dabcbbf410d2dd0899c1 + languageName: node + linkType: hard + +"@f/timestamp@npm:^1.0.0": + version: 1.0.0 + resolution: "@f/timestamp@npm:1.0.0" + checksum: 8c37acb40cf67952309a214be2ddfd5c4f6634a414efc47406e8d1c1624e74662f584f6b48f3382c742f256aea1537e730e74c6078e52be3a28d0624cc7306a0 + languageName: node + linkType: hard + +"@f/tween@npm:^1.0.0": + version: 1.0.1 + resolution: "@f/tween@npm:1.0.1" + dependencies: + "@f/map-obj": ^1.2.2 + checksum: e37155c43875becc9eca8425b8dc83e64a78fa327738b33fcb3946418369568aa77007ade39591991379187381da829702e5cb84729dd2a0bc3bb4b7c94e4f2b + languageName: node + linkType: hard + "@fmvilas/pseudo-yaml-ast@npm:^0.3.1": version: 0.3.1 resolution: "@fmvilas/pseudo-yaml-ast@npm:0.3.1" @@ -9346,6 +9685,30 @@ __metadata: languageName: node linkType: hard +"@frsource/base64@npm:1.0.5": + version: 1.0.5 + resolution: "@frsource/base64@npm:1.0.5" + checksum: ba526dd81df4e91b0abbcc34fc6818c8ff132e5230c23a4448a365ba934f782a86abb2a8566050f9450905851e467a7160f01f3d321fbb0fa3c8c516c5a82b9b + languageName: node + linkType: hard + +"@frsource/cypress-plugin-visual-regression-diff@npm:^3.2.8": + version: 3.2.8 + resolution: "@frsource/cypress-plugin-visual-regression-diff@npm:3.2.8" + dependencies: + "@frsource/base64": 1.0.5 + glob: 8.0.3 + meta-png: 1.0.5 + move-file: 2.1.0 + pixelmatch: 5.3.0 + pngjs: 6.0.0 + sharp: 0.31.2 + peerDependencies: + cypress: ">=4.5.0" + checksum: 7e5a1daf3a36ec7f62ea643da8eff73aadb595e4fb8b400abec37e38a356a200de1ce47ff2e14666e71556300e4d044d5fa5ad21bdd2d214b93402addb1a490e + languageName: node + linkType: hard + "@gar/promisify@npm:^1.0.1, @gar/promisify@npm:^1.1.3": version: 1.1.3 resolution: "@gar/promisify@npm:1.1.3" @@ -9449,8 +9812,8 @@ __metadata: linkType: hard "@google-cloud/storage@npm:^6.0.0": - version: 6.9.1 - resolution: "@google-cloud/storage@npm:6.9.1" + version: 6.9.2 + resolution: "@google-cloud/storage@npm:6.9.2" dependencies: "@google-cloud/paginator": ^3.0.7 "@google-cloud/projectify": ^3.0.0 @@ -9469,7 +9832,7 @@ __metadata: retry-request: ^5.0.0 teeny-request: ^8.0.0 uuid: ^8.0.0 - checksum: 9967bc0e80d1531f91030d27fc46cef0242f70dde2064b051104d8d261f1d3cba38b51789b0ae0850bf99b13d093365f85c85c43a2d9468d81f0f98238f76e78 + checksum: e4962be29ca3dd722019db95f99b095993534915d663a82b41c95beaf284d878de5b1357102441e4e375de838299f6d82eff55a2c4e9f610f9a12feac21e2735 languageName: node linkType: hard @@ -9960,7 +10323,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/merge@npm:8.3.16, @graphql-tools/merge@npm:^8.2.6": +"@graphql-tools/merge@npm:8.3.16": version: 8.3.16 resolution: "@graphql-tools/merge@npm:8.3.16" dependencies: @@ -9972,6 +10335,18 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/merge@npm:8.3.18, @graphql-tools/merge@npm:^8.2.6": + version: 8.3.18 + resolution: "@graphql-tools/merge@npm:8.3.18" + dependencies: + "@graphql-tools/utils": 9.2.1 + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 0834fcf13dec5ee92e39f2e1b27a098654e2db20bf6ef6f43d7efce56b7d750caa59dd16aacb211b8f5a198d29c19d36471401762f5a34e0bc22657285dcae92 + languageName: node + linkType: hard + "@graphql-tools/mock@npm:^8.1.2": version: 8.6.8 resolution: "@graphql-tools/mock@npm:8.6.8" @@ -10081,7 +10456,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/schema@npm:9.0.14, @graphql-tools/schema@npm:^9.0.0": +"@graphql-tools/schema@npm:9.0.14": version: 9.0.14 resolution: "@graphql-tools/schema@npm:9.0.14" dependencies: @@ -10095,6 +10470,20 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/schema@npm:^9.0.0": + version: 9.0.16 + resolution: "@graphql-tools/schema@npm:9.0.16" + dependencies: + "@graphql-tools/merge": 8.3.18 + "@graphql-tools/utils": 9.2.1 + tslib: ^2.4.0 + value-or-promise: 1.0.12 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: ecbd27b4a36424c8d4b2206b1f2d2ec2cff3c66f52ec452b17977642017ca7f00df4df44f2d0426174ceea9596474d0656bc0227f4c1149f2288abb6d186cd68 + languageName: node + linkType: hard + "@graphql-tools/url-loader@npm:7.16.28, @graphql-tools/url-loader@npm:^7.13.2, @graphql-tools/url-loader@npm:^7.9.7": version: 7.16.28 resolution: "@graphql-tools/url-loader@npm:7.16.28" @@ -10173,7 +10562,7 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/utils@npm:9.1.4, @graphql-tools/utils@npm:^9.0.0, @graphql-tools/utils@npm:^9.1.1": +"@graphql-tools/utils@npm:9.1.4": version: 9.1.4 resolution: "@graphql-tools/utils@npm:9.1.4" dependencies: @@ -10184,6 +10573,18 @@ __metadata: languageName: node linkType: hard +"@graphql-tools/utils@npm:9.2.1, @graphql-tools/utils@npm:^9.0.0, @graphql-tools/utils@npm:^9.1.1": + version: 9.2.1 + resolution: "@graphql-tools/utils@npm:9.2.1" + dependencies: + "@graphql-typed-document-node/core": ^3.1.1 + tslib: ^2.4.0 + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + checksum: 94ed12df5f49e5c338322ffd931236a687a3d5c443bf499f9baab5d4fcd9792234111142be8aa506a01ca2e82732996c4e1d8f6159ff9cc7fdc5c97f63e55226 + languageName: node + linkType: hard + "@graphql-tools/wrap@npm:9.2.23": version: 9.2.23 resolution: "@graphql-tools/wrap@npm:9.2.23" @@ -10328,6 +10729,7 @@ __metadata: resolution: "@internal/plugin-todo-list-backend@workspace:plugins/example-todo-list-backend" dependencies: "@backstage/backend-common": "workspace:^" + "@backstage/backend-plugin-api": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" "@backstage/errors": "workspace:^" @@ -10711,460 +11113,6 @@ __metadata: languageName: node linkType: hard -"@jimp/bmp@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/bmp@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - bmp-js: ^0.1.0 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 3a5a7600add0edf68a0d8f8397dd0ca37fa20d93b4d9ea8321875acd92e4a632a31ac1af0fc2129cff76cdc77aa77b359327613f822c0bc48781f007459e44f8 - languageName: node - linkType: hard - -"@jimp/core@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/core@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - any-base: ^1.1.0 - buffer: ^5.2.0 - core-js: ^3.4.1 - exif-parser: ^0.1.12 - file-type: ^9.0.0 - load-bmfont: ^1.3.1 - mkdirp: ^0.5.1 - phin: ^2.9.1 - pixelmatch: ^4.0.2 - tinycolor2: ^1.4.1 - checksum: f67db147a114b7a220621b80f9c65cf0d1434f4bd7ab8203b44246b79093f9ec46170a6205755eae7fe53c14d2935d8565f5a2185ccd4495ae1cd6f13bcf4852 - languageName: node - linkType: hard - -"@jimp/custom@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/custom@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/core": ^0.10.3 - core-js: ^3.4.1 - checksum: 8a282c9387cb975a533d140a8e0c9f7c23b12b6e6a92e9a1e979b7cc7d89e623189c47b0d0119aa7e497149f0d9a85e4ec14a605921527340c307b7750ba519a - languageName: node - linkType: hard - -"@jimp/gif@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/gif@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - omggif: ^1.0.9 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: a23253e6762e6f19b0fa6deaa9db3bca25b313eb5ea506dd3d844407598d86cf4188cc92c091019a4257450821ffe24f0475cc2e4a1ef24998d95d176078cf2f - languageName: node - linkType: hard - -"@jimp/jpeg@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/jpeg@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - jpeg-js: ^0.3.4 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: cb830ddf3fddbc4f833086ffbf8f21e0c73863522cce86d6f6c82d4f0183d0619ee280736c5ed07563bfcf9f884fd96bdb845a95c5a8a6e11f961f4e95cb4b24 - languageName: node - linkType: hard - -"@jimp/plugin-blit@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-blit@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 5858557359eb0405c29741c8ffe98b4bb26b42564c178402e21b74733c02e04233d2a1af095cff4cd2864ab43d79e01033559f250b3eb879600138007a7d7453 - languageName: node - linkType: hard - -"@jimp/plugin-blur@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-blur@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: d3a2e14d4dc5f3b5ad010888e70193cc8f7327aeda67989eba3f49de2e5324fa6adf89e3a7f1597343f59688ce6e299ad5818aa6c670ed8596ddfba0ca281db3 - languageName: node - linkType: hard - -"@jimp/plugin-circle@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-circle@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: d351c85f4a9aeb404302a03f5d99e6486b95f271f2c628758c8e3d1574bc268278ffc34d9d00d3221d6079431b06a5e6a5a3922863994000b5eb3c81f8c27d35 - languageName: node - linkType: hard - -"@jimp/plugin-color@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-color@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - tinycolor2: ^1.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 2014802f3b0465abbd7682336836183293a26315acbb84826f5207a374f807c574e120790738dcf51aba2cb7e9a481003289ef8bf429facb9fe36a19ce5b6924 - languageName: node - linkType: hard - -"@jimp/plugin-contain@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-contain@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-blit": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - "@jimp/plugin-scale": ">=0.3.5" - checksum: 91d77621a350a733839fa8bc15880bc74ac666682a651e676797207dc4f55cba465f3695f6280f3d05122d67067a0a94f3905f323410d880484dd769055c9b49 - languageName: node - linkType: hard - -"@jimp/plugin-cover@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-cover@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-crop": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - "@jimp/plugin-scale": ">=0.3.5" - checksum: ca664509c2d42e987a56e80fa848ec3e5590a1f919c0a607d7a03b8192571456c0287a2f768785bf816ecfe7889a7a786490a408866acc94ce5306b39ca82f6b - languageName: node - linkType: hard - -"@jimp/plugin-crop@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-crop@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 54e79b6fa831f813f28dac53187dfa1684d00ece8a98433c30c1e61333ba2b5373758889d202418a55c99dffe235daf153454f35691324b42dd9a103a65d3bd2 - languageName: node - linkType: hard - -"@jimp/plugin-displace@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-displace@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 17b5792ce07ac3b7b5aa8c457626fb46ded0a8f05027c4580fac1b22428e7c2c0ff04e0ad4f7249c179322afa7921e57afc88ace03e2d08b67e864c24ef5c060 - languageName: node - linkType: hard - -"@jimp/plugin-dither@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-dither@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 77caaceebccceb73e32616fd79517d9640d217403577715e08146bf2a67599822070be8a51783ad76402e0303f6bc9afcebf75f75cc73ae44c684e3c1e1248ea - languageName: node - linkType: hard - -"@jimp/plugin-fisheye@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-fisheye@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: f60d9c6c40d6fa1b85658ffa23568f95aeab1635165f2b74266b128be0f9b4bf963cfa755e5a43b7f18421340a4bb7b628ab75821b34b602924154b55c0f39ce - languageName: node - linkType: hard - -"@jimp/plugin-flip@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-flip@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-rotate": ">=0.3.5" - checksum: 6542b9e595e3b95cf8ac741226e9089ab1edd3fa19bc4ac159afd5bc3801a55f6ee93ade029626dbde7977eb1fbca51c71d9c6546b5285754eb29b54a7f5f5ff - languageName: node - linkType: hard - -"@jimp/plugin-gaussian@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-gaussian@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 465ca178b9a9062eb6db4a57fe3b47236270bad9600cfb4aae162aad43f20e5cf90c19bf77bd08867f25abd0679c6018412e0ad8cb3777640dad5c3f51e64dd9 - languageName: node - linkType: hard - -"@jimp/plugin-invert@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-invert@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 917419301a961541466e04c7fc974fc95f35e3f95f75e6bfef141270f06b68bcdf61321cad2d545cd8115a6e3a99992345f964bdb94cbdb48aaf42885b8fcae7 - languageName: node - linkType: hard - -"@jimp/plugin-mask@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-mask@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 7870fe46d9fcec6145476bcceed23568012e87953772e7ea2b82c8ed48375ee525db6b977816574b47e08c595bd6123c8d9cd24a95e649677d0ec1fada4e6831 - languageName: node - linkType: hard - -"@jimp/plugin-normalize@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-normalize@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: f34ebf3ef03d83b2cb9a8779d39a3e10707aeae4dfe67e1bb30202589e72d09c0e9a61b429de4e4886728f0faabbb83a6ff081f2e9bf79b517e2ea387c53a9a4 - languageName: node - linkType: hard - -"@jimp/plugin-print@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-print@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - load-bmfont: ^1.4.0 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-blit": ">=0.3.5" - checksum: 772a7d82741af583293006f699bbbaf116c264327412ea6ed0f3b7fb14d0dcbef51278e6fce3e966ad23e655114bfe949106d551d06459819b26e461638e8a5e - languageName: node - linkType: hard - -"@jimp/plugin-resize@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-resize@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: aff691d2936bd88d59a68b36a6f06b306d0f7c654baec7b43371e6b250397b3a19d0b98a620ad26738ed51357f5899a8002761d8f6f0ae6074e6c63905895a5d - languageName: node - linkType: hard - -"@jimp/plugin-rotate@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-rotate@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-blit": ">=0.3.5" - "@jimp/plugin-crop": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - checksum: f1771365753a64387e455006467dd41e09c1e329180e57df77ef78f72dfac7d889e5d03adae1038f4c6ac081a9d5113c59541983fdda6bf08da9af852650c468 - languageName: node - linkType: hard - -"@jimp/plugin-scale@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-scale@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - checksum: 356e8ab040bf842845a29cc0a6d7170fd15c1a9e9407028b88771bff01ecb6870c8f26d15b59b432914c430e611f36f54f932aaa7a68dcc9e32efae2f6dd1946 - languageName: node - linkType: hard - -"@jimp/plugin-shadow@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-shadow@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-blur": ">=0.3.5" - "@jimp/plugin-resize": ">=0.3.5" - checksum: 7f07387160b31732b3d43a2c4d44f8716be7a08b2f0fcfd58d18ca76ed62d825629c6273434bd23d02ade6a18bc8d7c7020eb9ab0ccc95acd85e718b5ab2c7a0 - languageName: node - linkType: hard - -"@jimp/plugin-threshold@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugin-threshold@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - "@jimp/plugin-color": ">=0.8.0" - "@jimp/plugin-resize": ">=0.8.0" - checksum: 550de37f82ccfaacb954b0e183a7bfa77f80f33ec9f5b08756ce778675284e486a2b5860c69c6d15a38d1d7030f1896e1ac79e8675603ead9e36c075f3cf636e - languageName: node - linkType: hard - -"@jimp/plugins@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/plugins@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/plugin-blit": ^0.10.3 - "@jimp/plugin-blur": ^0.10.3 - "@jimp/plugin-circle": ^0.10.3 - "@jimp/plugin-color": ^0.10.3 - "@jimp/plugin-contain": ^0.10.3 - "@jimp/plugin-cover": ^0.10.3 - "@jimp/plugin-crop": ^0.10.3 - "@jimp/plugin-displace": ^0.10.3 - "@jimp/plugin-dither": ^0.10.3 - "@jimp/plugin-fisheye": ^0.10.3 - "@jimp/plugin-flip": ^0.10.3 - "@jimp/plugin-gaussian": ^0.10.3 - "@jimp/plugin-invert": ^0.10.3 - "@jimp/plugin-mask": ^0.10.3 - "@jimp/plugin-normalize": ^0.10.3 - "@jimp/plugin-print": ^0.10.3 - "@jimp/plugin-resize": ^0.10.3 - "@jimp/plugin-rotate": ^0.10.3 - "@jimp/plugin-scale": ^0.10.3 - "@jimp/plugin-shadow": ^0.10.3 - "@jimp/plugin-threshold": ^0.10.3 - core-js: ^3.4.1 - timm: ^1.6.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 28833f4ce43f911c41b5c251759f5e38406ba99249a7c00d11a81e4ce58027eb993ca0c220601c3702832c2855dbb0554762bf724107bb4fdd0a2c1cf8753895 - languageName: node - linkType: hard - -"@jimp/png@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/png@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/utils": ^0.10.3 - core-js: ^3.4.1 - pngjs: ^3.3.3 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 1520b669d0dc6a0b47d7357c5e75a080e3a56db0459fad5c56e299b03c4a7bf560540480b08fa2e1eaf283ba153efb26a3e90d90fe373069c6c6184f7ea3e163 - languageName: node - linkType: hard - -"@jimp/tiff@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/tiff@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - core-js: ^3.4.1 - utif: ^2.0.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 6ff41bc962afdbcd223b331beed232b5aecf2b0ba06cccddaef5b5d4b925fa51d6a26d226080baabd25d7dceb0b9b0e5c69b60c44494822cb69988c3221c6514 - languageName: node - linkType: hard - -"@jimp/types@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/types@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/bmp": ^0.10.3 - "@jimp/gif": ^0.10.3 - "@jimp/jpeg": ^0.10.3 - "@jimp/png": ^0.10.3 - "@jimp/tiff": ^0.10.3 - core-js: ^3.4.1 - timm: ^1.6.1 - peerDependencies: - "@jimp/custom": ">=0.3.5" - checksum: 68ea0f1a1f3116ba779c898c9fb38fb7b822addaa14ac153aea4cdc9e274efc34a46a6ad4c8a9978fb88b4ab5a826190e28b89b431dce735ee760997f2d66678 - languageName: node - linkType: hard - -"@jimp/utils@npm:^0.10.3": - version: 0.10.3 - resolution: "@jimp/utils@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - core-js: ^3.4.1 - regenerator-runtime: ^0.13.3 - checksum: bd36b8bbaaf878190afe357f1bcebed9215dae503f06603039e6a106045588003b845150cb5a5ba6dd4d973e7e7b95779158ae921011fb6bd79083c8cb177804 - languageName: node - linkType: hard - "@josephg/resolvable@npm:^1.0.0": version: 1.0.1 resolution: "@josephg/resolvable@npm:1.0.1" @@ -11259,11 +11207,11 @@ __metadata: linkType: hard "@keyv/redis@npm:^2.5.3": - version: 2.5.4 - resolution: "@keyv/redis@npm:2.5.4" + version: 2.5.5 + resolution: "@keyv/redis@npm:2.5.5" dependencies: - ioredis: ^5.2.4 - checksum: 02f557e0bc0343aea44b8dd4b6ff273da5833b935236c4036fa89e90b1d92d9688dafa064d5318d26744a6c72497cd4c41dcac825cb8d5919271e3a25a3cadbb + ioredis: ^5.3.0 + checksum: dc4c4e9c0a44dec1166a6a4481a42342289b4618136c2f640306bee381cf30d737346b064ecc369fc8ebdd2baaaba47e099aabe9f37839911171a9c006d75e62 languageName: node linkType: hard @@ -11396,7 +11344,45 @@ __metadata: languageName: node linkType: hard -"@material-ui/core@npm:^4.11.0, @material-ui/core@npm:^4.11.3, @material-ui/core@npm:^4.12.1, @material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4": +"@material-ui/core@npm:^3.9.3": + version: 3.9.4 + resolution: "@material-ui/core@npm:3.9.4" + dependencies: + "@babel/runtime": ^7.2.0 + "@material-ui/system": ^3.0.0-alpha.0 + "@material-ui/utils": ^3.0.0-alpha.2 + "@types/jss": ^9.5.6 + "@types/react-transition-group": ^2.0.8 + brcast: ^3.0.1 + classnames: ^2.2.5 + csstype: ^2.5.2 + debounce: ^1.1.0 + deepmerge: ^3.0.0 + dom-helpers: ^3.2.1 + hoist-non-react-statics: ^3.2.1 + is-plain-object: ^2.0.4 + jss: ^9.8.7 + jss-camel-case: ^6.0.0 + jss-default-unit: ^8.0.2 + jss-global: ^3.0.0 + jss-nested: ^6.0.1 + jss-props-sort: ^6.0.0 + jss-vendor-prefixer: ^7.0.0 + normalize-scroll-left: ^0.1.2 + popper.js: ^1.14.1 + prop-types: ^15.6.0 + react-event-listener: ^0.6.2 + react-transition-group: ^2.2.1 + recompose: 0.28.0 - 0.30.0 + warning: ^4.0.1 + peerDependencies: + react: ^16.3.0 + react-dom: ^16.3.0 + checksum: c3923bfba4ef4449fbdcd37712ec0d11393ffa396c575a05b5f9530ca1e714028882d39402eb6918dad49f2eb1c53b261cfd0f2dfed494c2dfd96ee335336b44 + languageName: node + linkType: hard + +"@material-ui/core@npm:^4.11.0, @material-ui/core@npm:^4.11.3, @material-ui/core@npm:^4.12.1, @material-ui/core@npm:^4.12.2, @material-ui/core@npm:^4.12.4, @material-ui/core@npm:^4.9.13": version: 4.12.4 resolution: "@material-ui/core@npm:4.12.4" dependencies: @@ -11554,6 +11540,21 @@ __metadata: languageName: node linkType: hard +"@material-ui/system@npm:^3.0.0-alpha.0": + version: 3.0.0-alpha.2 + resolution: "@material-ui/system@npm:3.0.0-alpha.2" + dependencies: + "@babel/runtime": ^7.2.0 + deepmerge: ^3.0.0 + prop-types: ^15.6.0 + warning: ^4.0.1 + peerDependencies: + react: ^16.3.0 + react-dom: ^16.3.0 + checksum: 3aacb8b4afe2a820844b90905cfcd86ab934108cd719f11713ee46f8a4da30cb8c50e86219e1440b3c6dd333a09d4123cd1990d9c51862c1d56ec2b36e4d7ded + languageName: node + linkType: hard + "@material-ui/system@npm:^4.12.2": version: 4.12.2 resolution: "@material-ui/system@npm:4.12.2" @@ -11597,6 +11598,20 @@ __metadata: languageName: node linkType: hard +"@material-ui/utils@npm:^3.0.0-alpha.2": + version: 3.0.0-alpha.3 + resolution: "@material-ui/utils@npm:3.0.0-alpha.3" + dependencies: + "@babel/runtime": ^7.2.0 + prop-types: ^15.6.0 + react-is: ^16.6.3 + peerDependencies: + react: ^16.3.0 + react-dom: ^16.3.0 + checksum: 589a16245338c374b604a793d9f7a05bd932c9710f78261e46176b74a61d5d308b3e36dab688988ee5177d689c53d9e9da4649eca009ea11d8aac948db27af50 + languageName: node + linkType: hard + "@material-ui/utils@npm:^4.11.2, @material-ui/utils@npm:^4.11.3, @material-ui/utils@npm:^4.7.1": version: 4.11.3 resolution: "@material-ui/utils@npm:4.11.3" @@ -11670,7 +11685,7 @@ __metadata: languageName: node linkType: hard -"@microsoft/microsoft-graph-types@npm:^2.6.0": +"@microsoft/microsoft-graph-types@npm:^2.25.0, @microsoft/microsoft-graph-types@npm:^2.6.0": version: 2.25.0 resolution: "@microsoft/microsoft-graph-types@npm:2.25.0" checksum: b311385e0a51f827082af5a016f35b1d6cd4d861121e8767b3279ab1a17475977cddbbdcc208abdddf0f953fd6ce4634d3c302e317cf50af4cb6111385347b4f @@ -12815,9 +12830,9 @@ __metadata: languageName: node linkType: hard -"@rjsf/core-v5@npm:@rjsf/core@5.0.1": - version: 5.0.1 - resolution: "@rjsf/core@npm:5.0.1" +"@rjsf/core-v5@npm:@rjsf/core@5.1.0": + version: 5.1.0 + resolution: "@rjsf/core@npm:5.1.0" dependencies: lodash: ^4.17.15 lodash-es: ^4.17.15 @@ -12826,7 +12841,7 @@ __metadata: peerDependencies: "@rjsf/utils": ^5.0.0 react: ^16.14.0 || >=17 - checksum: befd0b39aab7462f23ed926ca17314a049f338e81cdda8233e35c45febb34e3d1ff550ca8db7e64570342be281e3556a4cb045b52af86a0a551041d5ee22472b + checksum: ff38046bb25de86b57e404c5d1ccea84b0485e916caf33a3cb25b030a38ed930a9b89a411671a7c29b047da38e0c682e20b43b13e61d77df770299385ba3c67a languageName: node linkType: hard @@ -12849,16 +12864,16 @@ __metadata: languageName: node linkType: hard -"@rjsf/material-ui-v5@npm:@rjsf/material-ui@5.0.1": - version: 5.0.1 - resolution: "@rjsf/material-ui@npm:5.0.1" +"@rjsf/material-ui-v5@npm:@rjsf/material-ui@5.1.0": + version: 5.1.0 + resolution: "@rjsf/material-ui@npm:5.1.0" peerDependencies: "@material-ui/core": ^4.12.3 "@material-ui/icons": ^4.11.2 "@rjsf/core": ^5.0.0 "@rjsf/utils": ^5.0.0 react: ^16.14.0 || >=17 - checksum: 041f21caef8fd8e594c3b517eea15344c97017d03de5e608b1aead3e94cca474dc874930b64f59b506c1e578889664718d07e1e580fcffc78611dad3d33099fc + checksum: f8e2c486f33a60703cae5bb42ae82bc431b3d743c239868ca71907e7b4127a39d1688200820c0c8acdf3ecd3f5eaddef4ba47264695feead28e9b439d9be7ce8 languageName: node linkType: hard @@ -12874,9 +12889,9 @@ __metadata: languageName: node linkType: hard -"@rjsf/utils@npm:5.0.1": - version: 5.0.1 - resolution: "@rjsf/utils@npm:5.0.1" +"@rjsf/utils@npm:5.1.0": + version: 5.1.0 + resolution: "@rjsf/utils@npm:5.1.0" dependencies: json-schema-merge-allof: ^0.8.1 jsonpointer: ^5.0.1 @@ -12885,21 +12900,21 @@ __metadata: react-is: ^18.2.0 peerDependencies: react: ^16.14.0 || >=17 - checksum: 7bf76106eced48cfdf3072a26306ff00d0792774dfe501b061db110e9dd2e17bead77bc9b335c85adf27926ab860c39ee4f676007f8f07e74a438f3a31004692 + checksum: 76d7caf144f98b1547067bfdc2b1baecf452c63bd9d6f2cae6c162a99154bb4d2f5033f6abc0e26ab767dca9ea743f1f5a9d796696e90dd5ed1fdca5ee3c5280 languageName: node linkType: hard -"@rjsf/validator-ajv8@npm:5.0.1": - version: 5.0.1 - resolution: "@rjsf/validator-ajv8@npm:5.0.1" +"@rjsf/validator-ajv8@npm:5.1.0": + version: 5.1.0 + resolution: "@rjsf/validator-ajv8@npm:5.1.0" dependencies: - ajv: ^8.11.0 + ajv: ^8.12.0 ajv-formats: ^2.1.1 lodash: ^4.17.15 lodash-es: ^4.17.15 peerDependencies: "@rjsf/utils": ^5.0.0 - checksum: 7d36dac876fd3536dda26af5726e847af02a9880fac9feb2683809e8c1f3743e4f5ff8399e961a79f5383e28fdb5a1e9695d89ebf7dffaa22dd76948be830c54 + checksum: 3430a0aea8235deedd306b535ea3a62a9bcdd4dcf0bf1d022fe036041dceb5ca7c147b17ae50a0d25bb7c953729e987d96c68f5220f8fde4b56a89cf3f106a49 languageName: node linkType: hard @@ -13190,9 +13205,9 @@ __metadata: linkType: hard "@sideway/formula@npm:^3.0.0": - version: 3.0.0 - resolution: "@sideway/formula@npm:3.0.0" - checksum: 8ae26a0ed6bc84f7310be6aae6eb9d81e97f382619fc69025d346871a707eaab0fa38b8c857e3f0c35a19923de129f42d35c50b8010c928d64aab41578580ec4 + version: 3.0.1 + resolution: "@sideway/formula@npm:3.0.1" + checksum: e4beeebc9dbe2ff4ef0def15cec0165e00d1612e3d7cea0bc9ce5175c3263fc2c818b679bd558957f49400ee7be9d4e5ac90487e1625b4932e15c4aa7919c57a languageName: node linkType: hard @@ -13506,90 +13521,90 @@ __metadata: languageName: node linkType: hard -"@swc/core-darwin-arm64@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-darwin-arm64@npm:1.3.30" +"@swc/core-darwin-arm64@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-darwin-arm64@npm:1.3.34" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard -"@swc/core-darwin-x64@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-darwin-x64@npm:1.3.30" +"@swc/core-darwin-x64@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-darwin-x64@npm:1.3.34" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@swc/core-linux-arm-gnueabihf@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.30" +"@swc/core-linux-arm-gnueabihf@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.34" conditions: os=linux & cpu=arm languageName: node linkType: hard -"@swc/core-linux-arm64-gnu@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-arm64-gnu@npm:1.3.30" +"@swc/core-linux-arm64-gnu@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-arm64-gnu@npm:1.3.34" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-arm64-musl@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-arm64-musl@npm:1.3.30" +"@swc/core-linux-arm64-musl@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-arm64-musl@npm:1.3.34" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard -"@swc/core-linux-x64-gnu@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-x64-gnu@npm:1.3.30" +"@swc/core-linux-x64-gnu@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-x64-gnu@npm:1.3.34" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@swc/core-linux-x64-musl@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-linux-x64-musl@npm:1.3.30" +"@swc/core-linux-x64-musl@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-linux-x64-musl@npm:1.3.34" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@swc/core-win32-arm64-msvc@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-win32-arm64-msvc@npm:1.3.30" +"@swc/core-win32-arm64-msvc@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-win32-arm64-msvc@npm:1.3.34" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@swc/core-win32-ia32-msvc@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-win32-ia32-msvc@npm:1.3.30" +"@swc/core-win32-ia32-msvc@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-win32-ia32-msvc@npm:1.3.34" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard -"@swc/core-win32-x64-msvc@npm:1.3.30": - version: 1.3.30 - resolution: "@swc/core-win32-x64-msvc@npm:1.3.30" +"@swc/core-win32-x64-msvc@npm:1.3.34": + version: 1.3.34 + resolution: "@swc/core-win32-x64-msvc@npm:1.3.34" conditions: os=win32 & cpu=x64 languageName: node linkType: hard "@swc/core@npm:^1.3.9": - version: 1.3.30 - resolution: "@swc/core@npm:1.3.30" + version: 1.3.34 + resolution: "@swc/core@npm:1.3.34" dependencies: - "@swc/core-darwin-arm64": 1.3.30 - "@swc/core-darwin-x64": 1.3.30 - "@swc/core-linux-arm-gnueabihf": 1.3.30 - "@swc/core-linux-arm64-gnu": 1.3.30 - "@swc/core-linux-arm64-musl": 1.3.30 - "@swc/core-linux-x64-gnu": 1.3.30 - "@swc/core-linux-x64-musl": 1.3.30 - "@swc/core-win32-arm64-msvc": 1.3.30 - "@swc/core-win32-ia32-msvc": 1.3.30 - "@swc/core-win32-x64-msvc": 1.3.30 + "@swc/core-darwin-arm64": 1.3.34 + "@swc/core-darwin-x64": 1.3.34 + "@swc/core-linux-arm-gnueabihf": 1.3.34 + "@swc/core-linux-arm64-gnu": 1.3.34 + "@swc/core-linux-arm64-musl": 1.3.34 + "@swc/core-linux-x64-gnu": 1.3.34 + "@swc/core-linux-x64-musl": 1.3.34 + "@swc/core-win32-arm64-msvc": 1.3.34 + "@swc/core-win32-ia32-msvc": 1.3.34 + "@swc/core-win32-x64-msvc": 1.3.34 dependenciesMeta: "@swc/core-darwin-arm64": optional: true @@ -13611,7 +13626,7 @@ __metadata: optional: true "@swc/core-win32-x64-msvc": optional: true - checksum: 157a8ae43ebc73fa28d53e913fd7fc879289d5479ba5ea0c5e9abad0b379ca7e292d45f4f07bfd3ec0ff1998b55f300737724dfe463f1f7b361661e083db950c + checksum: a77c286b6b34d40f634ae882aac66f882cf806c165453878c31311070c46f020c607925bd454f64959504f08852b204cc37feba93fc6a3c1a64b295a9b5007d6 languageName: node linkType: hard @@ -13681,6 +13696,7 @@ __metadata: "@backstage/cli-common": "workspace:^" "@backstage/config": "workspace:^" "@backstage/plugin-techdocs-node": "workspace:^" + "@frsource/cypress-plugin-visual-regression-diff": ^3.2.8 "@types/commander": ^2.12.2 "@types/dockerode": ^3.3.0 "@types/fs-extra": ^9.0.6 @@ -13690,7 +13706,6 @@ __metadata: "@types/webpack-env": ^1.15.3 commander: ^9.1.0 cypress: ^10.0.0 - cypress-plugin-snapshots: ^1.4.4 dockerode: ^3.3.1 find-process: ^1.4.5 fs-extra: ^10.0.1 @@ -13751,7 +13766,7 @@ __metadata: languageName: node linkType: hard -"@testing-library/react-hooks@npm:^8.0.0": +"@testing-library/react-hooks@npm:^8.0.0, @testing-library/react-hooks@npm:^8.0.1": version: 8.0.1 resolution: "@testing-library/react-hooks@npm:8.0.1" dependencies: @@ -14180,6 +14195,15 @@ __metadata: languageName: node linkType: hard +"@types/cross-spawn@npm:^6.0.2": + version: 6.0.2 + resolution: "@types/cross-spawn@npm:6.0.2" + dependencies: + "@types/node": "*" + checksum: fa9edd32178878cab3ea8d6d0260639e0fe4860ddb3887b8de53d6e8036e154fc5f313c653f690975aa25025aea8beb83fb0870b931bf8d9202c3ac530a24c9d + languageName: node + linkType: hard + "@types/d3-color@npm:*": version: 3.0.2 resolution: "@types/d3-color@npm:3.0.2" @@ -14745,7 +14769,7 @@ __metadata: languageName: node linkType: hard -"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": +"@types/json-schema@npm:*, @types/json-schema@npm:^7.0.11, @types/json-schema@npm:^7.0.4, @types/json-schema@npm:^7.0.5, @types/json-schema@npm:^7.0.6, @types/json-schema@npm:^7.0.7, @types/json-schema@npm:^7.0.8, @types/json-schema@npm:^7.0.9": version: 7.0.11 resolution: "@types/json-schema@npm:7.0.11" checksum: 527bddfe62db9012fccd7627794bd4c71beb77601861055d87e3ee464f2217c85fca7a4b56ae677478367bbd248dbde13553312b7d4dbc702a2f2bbf60c4018d @@ -14784,6 +14808,16 @@ __metadata: languageName: node linkType: hard +"@types/jss@npm:^9.5.6": + version: 9.5.8 + resolution: "@types/jss@npm:9.5.8" + dependencies: + csstype: ^2.0.0 + indefinite-observable: ^1.0.1 + checksum: 6e51707529a15f2f5ff94555ecb555d29427fd10c4f3d2d29474934292e365c2dfaa4ad30b2ab46946201cbca7f6e8df56513a22fc6ceca10a979db8338935c5 + languageName: node + linkType: hard + "@types/jwt-decode@npm:^3.1.0": version: 3.1.0 resolution: "@types/jwt-decode@npm:3.1.0" @@ -14976,9 +15010,9 @@ __metadata: linkType: hard "@types/node@npm:*, @types/node@npm:>= 8, @types/node@npm:>=12.12.47, @types/node@npm:>=13.7.0, @types/node@npm:^18.11.17": - version: 18.11.18 - resolution: "@types/node@npm:18.11.18" - checksum: 03f17f9480f8d775c8a72da5ea7e9383db5f6d85aa5fefde90dd953a1449bd5e4ffde376f139da4f3744b4c83942166d2a7603969a6f8ea826edfb16e6e3b49d + version: 18.11.19 + resolution: "@types/node@npm:18.11.19" + checksum: d7cd19fcfc59cbdd3f9ba0b4072cb7adc21bd575bd8eb7d7e698975e63564aaa83f03434f32b12331f84f73d0b369d9cbe2371e359d9d7f5c3361f4987f4f7da languageName: node linkType: hard @@ -15018,9 +15052,9 @@ __metadata: linkType: hard "@types/node@npm:^16.0.0, @types/node@npm:^16.11.26, @types/node@npm:^16.9.2": - version: 16.18.11 - resolution: "@types/node@npm:16.18.11" - checksum: 2a3b1da13063debe6e26f732defb5f03ef4ef732c3e08daba838d8850433bd00e537ce1a97ce9bcfc4b15db5218d701d1265fae94e0d6926906bec157e6b46e0 + version: 16.18.12 + resolution: "@types/node@npm:16.18.12" + checksum: fc3271182414f8593018ef8f00b4718116a92f463f619081bd399d9460e7861e1dd7eebc7cf94c23567e418ff397babed077011711aae8d47171b5a81c5bd71d languageName: node linkType: hard @@ -15243,6 +15277,15 @@ __metadata: languageName: node linkType: hard +"@types/react-transition-group@npm:^2.0.8": + version: 2.9.2 + resolution: "@types/react-transition-group@npm:2.9.2" + dependencies: + "@types/react": "*" + checksum: 6f30fffc221339de90bd3e999f32328618cfaedfcaa501603f5ddc5bed1c2dbaa0d51347c3a25e5f8fd3041c671aabd2b7bfbcad611a7636adcd9da4e0666fa5 + languageName: node + linkType: hard + "@types/react-transition-group@npm:^4.2.0": version: 4.2.4 resolution: "@types/react-transition-group@npm:4.2.4" @@ -16404,13 +16447,6 @@ __metadata: languageName: node linkType: hard -"after@npm:0.8.2": - version: 0.8.2 - resolution: "after@npm:0.8.2" - checksum: 52ea9be2e97d764de256dfb3843d68cb180d2d464748010ea0d8079ee28190190080a53e4005a62adbf4337c2d613906db82f08c0091cccb8e817625ccf94c52 - languageName: node - linkType: hard - "agent-base@npm:6, agent-base@npm:^6.0.2": version: 6.0.2 resolution: "agent-base@npm:6.0.2" @@ -16487,7 +16523,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.11.0, ajv@npm:^8.8.0": +"ajv@npm:^8.0.0, ajv@npm:^8.10.0, ajv@npm:^8.12.0, ajv@npm:^8.8.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" dependencies: @@ -16607,13 +16643,6 @@ __metadata: languageName: node linkType: hard -"any-base@npm:^1.1.0": - version: 1.1.0 - resolution: "any-base@npm:1.1.0" - checksum: c1fd040de52e710e2de7d9ae4df52bac589f35622adb24686c98ce21c7b824859a8db9614bc119ed8614f42fd08918b2612e6a6c385480462b3100a1af59289d - languageName: node - linkType: hard - "any-promise@npm:^1.0.0": version: 1.3.0 resolution: "any-promise@npm:1.3.0" @@ -16918,7 +16947,7 @@ __metadata: languageName: node linkType: hard -"array-includes@npm:^3.1.4, array-includes@npm:^3.1.5, array-includes@npm:^3.1.6": +"array-includes@npm:^3.1.5, array-includes@npm:^3.1.6": version: 3.1.6 resolution: "array-includes@npm:3.1.6" dependencies: @@ -16931,15 +16960,6 @@ __metadata: languageName: node linkType: hard -"array-union@npm:^1.0.1": - version: 1.0.2 - resolution: "array-union@npm:1.0.2" - dependencies: - array-uniq: ^1.0.1 - checksum: 82cec6421b6e6766556c484835a6d476a873f1b71cace5ab2b4f1b15b1e3162dc4da0d16f7a2b04d4aec18146c6638fe8f661340b31ba8e469fd811a1b45dc8d - languageName: node - linkType: hard - "array-union@npm:^2.1.0": version: 2.1.0 resolution: "array-union@npm:2.1.0" @@ -16947,22 +16967,15 @@ __metadata: languageName: node linkType: hard -"array-uniq@npm:^1.0.1": - version: 1.0.3 - resolution: "array-uniq@npm:1.0.3" - checksum: 1625f06b093d8bf279b81adfec6e72951c0857d65b5e3f65f053fffe9f9dd61c2fc52cff57e38a4700817e7e3f01a4faa433d505ea9e33cdae4514c334e0bf9e - languageName: node - linkType: hard - -"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.2.5": - version: 1.3.0 - resolution: "array.prototype.flat@npm:1.3.0" +"array.prototype.flat@npm:^1.2.3, array.prototype.flat@npm:^1.3.1": + version: 1.3.1 + resolution: "array.prototype.flat@npm:1.3.1" dependencies: call-bind: ^1.0.2 - define-properties: ^1.1.3 - es-abstract: ^1.19.2 + define-properties: ^1.1.4 + es-abstract: ^1.20.4 es-shim-unscopables: ^1.0.0 - checksum: 2a652b3e8dc0bebb6117e42a5ab5738af0203a14c27341d7bb2431467bdb4b348e2c5dc555dfcda8af0a5e4075c400b85311ded73861c87290a71a17c3e0a257 + checksum: 5a8415949df79bf6e01afd7e8839bbde5a3581300e8ad5d8449dea52639e9e59b26a467665622783697917b43bf39940a6e621877c7dd9b3d1c1f97484b9b88b languageName: node linkType: hard @@ -16991,13 +17004,6 @@ __metadata: languageName: node linkType: hard -"arraybuffer.slice@npm:~0.0.7": - version: 0.0.7 - resolution: "arraybuffer.slice@npm:0.0.7" - checksum: c6bacada71e8fe3e63c5f5ac5f23810e617faa1bcf33be21db43f40819edc7e186172d4a6fb8f1b9baea26689fc8da6ddeb78cefa2cc9723156c0247c3587118 - languageName: node - linkType: hard - "arrify@npm:^1.0.1": version: 1.0.1 resolution: "arrify@npm:1.0.1" @@ -17166,6 +17172,13 @@ __metadata: languageName: node linkType: hard +"avsc@npm:^5.7.3": + version: 5.7.7 + resolution: "avsc@npm:5.7.7" + checksum: e3361aa88a61397b3345876263f79c8c8bfe013d849142202758205459a37e24cdbf02edc49ae019d6e82d93bbc7bc73e9e7fefca049aae91626bae28de4d1a9 + languageName: node + linkType: hard + "aws-lambda@npm:^1.0.7": version: 1.0.7 resolution: "aws-lambda@npm:1.0.7" @@ -17543,7 +17556,7 @@ __metadata: languageName: node linkType: hard -"backo2@npm:1.0.2, backo2@npm:^1.0.2": +"backo2@npm:^1.0.2": version: 1.0.2 resolution: "backo2@npm:1.0.2" checksum: fda8d0a0f4810068d23715f2f45153146d6ee8f62dd827ce1e0b6cc3c8328e84ad61e11399a83931705cef702fe7cbb457856bf99b9bd10c4ed57b0786252385 @@ -17585,13 +17598,6 @@ __metadata: languageName: node linkType: hard -"base64-arraybuffer@npm:0.1.4": - version: 0.1.4 - resolution: "base64-arraybuffer@npm:0.1.4" - checksum: d249a929e27b2430d7ba1527e91a36e14da37ae2f80e350c4d696a038257718f8da07577e820e7262f86a0ecd573c283db10c46502080f53ae11bfdd99b6a029 - languageName: node - linkType: hard - "base64-js@npm:^1.0.2, base64-js@npm:^1.3.0, base64-js@npm:^1.3.1, base64-js@npm:^1.5.1": version: 1.5.1 resolution: "base64-js@npm:1.5.1" @@ -17606,13 +17612,6 @@ __metadata: languageName: node linkType: hard -"base64id@npm:2.0.0": - version: 2.0.0 - resolution: "base64id@npm:2.0.0" - checksum: 581b1d37e6cf3738b7ccdd4d14fe2bfc5c238e696e2720ee6c44c183b838655842e22034e53ffd783f872a539915c51b0d4728a49c7cc678ac5a758e00d62168 - languageName: node - linkType: hard - "base64url@npm:3.x.x": version: 3.0.1 resolution: "base64url@npm:3.0.1" @@ -17662,13 +17661,13 @@ __metadata: linkType: hard "better-sqlite3@npm:^8.0.0": - version: 8.0.1 - resolution: "better-sqlite3@npm:8.0.1" + version: 8.1.0 + resolution: "better-sqlite3@npm:8.1.0" dependencies: bindings: ^1.5.0 node-gyp: latest prebuild-install: ^7.1.0 - checksum: 533b2cc32bd54e2a943a4f63e079f8cc8945879b7a0ebd2085b948824ea067792f6f903ab75ba75d7cafc5f8f973354cdf9a042fa6216e077925d07a8df1bead + checksum: 7e6b4c33142f4b2ee2e60d3926b4db682feb0d09554efdaf7bea53cd5bef406ca0a5115ab9b411458daa45ba14973252ebd6853a3cdf94e4fd49cc8291730ade languageName: node linkType: hard @@ -17719,10 +17718,10 @@ __metadata: languageName: node linkType: hard -"binary-extensions@npm:^2.0.0": - version: 2.0.0 - resolution: "binary-extensions@npm:2.0.0" - checksum: 554f65d3378cf71c3185c17dec3ca58334b8ff6ae242db3107284765ce33b2af19efd20c11faec41907a40534929e34b3a98e7d391c61e4211b45732dccb1115 +"binary-extensions@npm:^2.0.0, binary-extensions@npm:^2.2.0": + version: 2.2.0 + resolution: "binary-extensions@npm:2.2.0" + checksum: ccd267956c58d2315f5d3ea6757cf09863c5fc703e50fbeb13a7dc849b812ef76e3cf9ca8f35a0c48498776a7478d7b4a0418e1e2b8cb9cb9731f2922aaad7f8 languageName: node linkType: hard @@ -17774,13 +17773,6 @@ __metadata: languageName: node linkType: hard -"blob@npm:0.0.5": - version: 0.0.5 - resolution: "blob@npm:0.0.5" - checksum: ca6a025f1108b7fd317fba0a0d64424f5cf47be6f4653ec8ba44777804a4e5c86c8d7a7e41f75fe52fd77c5bfc3479f68e017abe649a241c15c3fc07bfc59e7b - languageName: node - linkType: hard - "bluebird@npm:3.7.2, bluebird@npm:^3.3.5, bluebird@npm:^3.5.5, bluebird@npm:^3.7.2": version: 3.7.2 resolution: "bluebird@npm:3.7.2" @@ -17788,13 +17780,6 @@ __metadata: languageName: node linkType: hard -"bmp-js@npm:^0.1.0": - version: 0.1.0 - resolution: "bmp-js@npm:0.1.0" - checksum: 2f6cf7eeabae2aa50eb768122f59e9752caa97248028cb8b5cf0d9db7cf8fb3a60262aeb2c6889dd21357ab061b2fb318f21f20d2b24963ba36ead8e264c6654 - languageName: node - linkType: hard - "bn.js@npm:^4.0.0, bn.js@npm:^4.1.0, bn.js@npm:^4.1.1, bn.js@npm:^4.11.9": version: 4.12.0 resolution: "bn.js@npm:4.12.0" @@ -17890,6 +17875,13 @@ __metadata: languageName: node linkType: hard +"brcast@npm:^3.0.1": + version: 3.0.2 + resolution: "brcast@npm:3.0.2" + checksum: 7abae42088c6ffad9ff9e0fc756607a1764e299d737b3007fa49a73a38b3fda1e51362713f645db7991878ca388de94942011188450324c3debd08509315fa94 + languageName: node + linkType: hard + "breakword@npm:^1.0.5": version: 1.0.5 resolution: "breakword@npm:1.0.5" @@ -18035,13 +18027,6 @@ __metadata: languageName: node linkType: hard -"buffer-equal@npm:0.0.1": - version: 0.0.1 - resolution: "buffer-equal@npm:0.0.1" - checksum: ca4b52e6c01143529d957a78cb9a93e4257f172bbab30d9eb87c20ae085ed23c5e07f236ac051202dacbf3d17aba42e1455f84cba21ea79b67d57f2b05e9a613 - languageName: node - linkType: hard - "buffer-from@npm:^1.0.0": version: 1.1.2 resolution: "buffer-from@npm:1.1.2" @@ -18084,7 +18069,7 @@ __metadata: languageName: node linkType: hard -"buffer@npm:^5.2.0, buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.6.0": +"buffer@npm:^5.2.1, buffer@npm:^5.5.0, buffer@npm:^5.6.0": version: 5.7.1 resolution: "buffer@npm:5.7.1" dependencies: @@ -18472,6 +18457,13 @@ __metadata: languageName: node linkType: hard +"change-emitter@npm:^0.1.2": + version: 0.1.6 + resolution: "change-emitter@npm:0.1.6" + checksum: 0ed494ba9901ca56ea6f942668fd294465c334a9a0981dca96da5aea5e387c0023a630d7c658c1b532d203db54c928ddca2564e434b4a8b7f6d39155d09db255 + languageName: node + linkType: hard + "char-regex@npm:^1.0.2": version: 1.0.2 resolution: "char-regex@npm:1.0.2" @@ -18728,6 +18720,17 @@ __metadata: languageName: node linkType: hard +"clipboard@npm:^2.0.4": + version: 2.0.11 + resolution: "clipboard@npm:2.0.11" + dependencies: + good-listener: ^1.2.2 + select: ^1.1.2 + tiny-emitter: ^2.0.0 + checksum: 413055a6038e43898e0e895216b58ed54fbf386f091cb00188875ef35b186cefbd258acdf4cb4b0ac87cbc00de936f41b45dde9fe1fd1a57f7babb28363b8748 + languageName: node + linkType: hard + "cliui@npm:^3.2.0": version: 3.2.0 resolution: "cliui@npm:3.2.0" @@ -18993,7 +18996,7 @@ __metadata: languageName: node linkType: hard -"color@npm:^4.0.1": +"color@npm:^4.0.1, color@npm:^4.2.3": version: 4.2.3 resolution: "color@npm:4.2.3" dependencies: @@ -19078,7 +19081,7 @@ __metadata: languageName: node linkType: hard -"commander@npm:*, commander@npm:^9.1.0, commander@npm:^9.4.1": +"commander@npm:*, commander@npm:^9.1.0, commander@npm:^9.4.1, commander@npm:^9.5.0": version: 9.5.0 resolution: "commander@npm:9.5.0" checksum: c7a3e27aa59e913b54a1bafd366b88650bc41d6651f0cbe258d4ff09d43d6a7394232a4dadd0bf518b3e696fdf595db1028a0d82c785b88bd61f8a440cecfade @@ -19162,6 +19165,20 @@ __metadata: languageName: node linkType: hard +"commonmark@npm:^0.29.0": + version: 0.29.3 + resolution: "commonmark@npm:0.29.3" + dependencies: + entities: ~2.0 + mdurl: ~1.0.1 + minimist: ">=1.2.2" + string.prototype.repeat: ^0.2.0 + bin: + commonmark: bin/commonmark + checksum: 892b603aeffea6a04770ce24979fc4024d2d92ca6d5146353398faab15be980af6802f3d7b35357400b870e752a9f80b766fed5c09ca063a2951c8c26013876c + languageName: node + linkType: hard + "compare-versions@npm:4.1.3": version: 4.1.3 resolution: "compare-versions@npm:4.1.3" @@ -19169,34 +19186,13 @@ __metadata: languageName: node linkType: hard -"component-bind@npm:1.0.0": - version: 1.0.0 - resolution: "component-bind@npm:1.0.0" - checksum: 746c5810b9f8735643840ad04072e1ab817444d44dc1aadc813f1f1a17c47c27616584caa0db93db7e687bfe73b65073d8246c785bcdac80f8f3627d3bb26883 - languageName: node - linkType: hard - -"component-emitter@npm:1.2.1": - version: 1.2.1 - resolution: "component-emitter@npm:1.2.1" - checksum: 00599b827635cab65bb20e5e3e2db4cea120b76b6626ce3ac6c85d7f5f39bbadd9fec530da444380035dd1c8ff08f9badca54d40b68feaf74bc64f75d537ef61 - languageName: node - linkType: hard - -"component-emitter@npm:^1.3.0, component-emitter@npm:~1.3.0": +"component-emitter@npm:^1.3.0": version: 1.3.0 resolution: "component-emitter@npm:1.3.0" checksum: b3c46de38ffd35c57d1c02488355be9f218e582aec72d72d1b8bbec95a3ac1b38c96cd6e03ff015577e68f550fbb361a3bfdbd9bb248be9390b7b3745691be6b languageName: node linkType: hard -"component-inherit@npm:0.0.3": - version: 0.0.3 - resolution: "component-inherit@npm:0.0.3" - checksum: 9f5b872a6b3a396cf827d471d07db5626d7918202ab6c1d59f2b849bab18ce4fe61d9b2b7b6ae5cd547b81f27a3de5ae678f438bfe4fadce8f26a1526ba86a74 - languageName: node - linkType: hard - "compress-commons@npm:^4.1.0": version: 4.1.0 resolution: "compress-commons@npm:4.1.0" @@ -19440,7 +19436,7 @@ __metadata: languageName: node linkType: hard -"cookie@npm:0.4.2, cookie@npm:^0.4.2, cookie@npm:~0.4.1": +"cookie@npm:0.4.2, cookie@npm:^0.4.2": version: 0.4.2 resolution: "cookie@npm:0.4.2" checksum: a00833c998bedf8e787b4c342defe5fa419abd96b32f4464f718b91022586b8f1bafbddd499288e75c037642493c83083da426c6a9080d309e3bd90fd11baa9b @@ -19487,6 +19483,13 @@ __metadata: languageName: node linkType: hard +"core-js@npm:^1.0.0": + version: 1.2.7 + resolution: "core-js@npm:1.2.7" + checksum: 0b76371bfa98708351cde580f9287e2360d2209920e738ae950ae74ad08639a2e063541020bf666c28778956fc356ed9fe56d962129c88a87a6a4a0612526c75 + languageName: node + linkType: hard + "core-js@npm:^2.4.0, core-js@npm:^2.5.0": version: 2.6.12 resolution: "core-js@npm:2.6.12" @@ -19494,7 +19497,7 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.4.1, core-js@npm:^3.6.5": +"core-js@npm:^3.6.5": version: 3.27.2 resolution: "core-js@npm:3.27.2" checksum: 718debd426f55a6b97cf9b757c936be258afd6d4f7052f89d0f96c982d7013e9000b0b006df42831a0cf32adad298e34d6a19052dce9ae1c7ab87162c0c665e0 @@ -19851,6 +19854,15 @@ __metadata: languageName: node linkType: hard +"css-vendor@npm:^0.3.8": + version: 0.3.8 + resolution: "css-vendor@npm:0.3.8" + dependencies: + is-in-browser: ^1.0.2 + checksum: 0a2e0cd0d4adbfdb6236950e7f9697b8a9b294eb2ae7c95996a95d273d2a63316ce793cb4654ae048aa3c129327124d2a29aada9935a0c284f9cc341c2768c8a + languageName: node + linkType: hard + "css-vendor@npm:^2.0.8": version: 2.0.8 resolution: "css-vendor@npm:2.0.8" @@ -19992,10 +20004,10 @@ __metadata: languageName: node linkType: hard -"csstype@npm:^2.5.2, csstype@npm:^2.6.7": - version: 2.6.17 - resolution: "csstype@npm:2.6.17" - checksum: 6eee5cf81a4b1b2f0e8707b4accd7504f7cceb4b5a496d58c6e4fcea1a70c1443a975e45d722c892d372ffe788fa278ddfe4d70c5f881375f34e48bb99c70ecc +"csstype@npm:^2.0.0, csstype@npm:^2.5.2, csstype@npm:^2.6.7": + version: 2.6.21 + resolution: "csstype@npm:2.6.21" + checksum: 2ce8bc832375146eccdf6115a1f8565a27015b74cce197c35103b4494955e9516b246140425ad24103864076aa3e1257ac9bab25a06c8d931dd87a6428c9dccf languageName: node linkType: hard @@ -20039,31 +20051,6 @@ __metadata: languageName: node linkType: hard -"cypress-plugin-snapshots@npm:^1.4.4": - version: 1.4.4 - resolution: "cypress-plugin-snapshots@npm:1.4.4" - dependencies: - diff2html: ^2.7.0 - fs-extra: ^7.0.1 - image-size: ^0.7.2 - jimp: ^0.10.3 - js-base64: ^2.5.1 - lodash: ^4.17.13 - pixelmatch: ^4.0.2 - pngjs: ^3.3.3 - prettier: ^1.16.4 - rimraf: ^2.6.3 - sanitize-filename: ^1.6.1 - socket.io: ^2.2.0 - socket.io-client: ^2.2.0 - source-map-support: ^0.5.10 - unidiff: 1.0.2 - peerDependencies: - cypress: ^4.5.0 - checksum: 75b8d6b6e0353f7d2ac617c503f33c84c7c5f4e650dbaf59a76558e971bcc82532d2ac37b9eeccb7382354a13fe5d0068c8e97a9f7f6f1af6135cf5286c285f9 - languageName: node - linkType: hard - "cypress@npm:^10.0.0": version: 10.11.0 resolution: "cypress@npm:10.11.0" @@ -20394,14 +20381,14 @@ __metadata: languageName: node linkType: hard -"debounce@npm:^1.2.0": - version: 1.2.0 - resolution: "debounce@npm:1.2.0" - checksum: e39cb593ae26344921f5a2681b40b703bdd22bc43b179f0e7515176c790997932b3a0ee6ea9864f384c6ac58cecc08158fb102c3632d5d88ab621f8230ee39ff +"debounce@npm:^1.1.0, debounce@npm:^1.2.0": + version: 1.2.1 + resolution: "debounce@npm:1.2.1" + checksum: 682a89506d9e54fb109526f4da255c5546102fbb8e3ae75eef3b04effaf5d4853756aee97475cd4650641869794e44f410eeb20ace2b18ea592287ab2038519e languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.6.0, debug@npm:^2.6.9": +"debug@npm:2.6.9, debug@npm:^2.6.0": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -20431,24 +20418,6 @@ __metadata: languageName: node linkType: hard -"debug@npm:~3.1.0": - version: 3.1.0 - resolution: "debug@npm:3.1.0" - dependencies: - ms: 2.0.0 - checksum: 0b52718ab957254a5b3ca07fc34543bc778f358620c206a08452251eb7fc193c3ea3505072acbf4350219c14e2d71ceb7bdaa0d3370aa630b50da790458d08b3 - languageName: node - linkType: hard - -"debug@npm:~4.1.0": - version: 4.1.1 - resolution: "debug@npm:4.1.1" - dependencies: - ms: ^2.1.1 - checksum: 1e681f5cce94ba10f8dde74b20b42e4d8cf0d2a6700f4c165bb3bb6885565ef5ca5885bf07e704974a835f2415ff095a63164f539988a1f07e8a69fe8b1d65ad - languageName: node - linkType: hard - "debuglog@npm:^1.0.1": version: 1.0.1 resolution: "debuglog@npm:1.0.1" @@ -20567,6 +20536,13 @@ __metadata: languageName: node linkType: hard +"deepmerge@npm:^3.0.0": + version: 3.3.0 + resolution: "deepmerge@npm:3.3.0" + checksum: 4322195389e0170a0443c07b36add19b90249802c4b47b96265fdc5f5d8beddf491d5e50cbc5bfd65f85ccf76598173083863c202f5463b3b667aca8be75d5ac + languageName: node + linkType: hard + "deepmerge@npm:^4.2.2, deepmerge@npm:~4.2.2": version: 4.2.2 resolution: "deepmerge@npm:4.2.2" @@ -20646,6 +20622,13 @@ __metadata: languageName: node linkType: hard +"delegate@npm:^3.1.2": + version: 3.2.0 + resolution: "delegate@npm:3.2.0" + checksum: d943058fe05897228b158cbd1bab05164df28c8f54127873231d6b03b0a5acc1b3ee1f98ac70ccc9b79cd84aa47118a7de111fee2923753491583905069da27d + languageName: node + linkType: hard + "delegates@npm:^1.0.0": version: 1.0.0 resolution: "delegates@npm:1.0.0" @@ -20653,10 +20636,10 @@ __metadata: languageName: node linkType: hard -"denque@npm:^2.0.1": - version: 2.0.1 - resolution: "denque@npm:2.0.1" - checksum: ec398d1e3c6c8d4f5213dcf9ad74d7faa3b461e29a0019c9742b49a97ac5e16aa7134db45fa9d841e318e7722dd1ba670a474fde9a5b0d870b3a5fc6fe914c30 +"denque@npm:^2.0.1, denque@npm:^2.1.0": + version: 2.1.0 + resolution: "denque@npm:2.1.0" + checksum: 1d4ae1d05e59ac3a3481e7b478293f4b4c813819342273f3d5b826c7ffa9753c520919ba264f377e09108d24ec6cf0ec0ac729a5686cbb8f32d797126c5dae74 languageName: node linkType: hard @@ -20728,7 +20711,7 @@ __metadata: languageName: node linkType: hard -"detect-libc@npm:^2.0.0": +"detect-libc@npm:^2.0.0, detect-libc@npm:^2.0.1": version: 2.0.1 resolution: "detect-libc@npm:2.0.1" checksum: ccb05fcabbb555beb544d48080179c18523a343face9ee4e1a86605a8715b4169f94d663c21a03c310ac824592f2ba9a5270218819bb411ad7be578a527593d7 @@ -20793,18 +20776,6 @@ __metadata: languageName: node linkType: hard -"diff2html@npm:^2.7.0": - version: 2.12.2 - resolution: "diff2html@npm:2.12.2" - dependencies: - diff: ^4.0.1 - hogan.js: ^3.0.2 - merge: ^1.2.1 - whatwg-fetch: ^3.0.0 - checksum: 4d659fb558575406881bc9e6b892a236d038bca1335cbdde65ebfde462cf1cff7a7ab4a2e82d9874205e658dd2caa31699ba24ea4efb714d7c7277ddb96c7768 - languageName: node - linkType: hard - "diff3@npm:0.0.3": version: 0.0.3 resolution: "diff3@npm:0.0.3" @@ -20812,13 +20783,6 @@ __metadata: languageName: node linkType: hard -"diff@npm:^2.2.2": - version: 2.2.3 - resolution: "diff@npm:2.2.3" - checksum: b5b6fbe1848c621ef6f6c154e689842f7bb992b4c0ae257b70f5577b0906812537bf2eccda14d0215f90579567c8880eb44f1cf3851bd615583214a4f4d61ce3 - languageName: node - linkType: hard - "diff@npm:^4.0.1": version: 4.0.2 resolution: "diff@npm:4.0.2" @@ -20844,15 +20808,6 @@ __metadata: languageName: node linkType: hard -"dir-glob@npm:^2.0.0": - version: 2.2.2 - resolution: "dir-glob@npm:2.2.2" - dependencies: - path-type: ^3.0.0 - checksum: 3aa48714a9f7845ffc30ab03a5c674fe760477cc55e67b0847333371549227d93953e6627ec160f75140c5bea5c5f88d13c01de79bd1997a588efbcf06980842 - languageName: node - linkType: hard - "dir-glob@npm:^3.0.1": version: 3.0.1 resolution: "dir-glob@npm:3.0.1" @@ -20944,7 +20899,7 @@ __metadata: languageName: node linkType: hard -"dom-helpers@npm:^3.4.0": +"dom-helpers@npm:^3.2.1, dom-helpers@npm:^3.4.0": version: 3.4.0 resolution: "dom-helpers@npm:3.4.0" dependencies: @@ -20985,13 +20940,6 @@ __metadata: languageName: node linkType: hard -"dom-walk@npm:^0.1.0": - version: 0.1.1 - resolution: "dom-walk@npm:0.1.1" - checksum: f55db10d0344b87cca1046a2356915a8e9ddee90fc97d3ebb26c4021e593cce47d24cbef18aac721421787c6a60fa14dd079a0b81f8610c9aeeec52177fd2acf - languageName: node - linkType: hard - "domain-browser@npm:^1.1.1": version: 1.2.0 resolution: "domain-browser@npm:1.2.0" @@ -21298,7 +21246,7 @@ __metadata: languageName: node linkType: hard -"encoding@npm:^0.1.12, encoding@npm:^0.1.13": +"encoding@npm:^0.1.11, encoding@npm:^0.1.12, encoding@npm:^0.1.13": version: 0.1.13 resolution: "encoding@npm:0.1.13" dependencies: @@ -21316,52 +21264,6 @@ __metadata: languageName: node linkType: hard -"engine.io-client@npm:~3.5.0": - version: 3.5.2 - resolution: "engine.io-client@npm:3.5.2" - dependencies: - component-emitter: ~1.3.0 - component-inherit: 0.0.3 - debug: ~3.1.0 - engine.io-parser: ~2.2.0 - has-cors: 1.1.0 - indexof: 0.0.1 - parseqs: 0.0.6 - parseuri: 0.0.6 - ws: ~7.4.2 - xmlhttprequest-ssl: ~1.6.2 - yeast: 0.1.2 - checksum: 2a4a8407ea939c45826d3483e39fe017e66d488dfd101282a79644f75196f7b84cac9d6f981e3ace1ab231e2469c18b25d79b114f20ab58d0eaa393bf66f213e - languageName: node - linkType: hard - -"engine.io-parser@npm:~2.2.0": - version: 2.2.1 - resolution: "engine.io-parser@npm:2.2.1" - dependencies: - after: 0.8.2 - arraybuffer.slice: ~0.0.7 - base64-arraybuffer: 0.1.4 - blob: 0.0.5 - has-binary2: ~1.0.2 - checksum: c7291955c1af4b6c384c2642c2d10a4df0ca7b6acc5cb178dd4933bcb1522eccc3d89ffae6e6080ce760451378fce49db47af3fb9999cae08de310e75f3c29d6 - languageName: node - linkType: hard - -"engine.io@npm:~3.6.0": - version: 3.6.1 - resolution: "engine.io@npm:3.6.1" - dependencies: - accepts: ~1.3.4 - base64id: 2.0.0 - cookie: ~0.4.1 - debug: ~4.1.0 - engine.io-parser: ~2.2.0 - ws: ~7.4.2 - checksum: 7dd9eddc79aedf61a63141e6208ee89936c659445378f965bff6d5eb3e9cb4815aff735f2b1886487e2be51116a343e89d972ae5790bf2ac9fdcb2572baf8935 - languageName: node - linkType: hard - "enhanced-resolve@npm:^5.10.0": version: 5.10.0 resolution: "enhanced-resolve@npm:5.10.0" @@ -21402,6 +21304,13 @@ __metadata: languageName: node linkType: hard +"entities@npm:~2.0": + version: 2.0.3 + resolution: "entities@npm:2.0.3" + checksum: 5a7899fcc622e0d76afdeafe4c58a6b40ae3a8ee4772e5825a648c11a2ca324a9a02515386f512e466baac4aeb551f3d3b79eaece5cd98369b9f8601be336b1a + languageName: node + linkType: hard + "entities@npm:~2.1.0": version: 2.1.0 resolution: "entities@npm:2.1.0" @@ -21455,7 +21364,7 @@ __metadata: languageName: node linkType: hard -"es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.2, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.4": +"es-abstract@npm:^1.19.0, es-abstract@npm:^1.19.5, es-abstract@npm:^1.20.4": version: 1.20.4 resolution: "es-abstract@npm:1.20.4" dependencies: @@ -21693,7 +21602,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.15.6": +"esbuild@npm:^0.15.6, esbuild@npm:~0.15.10": version: 0.15.18 resolution: "esbuild@npm:0.15.18" dependencies: @@ -21771,31 +21680,31 @@ __metadata: linkType: hard "esbuild@npm:^0.17.0": - version: 0.17.5 - resolution: "esbuild@npm:0.17.5" + version: 0.17.6 + resolution: "esbuild@npm:0.17.6" dependencies: - "@esbuild/android-arm": 0.17.5 - "@esbuild/android-arm64": 0.17.5 - "@esbuild/android-x64": 0.17.5 - "@esbuild/darwin-arm64": 0.17.5 - "@esbuild/darwin-x64": 0.17.5 - "@esbuild/freebsd-arm64": 0.17.5 - "@esbuild/freebsd-x64": 0.17.5 - "@esbuild/linux-arm": 0.17.5 - "@esbuild/linux-arm64": 0.17.5 - "@esbuild/linux-ia32": 0.17.5 - "@esbuild/linux-loong64": 0.17.5 - "@esbuild/linux-mips64el": 0.17.5 - "@esbuild/linux-ppc64": 0.17.5 - "@esbuild/linux-riscv64": 0.17.5 - "@esbuild/linux-s390x": 0.17.5 - "@esbuild/linux-x64": 0.17.5 - "@esbuild/netbsd-x64": 0.17.5 - "@esbuild/openbsd-x64": 0.17.5 - "@esbuild/sunos-x64": 0.17.5 - "@esbuild/win32-arm64": 0.17.5 - "@esbuild/win32-ia32": 0.17.5 - "@esbuild/win32-x64": 0.17.5 + "@esbuild/android-arm": 0.17.6 + "@esbuild/android-arm64": 0.17.6 + "@esbuild/android-x64": 0.17.6 + "@esbuild/darwin-arm64": 0.17.6 + "@esbuild/darwin-x64": 0.17.6 + "@esbuild/freebsd-arm64": 0.17.6 + "@esbuild/freebsd-x64": 0.17.6 + "@esbuild/linux-arm": 0.17.6 + "@esbuild/linux-arm64": 0.17.6 + "@esbuild/linux-ia32": 0.17.6 + "@esbuild/linux-loong64": 0.17.6 + "@esbuild/linux-mips64el": 0.17.6 + "@esbuild/linux-ppc64": 0.17.6 + "@esbuild/linux-riscv64": 0.17.6 + "@esbuild/linux-s390x": 0.17.6 + "@esbuild/linux-x64": 0.17.6 + "@esbuild/netbsd-x64": 0.17.6 + "@esbuild/openbsd-x64": 0.17.6 + "@esbuild/sunos-x64": 0.17.6 + "@esbuild/win32-arm64": 0.17.6 + "@esbuild/win32-ia32": 0.17.6 + "@esbuild/win32-x64": 0.17.6 dependenciesMeta: "@esbuild/android-arm": optional: true @@ -21843,7 +21752,7 @@ __metadata: optional: true bin: esbuild: bin/esbuild - checksum: 31829b46a0cad65f58d90d39b66fd32a2de820539dd8530199975e417aa0ecd63cfc340e402f4f4e9235906a1a05969ddac1d4ad1ba611a3f11c0f62516b9390 + checksum: 13c4c3bcaa7ff291810d2c8294a1eb997b4672c60a9285b32d8eafcbc552e4468e06efe9d1a15067f5cbd41adcb12ddb4362618845e2e916e437f281c9aa80ab languageName: node linkType: hard @@ -21951,23 +21860,26 @@ __metadata: languageName: node linkType: hard -"eslint-import-resolver-node@npm:^0.3.6": - version: 0.3.6 - resolution: "eslint-import-resolver-node@npm:0.3.6" +"eslint-import-resolver-node@npm:^0.3.7": + version: 0.3.7 + resolution: "eslint-import-resolver-node@npm:0.3.7" dependencies: debug: ^3.2.7 - resolve: ^1.20.0 - checksum: 6266733af1e112970e855a5bcc2d2058fb5ae16ad2a6d400705a86b29552b36131ffc5581b744c23d550de844206fb55e9193691619ee4dbf225c4bde526b1c8 + is-core-module: ^2.11.0 + resolve: ^1.22.1 + checksum: 3379aacf1d2c6952c1b9666c6fa5982c3023df695430b0d391c0029f6403a7775414873d90f397e98ba6245372b6c8960e16e74d9e4a3b0c0a4582f3bdbe3d6e languageName: node linkType: hard -"eslint-module-utils@npm:^2.1.1, eslint-module-utils@npm:^2.7.3": - version: 2.7.3 - resolution: "eslint-module-utils@npm:2.7.3" +"eslint-module-utils@npm:^2.7.4": + version: 2.7.4 + resolution: "eslint-module-utils@npm:2.7.4" dependencies: debug: ^3.2.7 - find-up: ^2.1.0 - checksum: 77048263f309167a1e6a1e1b896bfb5ddd1d3859b2e2abbd9c32c432aee13d610d46e6820b1ca81b37fba437cf423a404bc6649be64ace9148a3062d1886a678 + peerDependenciesMeta: + eslint: + optional: true + checksum: 5da13645daff145a5c922896b258f8bba560722c3767254e458d894ff5fbb505d6dfd945bffa932a5b0ae06714da2379bd41011c4c20d2d59cc83e23895360f7 languageName: node linkType: hard @@ -21997,25 +21909,27 @@ __metadata: linkType: hard "eslint-plugin-import@npm:^2.25.4": - version: 2.26.0 - resolution: "eslint-plugin-import@npm:2.26.0" + version: 2.27.5 + resolution: "eslint-plugin-import@npm:2.27.5" dependencies: - array-includes: ^3.1.4 - array.prototype.flat: ^1.2.5 - debug: ^2.6.9 + array-includes: ^3.1.6 + array.prototype.flat: ^1.3.1 + array.prototype.flatmap: ^1.3.1 + debug: ^3.2.7 doctrine: ^2.1.0 - eslint-import-resolver-node: ^0.3.6 - eslint-module-utils: ^2.7.3 + eslint-import-resolver-node: ^0.3.7 + eslint-module-utils: ^2.7.4 has: ^1.0.3 - is-core-module: ^2.8.1 + is-core-module: ^2.11.0 is-glob: ^4.0.3 minimatch: ^3.1.2 - object.values: ^1.1.5 - resolve: ^1.22.0 + object.values: ^1.1.6 + resolve: ^1.22.1 + semver: ^6.3.0 tsconfig-paths: ^3.14.1 peerDependencies: eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 - checksum: 0bf77ad80339554481eafa2b1967449e1f816b94c7a6f9614ce33fb4083c4e6c050f10d241dd50b4975d47922880a34de1e42ea9d8e6fd663ebb768baa67e655 + checksum: f500571a380167e25d72a4d925ef9a7aae8899eada57653e5f3051ec3d3c16d08271fcefe41a30a9a2f4fefc232f066253673ee4ea77b30dba65ae173dade85d languageName: node linkType: hard @@ -22062,21 +21976,6 @@ __metadata: languageName: node linkType: hard -"eslint-plugin-monorepo@npm:^0.3.2": - version: 0.3.2 - resolution: "eslint-plugin-monorepo@npm:0.3.2" - dependencies: - eslint-module-utils: ^2.1.1 - get-monorepo-packages: ^1.1.0 - globby: ^7.1.1 - load-json-file: ^4.0.0 - minimatch: ^3.0.4 - parse-package-name: ^0.1.0 - path-is-inside: ^1.0.2 - checksum: b6f17efbc9e66aefbf7aacf9aa130bc8e729a6311c412d1b0026ce7d93361c10b0e6e4b10cf434f24ca9a8b2e891bc8e5b82e66dc9ccc5e782f9c18a989eca28 - languageName: node - linkType: hard - "eslint-plugin-notice@npm:^0.9.10": version: 0.9.10 resolution: "eslint-plugin-notice@npm:0.9.10" @@ -22125,13 +22024,13 @@ __metadata: linkType: hard "eslint-plugin-testing-library@npm:^5.9.1": - version: 5.10.0 - resolution: "eslint-plugin-testing-library@npm:5.10.0" + version: 5.10.1 + resolution: "eslint-plugin-testing-library@npm:5.10.1" dependencies: "@typescript-eslint/utils": ^5.43.0 peerDependencies: eslint: ^7.5.0 || ^8.0.0 - checksum: 3278fc4683a99d24ac2b6d2ed0359db1b0509674350e4b9a958a226f57b4b90e070c02e1f4c2806da885d8025c1e8c952cb9a5e9751e69baac3d12cfe6804000 + checksum: fbc24ce8cbd03bee283ae520f023bd52f10d21fb25bd66c5a09a0e0ad17997bae559450438ce0b4fcc38b4e14b6802f18482c41d2bc72bf1a8e1a12a91766572 languageName: node linkType: hard @@ -22196,7 +22095,7 @@ __metadata: languageName: node linkType: hard -"eslint@npm:^8.6.0": +"eslint@npm:^8.33.0, eslint@npm:^8.6.0": version: 8.33.0 resolution: "eslint@npm:8.33.0" dependencies: @@ -22455,6 +22354,7 @@ __metadata: "@backstage/plugin-code-coverage": "workspace:^" "@backstage/plugin-cost-insights": "workspace:^" "@backstage/plugin-dynatrace": "workspace:^" + "@backstage/plugin-entity-feedback": "workspace:^" "@backstage/plugin-explore": "workspace:^" "@backstage/plugin-gcalendar": "workspace:^" "@backstage/plugin-gcp-projects": "workspace:^" @@ -22466,6 +22366,9 @@ __metadata: "@backstage/plugin-kafka": "workspace:^" "@backstage/plugin-kubernetes": "workspace:^" "@backstage/plugin-lighthouse": "workspace:^" + "@backstage/plugin-linguist": "workspace:^" + "@backstage/plugin-linguist-common": "workspace:^" + "@backstage/plugin-microsoft-calendar": "workspace:^" "@backstage/plugin-newrelic": "workspace:^" "@backstage/plugin-newrelic-dashboard": "workspace:^" "@backstage/plugin-org": "workspace:^" @@ -22556,6 +22459,7 @@ __metadata: "@backstage/plugin-catalog-backend": "workspace:^" "@backstage/plugin-catalog-node": "workspace:^" "@backstage/plugin-code-coverage-backend": "workspace:^" + "@backstage/plugin-entity-feedback-backend": "workspace:^" "@backstage/plugin-events-backend": "workspace:^" "@backstage/plugin-events-node": "workspace:^" "@backstage/plugin-explore-backend": "workspace:^" @@ -22564,6 +22468,7 @@ __metadata: "@backstage/plugin-kafka-backend": "workspace:^" "@backstage/plugin-kubernetes-backend": "workspace:^" "@backstage/plugin-lighthouse-backend": "workspace:^" + "@backstage/plugin-linguist-backend": "workspace:^" "@backstage/plugin-permission-backend": "workspace:^" "@backstage/plugin-permission-common": "workspace:^" "@backstage/plugin-permission-node": "workspace:^" @@ -22663,13 +22568,6 @@ __metadata: languageName: node linkType: hard -"exif-parser@npm:^0.1.12": - version: 0.1.12 - resolution: "exif-parser@npm:0.1.12" - checksum: 6ba50cb9e0b45a6efa37e966a9582ecd171b5c5b3ef0c47542f2b862c521f70d2f656dde85b4d2a5dd8e1163486b09049f4c412e9c6176bfbda1654a5b2f021c - languageName: node - linkType: hard - "exit-on-epipe@npm:~1.0.1": version: 1.0.1 resolution: "exit-on-epipe@npm:1.0.1" @@ -23040,6 +22938,21 @@ __metadata: languageName: node linkType: hard +"fbjs@npm:^0.8.1": + version: 0.8.18 + resolution: "fbjs@npm:0.8.18" + dependencies: + core-js: ^1.0.0 + isomorphic-fetch: ^2.1.1 + loose-envify: ^1.0.0 + object-assign: ^4.1.0 + promise: ^7.1.1 + setimmediate: ^1.0.5 + ua-parser-js: ^0.7.30 + checksum: 668731b946a765908c9cbe51d5160f973abb78004b3d122587c3e930e3e1ddcc0ce2b17f2a8637dc9d733e149aa580f8d3035a35cc2d3bc78b78f1b19aab90e2 + languageName: node + linkType: hard + "fbjs@npm:^3.0.0": version: 3.0.0 resolution: "fbjs@npm:3.0.0" @@ -23100,13 +23013,6 @@ __metadata: languageName: node linkType: hard -"file-type@npm:^9.0.0": - version: 9.0.0 - resolution: "file-type@npm:9.0.0" - checksum: 9ea78b29c3762d967eb1e3e4f45e401388b6d252b12c217f78f5ea97556ff7e35e4c7255cab68810ac414d51b776bd4e83504c86f132c262a454251561189efa - languageName: node - linkType: hard - "file-uri-to-path@npm:1.0.0": version: 1.0.0 resolution: "file-uri-to-path@npm:1.0.0" @@ -23214,15 +23120,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^2.1.0": - version: 2.1.0 - resolution: "find-up@npm:2.1.0" - dependencies: - locate-path: ^2.0.0 - checksum: 43284fe4da09f89011f08e3c32cd38401e786b19226ea440b75386c1b12a4cb738c94969808d53a84f564ede22f732c8409e3cfc3f7fb5b5c32378ad0bbf28bd - languageName: node - linkType: hard - "find-up@npm:^3.0.0": version: 3.0.0 resolution: "find-up@npm:3.0.0" @@ -23797,16 +23694,6 @@ __metadata: languageName: node linkType: hard -"get-monorepo-packages@npm:^1.1.0": - version: 1.2.0 - resolution: "get-monorepo-packages@npm:1.2.0" - dependencies: - globby: ^7.1.1 - load-json-file: ^4.0.0 - checksum: f9321c11b8e11f02138758db6589d8ab8b7e1b05e78cac92493b635faea10aa100c20fe40f2cf110c82ec100c118c6c131dff4c65d32a721c617dc2928f7b277 - languageName: node - linkType: hard - "get-package-type@npm:^0.1.0": version: 0.1.0 resolution: "get-package-type@npm:0.1.0" @@ -23854,6 +23741,13 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.4.0": + version: 4.4.0 + resolution: "get-tsconfig@npm:4.4.0" + checksum: e193558b4f0c84c81ae9688cf5b9950dc0b341e44f91b002713fd0c37cfb73108e1cd9998ed540bcc423f193fde32cc58a15e99dd469f5158a2eb4a148611176 + languageName: node + linkType: hard + "getopts@npm:2.3.0": version: 2.3.0 resolution: "getopts@npm:2.3.0" @@ -23944,7 +23838,20 @@ __metadata: languageName: node linkType: hard -"glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.2, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0": +"glob@npm:8.0.3": + version: 8.0.3 + resolution: "glob@npm:8.0.3" + dependencies: + fs.realpath: ^1.0.0 + inflight: ^1.0.4 + inherits: 2 + minimatch: ^5.0.1 + once: ^1.3.0 + checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 + languageName: node + linkType: hard + +"glob@npm:^7.0.0, glob@npm:^7.1.1, glob@npm:^7.1.3, glob@npm:^7.1.4, glob@npm:^7.1.6, glob@npm:^7.1.7, glob@npm:^7.2.0": version: 7.2.3 resolution: "glob@npm:7.2.3" dependencies: @@ -23959,15 +23866,15 @@ __metadata: linkType: hard "glob@npm:^8.0.0, glob@npm:^8.0.1, glob@npm:^8.0.3": - version: 8.0.3 - resolution: "glob@npm:8.0.3" + version: 8.1.0 + resolution: "glob@npm:8.1.0" dependencies: fs.realpath: ^1.0.0 inflight: ^1.0.4 inherits: 2 minimatch: ^5.0.1 once: ^1.3.0 - checksum: 50bcdea19d8e79d8de5f460b1939ffc2b3299eac28deb502093fdca22a78efebc03e66bf54f0abc3d3d07d8134d19a32850288b7440d77e072aa55f9d33b18c5 + checksum: 92fbea3221a7d12075f26f0227abac435de868dd0736a17170663783296d0dd8d3d532a5672b4488a439bf5d7fb85cdd07c11185d6cd39184f0385cbdfb86a47 languageName: node linkType: hard @@ -24014,16 +23921,6 @@ __metadata: languageName: node linkType: hard -"global@npm:~4.4.0": - version: 4.4.0 - resolution: "global@npm:4.4.0" - dependencies: - min-document: ^2.19.0 - process: ^0.11.10 - checksum: 9c057557c8f5a5bcfbeb9378ba4fe2255d04679452be504608dd5f13b54edf79f7be1db1031ea06a4ec6edd3b9f5f17d2d172fb47e6c69dae57fd84b7e72b77f - languageName: node - linkType: hard - "globals@npm:^11.1.0, globals@npm:^11.12.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -24063,17 +23960,12 @@ __metadata: languageName: node linkType: hard -"globby@npm:^7.1.1": - version: 7.1.1 - resolution: "globby@npm:7.1.1" +"good-listener@npm:^1.2.2": + version: 1.2.2 + resolution: "good-listener@npm:1.2.2" dependencies: - array-union: ^1.0.1 - dir-glob: ^2.0.0 - glob: ^7.1.2 - ignore: ^3.3.5 - pify: ^3.0.0 - slash: ^1.0.0 - checksum: f0eba08a08ae7c98149a4411661c0bf08c4717d81e6f355cf624fb01880b249737eb8e951bf86124cb3af8ea1c793c0a9d363ed5cdec99bb2c6b68f8a323025f + delegate: ^3.1.2 + checksum: f39fb82c4e41524f56104cfd2d7aef1a88e72f3f75139115fbdf98cc7d844e0c1b39218b2e83438c6188727bf904ed78c7f0f2feff67b32833bc3af7f0202b33 languageName: node linkType: hard @@ -24289,6 +24181,27 @@ __metadata: languageName: node linkType: hard +"graphql-voyager@npm:^1.0.0-rc.31": + version: 1.0.0-rc.31 + resolution: "graphql-voyager@npm:1.0.0-rc.31" + dependencies: + "@f/animate": ^1.0.1 + "@material-ui/core": ^3.9.3 + classnames: ^2.2.6 + clipboard: ^2.0.4 + commonmark: ^0.29.0 + lodash: ^4.17.10 + prop-types: ^15.7.2 + svg-pan-zoom: ^3.6.0 + viz.js: 2.1.2 + peerDependencies: + graphql: ">=14.0.0" + react: ">=15.4.2" + react-dom: ">=15.4.2" + checksum: 80e826eeb42d286540447723ea35a463a3a155287914a45e24b69f31617d8ca74c0c43bdaa64123bf4ed046e68f7e31feca7661c06288551040f3e5b96c48fed + languageName: node + linkType: hard + "graphql-ws@npm:5.11.2, graphql-ws@npm:^5.4.1, graphql-ws@npm:^5.9.0": version: 5.11.2 resolution: "graphql-ws@npm:5.11.2" @@ -24419,22 +24332,6 @@ __metadata: languageName: node linkType: hard -"has-binary2@npm:~1.0.2": - version: 1.0.3 - resolution: "has-binary2@npm:1.0.3" - dependencies: - isarray: 2.0.1 - checksum: 9183a617830b1f50b35961fbe39278cc43089cdac7c47a7f16aafb9d8190553fa360e5da34a479d6c988db40ea10aa02bca5abcb2451b5fc3924ae2f013ffdab - languageName: node - linkType: hard - -"has-cors@npm:1.1.0": - version: 1.1.0 - resolution: "has-cors@npm:1.1.0" - checksum: 549ce94113fd23895b22d71ade9809918577b8558cd4d701fe79045d8b1d58d87eba870260b28f6a3229be933a691c55653afd496d0fc52e98fd2ff577f01197 - languageName: node - linkType: hard - "has-flag@npm:^3.0.0": version: 3.0.0 resolution: "has-flag@npm:3.0.0" @@ -24611,19 +24508,14 @@ __metadata: languageName: node linkType: hard -"hogan.js@npm:^3.0.2": - version: 3.0.2 - resolution: "hogan.js@npm:3.0.2" - dependencies: - mkdirp: 0.3.0 - nopt: 1.0.10 - bin: - hulk: ./bin/hulk - checksum: c7bbff84faa9ca265c39f4a2100546ba0388fcc9c5bac8526f488592ce3fcaa042eba6ac25db277f4478ec3855b9bc28ce59acffbf6e8a28d45a17df7590c6aa +"hoist-non-react-statics@npm:^2.3.1": + version: 2.5.5 + resolution: "hoist-non-react-statics@npm:2.5.5" + checksum: ee2d05e5c7e1398ad84a15b0327f66bd78f38a8e0015e852f954b36434e32eb7e942d5357505020a3a1147f247b165bf1e69d72393e3accab67cafdafeb86230 languageName: node linkType: hard -"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": +"hoist-non-react-statics@npm:^3.0.0, hoist-non-react-statics@npm:^3.2.1, hoist-non-react-statics@npm:^3.3.0, hoist-non-react-statics@npm:^3.3.2": version: 3.3.2 resolution: "hoist-non-react-statics@npm:3.3.2" dependencies: @@ -24770,9 +24662,9 @@ __metadata: linkType: hard "http-cache-semantics@npm:^4.0.0, http-cache-semantics@npm:^4.1.0": - version: 4.1.0 - resolution: "http-cache-semantics@npm:4.1.0" - checksum: 974de94a81c5474be07f269f9fd8383e92ebb5a448208223bfb39e172a9dbc26feff250192ecc23b9593b3f92098e010406b0f24bd4d588d631f80214648ed42 + version: 4.1.1 + resolution: "http-cache-semantics@npm:4.1.1" + checksum: 83ac0bc60b17a3a36f9953e7be55e5c8f41acc61b22583060e8dedc9dd5e3607c823a88d0926f9150e571f90946835c7fe150732801010845c72cd8bbff1a236 languageName: node linkType: hard @@ -25057,26 +24949,10 @@ __metadata: languageName: node linkType: hard -"ignore@npm:^3.3.5": - version: 3.3.10 - resolution: "ignore@npm:3.3.10" - checksum: 23e8cc776e367b56615ab21b78decf973a35dfca5522b39d9b47643d8168473b0d1f18dd1321a1bab466a12ea11a2411903f3b21644f4d5461ee0711ec8678bd - languageName: node - linkType: hard - -"ignore@npm:^5.1.4, ignore@npm:^5.2.0": - version: 5.2.0 - resolution: "ignore@npm:5.2.0" - checksum: 6b1f926792d614f64c6c83da3a1f9c83f6196c2839aa41e1e32dd7b8d174cef2e329d75caabb62cb61ce9dc432f75e67d07d122a037312db7caa73166a1bdb77 - languageName: node - linkType: hard - -"image-size@npm:^0.7.2": - version: 0.7.5 - resolution: "image-size@npm:0.7.5" - bin: - image-size: bin/image-size.js - checksum: f88860c9d9b5c8ad00d3de9d6f5ba105bda5a5024bfb6b90559a075a4b838ed4f5d3cba14edf0f18fe5d75df596a172b52feca43848e11c34f31f4df2c88a011 +"ignore@npm:^5.1.4, ignore@npm:^5.2.0, ignore@npm:^5.2.4": + version: 5.2.4 + resolution: "ignore@npm:5.2.4" + checksum: 3d4c309c6006e2621659311783eaea7ebcd41fe4ca1d78c91c473157ad6666a57a2df790fe0d07a12300d9aac2888204d7be8d59f9aaf665b1c7fcdb432517ef languageName: node linkType: hard @@ -25169,6 +25045,15 @@ __metadata: languageName: node linkType: hard +"indefinite-observable@npm:^1.0.1": + version: 1.0.2 + resolution: "indefinite-observable@npm:1.0.2" + dependencies: + symbol-observable: 1.2.0 + checksum: 69a337967f48fca18989f9d68ad98c7220ed9b499bf00330ff72669a9583cb7f8e82f801da10720f720edf1313f427c77ce793350bdc413c952cec8ce112fc12 + languageName: node + linkType: hard + "indefinite-observable@npm:^2.0.1": version: 2.0.1 resolution: "indefinite-observable@npm:2.0.1" @@ -25185,13 +25070,6 @@ __metadata: languageName: node linkType: hard -"indexof@npm:0.0.1": - version: 0.0.1 - resolution: "indexof@npm:0.0.1" - checksum: 0fb04e8b147b8585d981a6df1564f25bb3678d6fa74e33e5cecc1464b10f78e15e8ef6bb688f135fe5c2844a128fac8a7831cbe5adc81fdcf12681b093dfcc25 - languageName: node - linkType: hard - "infer-owner@npm:^1.0.4": version: 1.0.4 resolution: "infer-owner@npm:1.0.4" @@ -25363,20 +25241,20 @@ __metadata: languageName: node linkType: hard -"ioredis@npm:^5.2.4": - version: 5.2.4 - resolution: "ioredis@npm:5.2.4" +"ioredis@npm:^5.3.0": + version: 5.3.0 + resolution: "ioredis@npm:5.3.0" dependencies: "@ioredis/commands": ^1.1.1 cluster-key-slot: ^1.1.0 debug: ^4.3.4 - denque: ^2.0.1 + denque: ^2.1.0 lodash.defaults: ^4.2.0 lodash.isarguments: ^3.1.0 redis-errors: ^1.2.0 redis-parser: ^3.0.0 standard-as-callback: ^2.1.0 - checksum: c3a7df407a41ae516bede8b6db853c568198b13fe4a4785f66be5cd541087121b9d45fb9a6b1b6a5fb668c29ce52ab4685642b994803bdfa0a35f794ea8ef7ae + checksum: d3a1fd8f1b5d9ce6d4b7dd083773dbe0936ea3d1f55f07d36daa3d0b59c4636e95640f90bc003a08bc900a9803307e7d7e191817a2c2970696e204dae6f3dc35 languageName: node linkType: hard @@ -25567,12 +25445,12 @@ __metadata: languageName: node linkType: hard -"is-core-module@npm:^2.1.0, is-core-module@npm:^2.8.1, is-core-module@npm:^2.9.0": - version: 2.10.0 - resolution: "is-core-module@npm:2.10.0" +"is-core-module@npm:^2.1.0, is-core-module@npm:^2.11.0, is-core-module@npm:^2.9.0": + version: 2.11.0 + resolution: "is-core-module@npm:2.11.0" dependencies: has: ^1.0.3 - checksum: 0f3f77811f430af3256fa7bbc806f9639534b140f8ee69476f632c3e1eb4e28a38be0b9d1b8ecf596179c841b53576129279df95e7051d694dac4ceb6f967593 + checksum: f96fd490c6b48eb4f6d10ba815c6ef13f410b0ba6f7eb8577af51697de523e5f2cd9de1c441b51d27251bf0e4aebc936545e33a5d26d5d51f28d25698d4a8bab languageName: node linkType: hard @@ -25638,13 +25516,6 @@ __metadata: languageName: node linkType: hard -"is-function@npm:^1.0.1": - version: 1.0.2 - resolution: "is-function@npm:1.0.2" - checksum: 7d564562e07b4b51359547d3ccc10fb93bb392fd1b8177ae2601ee4982a0ece86d952323fc172a9000743a3971f09689495ab78a1d49a9b14fc97a7e28521dc0 - languageName: node - linkType: hard - "is-generator-fn@npm:^2.0.0": version: 2.1.0 resolution: "is-generator-fn@npm:2.1.0" @@ -25935,6 +25806,13 @@ __metadata: languageName: node linkType: hard +"is-stream@npm:^1.0.1": + version: 1.1.0 + resolution: "is-stream@npm:1.1.0" + checksum: 063c6bec9d5647aa6d42108d4c59723d2bd4ae42135a2d4db6eadbd49b7ea05b750fd69d279e5c7c45cf9da753ad2c00d8978be354d65aa9f6bb434969c6a2ae + languageName: node + linkType: hard + "is-stream@npm:^2.0.0": version: 2.0.1 resolution: "is-stream@npm:2.0.1" @@ -26077,13 +25955,6 @@ __metadata: languageName: node linkType: hard -"isarray@npm:2.0.1": - version: 2.0.1 - resolution: "isarray@npm:2.0.1" - checksum: a0521973213019b82889f028a41528ce7a3f103a710357b6875066202ee5a1fa9ae4135f915f3be9160b8c7a3ae4b39b073b804da3b58fa6b72f8b4b1e3dfef5 - languageName: node - linkType: hard - "isarray@npm:^1.0.0, isarray@npm:~1.0.0": version: 1.0.0 resolution: "isarray@npm:1.0.0" @@ -26098,7 +25969,7 @@ __metadata: languageName: node linkType: hard -"isbinaryfile@npm:^4.0.10, isbinaryfile@npm:^4.0.8": +"isbinaryfile@npm:^4.0.10, isbinaryfile@npm:^4.0.8, isbinaryfile@npm:^4.0.8 <5": version: 4.0.10 resolution: "isbinaryfile@npm:4.0.10" checksum: a6b28db7e23ac7a77d3707567cac81356ea18bd602a4f21f424f862a31d0e7ab4f250759c98a559ece35ffe4d99f0d339f1ab884ffa9795172f632ab8f88e686 @@ -26137,6 +26008,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^2.1.1": + version: 2.2.1 + resolution: "isomorphic-fetch@npm:2.2.1" + dependencies: + node-fetch: ^1.0.1 + whatwg-fetch: ">=0.10.0" + checksum: bb5daa7c3785d6742f4379a81e55b549a469503f7c9bf9411b48592e86632cf5e8fe8ea878dba185c0f33eb7c510c23abdeb55aebfdf5d3c70f031ced68c5424 + languageName: node + linkType: hard + "isomorphic-fetch@npm:^3.0.0": version: 3.0.0 resolution: "isomorphic-fetch@npm:3.0.0" @@ -26847,20 +26728,6 @@ __metadata: languageName: node linkType: hard -"jimp@npm:^0.10.3": - version: 0.10.3 - resolution: "jimp@npm:0.10.3" - dependencies: - "@babel/runtime": ^7.7.2 - "@jimp/custom": ^0.10.3 - "@jimp/plugins": ^0.10.3 - "@jimp/types": ^0.10.3 - core-js: ^3.4.1 - regenerator-runtime: ^0.13.3 - checksum: ace7d9d76c2802e26392603c1fdfc988e7c468941ac127405f6752db9b5c89e1e27f202f96b764eede001ac245be0b46f52bb87eb91a42374027050568a7e816 - languageName: node - linkType: hard - "jju@npm:~1.4.0": version: 1.4.0 resolution: "jju@npm:1.4.0" @@ -26896,9 +26763,9 @@ __metadata: linkType: hard "jose@npm:^4.10.0, jose@npm:^4.6.0": - version: 4.11.2 - resolution: "jose@npm:4.11.2" - checksum: bc9b2271c31683603857b2fd2903afa29f826fffd4bcefc0845355ed86995def87eaadf502033207996109d76698c912e5a986e279326d9629321a50504fe466 + version: 4.11.4 + resolution: "jose@npm:4.11.4" + checksum: ab870d61e3721cfa1059b1a79cfc2fce06586c8afd4164362e1725934a635191bd938893ebdfac55a0811efbe9b6fd650bbf82d36d87e8a0b7cb4b0379328dab languageName: node linkType: hard @@ -26909,24 +26776,10 @@ __metadata: languageName: node linkType: hard -"jpeg-js@npm:^0.3.4": - version: 0.3.7 - resolution: "jpeg-js@npm:0.3.7" - checksum: 85a1ab09fe696fdd7b94ee077f1b8d1de307ffd7a83c0c45cc5cc56b0d7b47e561b92f6f090165648387875d674ba3076e3fedf395d5be4ef46760b6ebc804fe - languageName: node - linkType: hard - -"js-base64@npm:^2.5.1": - version: 2.6.4 - resolution: "js-base64@npm:2.6.4" - checksum: 5f4084078d6c46f8529741d110df84b14fac3276b903760c21fa8cc8521370d607325dfe1c1a9fbbeaae1ff8e602665aaeef1362427d8fef704f9e3659472ce8 - languageName: node - linkType: hard - "js-base64@npm:^3.6.0": - version: 3.7.4 - resolution: "js-base64@npm:3.7.4" - checksum: b8f6b10033b57bbb87fa34629f44bc665cb8b7bcba49610a29b296592616e633e9c186afaa02de5a54d60c08aaa36c8c8ad585db095a96fb331fe022f54792a2 + version: 3.7.5 + resolution: "js-base64@npm:3.7.5" + checksum: 67a78c8b1c47b73f1c6fba1957e9fe6fd9dc78ac93ac46cc2e43472dcb9cf150d126fb0e593192e88e0497354fa634d17d255add7cc6ee3c7b4d29870faa8e18 languageName: node linkType: hard @@ -27207,13 +27060,6 @@ __metadata: languageName: node linkType: hard -"json-parse-better-errors@npm:^1.0.1": - version: 1.0.2 - resolution: "json-parse-better-errors@npm:1.0.2" - checksum: ff2b5ba2a70e88fd97a3cb28c1840144c5ce8fae9cbeeddba15afa333a5c407cf0e42300cd0a2885dbb055227fe68d405070faad941beeffbfde9cf3b2c78c5d - languageName: node - linkType: hard - "json-parse-even-better-errors@npm:^2.3.0, json-parse-even-better-errors@npm:^2.3.1": version: 2.3.1 resolution: "json-parse-even-better-errors@npm:2.3.1" @@ -27495,6 +27341,46 @@ __metadata: languageName: node linkType: hard +"jss-camel-case@npm:^6.0.0": + version: 6.1.0 + resolution: "jss-camel-case@npm:6.1.0" + dependencies: + hyphenate-style-name: ^1.0.2 + peerDependencies: + jss: ^9.7.0 + checksum: f20ad892cddc30241b5127d648233b513ce57b2a6ed2f710cd995510f8d06e1418cd8f6e8e50dc6bd60b2e8a35159bfdeb40143938e0f2cdc19fd39279953789 + languageName: node + linkType: hard + +"jss-default-unit@npm:^8.0.2": + version: 8.0.2 + resolution: "jss-default-unit@npm:8.0.2" + peerDependencies: + jss: ^9.4.0 + checksum: 5277c5ccc3d56f5c137d6d65f0fc36d15eb22ef08b5949e887ee75e9f4f25197e25f2a99706b70a2828cee010fd8ed7484fb1bb2086fe327f2b24b1bdcc22abb + languageName: node + linkType: hard + +"jss-global@npm:^3.0.0": + version: 3.0.0 + resolution: "jss-global@npm:3.0.0" + peerDependencies: + jss: ^9.0.0 + checksum: e3fa80d8251ba5f183d9b0b4416d64e8f5d285cfdb595cc600daf1a1366b67bca573939bb71d98b6596bf4d6f2c95711cf53a358af3d63cd7945dbb434c6547b + languageName: node + linkType: hard + +"jss-nested@npm:^6.0.1": + version: 6.0.1 + resolution: "jss-nested@npm:6.0.1" + dependencies: + warning: ^3.0.0 + peerDependencies: + jss: ^9.0.0 + checksum: 437bdacc559be0b4b5bc1faa2bc77b5c6cf14733fefbf73a34bb7335786b9f08e4e79b3d73cf83b386959adcd8fa9d725877912e96d9a0921ed38baa1a69b8d9 + languageName: node + linkType: hard + "jss-plugin-camel-case@npm:^10.5.1": version: 10.6.0 resolution: "jss-plugin-camel-case@npm:10.6.0" @@ -27569,6 +27455,26 @@ __metadata: languageName: node linkType: hard +"jss-props-sort@npm:^6.0.0": + version: 6.0.0 + resolution: "jss-props-sort@npm:6.0.0" + peerDependencies: + jss: ^9.0.0 + checksum: 82a04f625a2f2b3a71b2fcb00b1ed0478137c83dc47c323d24c7ea88d08c9ea295b061f99cf264db133fef3435366d6da594724f7713e0415dc50b4eff2aeb53 + languageName: node + linkType: hard + +"jss-vendor-prefixer@npm:^7.0.0": + version: 7.0.0 + resolution: "jss-vendor-prefixer@npm:7.0.0" + dependencies: + css-vendor: ^0.3.8 + peerDependencies: + jss: ^9.0.0 + checksum: 8ec3608711833e79da7ccfffd8177e752f16093589fa28d3a24ec19e6588fbc6a07cc5cacf59ef4c111ab1d0fb72e07b88cb3444eb8c0c6ed4cc8107da984633 + languageName: node + linkType: hard + "jss@npm:10.6.0": version: 10.6.0 resolution: "jss@npm:10.6.0" @@ -27594,6 +27500,17 @@ __metadata: languageName: node linkType: hard +"jss@npm:^9.8.7": + version: 9.8.7 + resolution: "jss@npm:9.8.7" + dependencies: + is-in-browser: ^1.1.3 + symbol-observable: ^1.1.0 + warning: ^3.0.0 + checksum: ebb264cc893fb8c17a0277875947f8f72e01f5be991bf7f61a39abb861e1276e75ee402fdd2be9ce827af839a1a08bd81ad86a4e108d57fd7d7ebb2361837a53 + languageName: node + linkType: hard + "jsx-ast-utils@npm:^2.4.1 || ^3.0.0, jsx-ast-utils@npm:^3.3.3": version: 3.3.3 resolution: "jsx-ast-utils@npm:3.3.3" @@ -27925,6 +27842,25 @@ __metadata: languageName: node linkType: hard +"linguist-js@npm:^2.5.3": + version: 2.5.4 + resolution: "linguist-js@npm:2.5.4" + dependencies: + binary-extensions: ^2.2.0 + commander: ^9.5.0 + common-path-prefix: ^3.0.0 + cross-fetch: ^3.1.5 + ignore: ^5.2.4 + isbinaryfile: ^4.0.8 <5 + js-yaml: ^4.1.0 + node-cache: ^5.1.2 + bin: + linguist: bin/index.js + linguist-js: bin/index.js + checksum: 92ebfc45f0f17a056153068f7e939a3e6755307d3696673597e824011d83b698063241aa7d082a7f9059416efce85a49056f0a31b3158c25f79e273e2f51e309 + languageName: node + linkType: hard + "linkify-it@npm:^3.0.1": version: 3.0.2 resolution: "linkify-it@npm:3.0.2" @@ -27935,8 +27871,8 @@ __metadata: linkType: hard "lint-staged@npm:^13.0.0": - version: 13.1.0 - resolution: "lint-staged@npm:13.1.0" + version: 13.1.2 + resolution: "lint-staged@npm:13.1.2" dependencies: cli-truncate: ^3.1.0 colorette: ^2.0.19 @@ -27953,7 +27889,7 @@ __metadata: yaml: ^2.1.3 bin: lint-staged: bin/lint-staged.js - checksum: adf20c4ca9285c4a93b06598b970d71b04cfe58a1a4c9006f753b83e02c1c622d1866c32a4f1e7e29a98091c501eac3345f7678af247b4f97d5be88b3d8727c1 + checksum: f854ad5c88542b8f06e27f3b4046927a4f3d4a451a04e079526559d819a325762268f65bd2df7156bcc0cb5f531f621c42cdb824b403f537c78305adc9e56a54 languageName: node linkType: hard @@ -28020,22 +27956,6 @@ __metadata: languageName: node linkType: hard -"load-bmfont@npm:^1.3.1, load-bmfont@npm:^1.4.0": - version: 1.4.1 - resolution: "load-bmfont@npm:1.4.1" - dependencies: - buffer-equal: 0.0.1 - mime: ^1.3.4 - parse-bmfont-ascii: ^1.0.3 - parse-bmfont-binary: ^1.0.5 - parse-bmfont-xml: ^1.1.4 - phin: ^2.9.1 - xhr: ^2.0.1 - xtend: ^4.0.0 - checksum: 688d932fb0dc4c9333747736ccd926261f0b91734b7bdb6ff24f8659ef068a0f0b2278084b208851afac0beec79af7bd6664fe2ed5b6c5e1db88755fc25f785e - languageName: node - linkType: hard - "load-json-file@npm:^1.0.0": version: 1.1.0 resolution: "load-json-file@npm:1.1.0" @@ -28049,18 +27969,6 @@ __metadata: languageName: node linkType: hard -"load-json-file@npm:^4.0.0": - version: 4.0.0 - resolution: "load-json-file@npm:4.0.0" - dependencies: - graceful-fs: ^4.1.2 - parse-json: ^4.0.0 - pify: ^3.0.0 - strip-bom: ^3.0.0 - checksum: 8f5d6d93ba64a9620445ee9bde4d98b1eac32cf6c8c2d20d44abfa41a6945e7969456ab5f1ca2fb06ee32e206c9769a20eec7002fe290de462e8c884b6b8b356 - languageName: node - linkType: hard - "load-yaml-file@npm:^0.2.0": version: 0.2.0 resolution: "load-yaml-file@npm:0.2.0" @@ -28109,16 +28017,6 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^2.0.0": - version: 2.0.0 - resolution: "locate-path@npm:2.0.0" - dependencies: - p-locate: ^2.0.0 - path-exists: ^3.0.0 - checksum: 02d581edbbbb0fa292e28d96b7de36b5b62c2fa8b5a7e82638ebb33afa74284acf022d3b1e9ae10e3ffb7658fbc49163fcd5e76e7d1baaa7801c3e05a81da755 - languageName: node - linkType: hard - "locate-path@npm:^3.0.0": version: 3.0.0 resolution: "locate-path@npm:3.0.0" @@ -28350,7 +28248,7 @@ __metadata: languageName: node linkType: hard -"lodash@npm:4.17.21, lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.13, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0, lodash@npm:~4.17.0, lodash@npm:~4.17.15": +"lodash@npm:4.17.21, lodash@npm:^4.17.10, lodash@npm:^4.17.11, lodash@npm:^4.17.14, lodash@npm:^4.17.15, lodash@npm:^4.17.19, lodash@npm:^4.17.20, lodash@npm:^4.17.21, lodash@npm:^4.17.4, lodash@npm:^4.7.0, lodash@npm:~4.17.0, lodash@npm:~4.17.15": version: 4.17.21 resolution: "lodash@npm:4.17.21" checksum: eb835a2e51d381e561e508ce932ea50a8e5a68f4ebdd771ea240d3048244a8d13658acbd502cd4829768c56f2e16bdd4340b9ea141297d472517b83868e677f7 @@ -28508,6 +28406,13 @@ __metadata: languageName: node linkType: hard +"luxon@npm:^2.0.2": + version: 2.5.2 + resolution: "luxon@npm:2.5.2" + checksum: d8b671ffd2ff0b438af862ac11082a81b3aedd9f8b6a4ca636f944224f8dd381125cd4d274ca0a8aedcaf2cfca0fc2ca96fe4cc1b16a28b7af701afb95c0bff8 + languageName: node + linkType: hard + "luxon@npm:^3.0.0, luxon@npm:^3.2.1": version: 3.2.1 resolution: "luxon@npm:3.2.1" @@ -28907,7 +28812,7 @@ __metadata: languageName: node linkType: hard -"mdurl@npm:^1.0.0, mdurl@npm:^1.0.1": +"mdurl@npm:^1.0.0, mdurl@npm:^1.0.1, mdurl@npm:~1.0.1": version: 1.0.1 resolution: "mdurl@npm:1.0.1" checksum: 71731ecba943926bfbf9f9b51e28b5945f9411c4eda80894221b47cc105afa43ba2da820732b436f0798fd3edbbffcd1fc1415843c41a87fea08a41cc1e3d02b @@ -29019,13 +28924,6 @@ __metadata: languageName: node linkType: hard -"merge@npm:^1.2.1": - version: 1.2.1 - resolution: "merge@npm:1.2.1" - checksum: 2298c4fdcf64561f320b92338681f7ffcafafb579a6e294066ae3e7bd10ae25df363903d2f028072733b9f79a1f75d2b999aef98ad5d73de13641da39cda0913 - languageName: node - linkType: hard - "meros@npm:1.2.1, meros@npm:^1.1.4": version: 1.2.1 resolution: "meros@npm:1.2.1" @@ -29038,6 +28936,13 @@ __metadata: languageName: node linkType: hard +"meta-png@npm:1.0.5": + version: 1.0.5 + resolution: "meta-png@npm:1.0.5" + checksum: b8c92a696d5e78ef7f0ab53eb9039f108ee0c55cdac9d70e92e5fe2187b090176c0489426d58e594c7d1096481dac37913dd1265f28dfa39926db4d702a5e21d + languageName: node + linkType: hard + "methods@npm:^1.0.0, methods@npm:^1.1.2, methods@npm:~1.1.2": version: 1.1.2 resolution: "methods@npm:1.1.2" @@ -29427,7 +29332,7 @@ __metadata: languageName: node linkType: hard -"mime@npm:1.6.0, mime@npm:^1.3.4": +"mime@npm:1.6.0": version: 1.6.0 resolution: "mime@npm:1.6.0" bin: @@ -29489,15 +29394,6 @@ __metadata: languageName: node linkType: hard -"min-document@npm:^2.19.0": - version: 2.19.0 - resolution: "min-document@npm:2.19.0" - dependencies: - dom-walk: ^0.1.0 - checksum: da6437562ea2228041542a2384528e74e22d1daa1a4ec439c165abf0b9d8a63e17e3b8a6dc6e0c731845e85301198730426932a0e813d23f932ca668340c9623 - languageName: node - linkType: hard - "min-indent@npm:^1.0.0": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -29568,7 +29464,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.7": +"minimist@npm:>=1.2.2, minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.5, minimist@npm:^1.2.6, minimist@npm:^1.2.7": version: 1.2.7 resolution: "minimist@npm:1.2.7" checksum: 7346574a1038ca23c32e02252f603801f09384dd1d78b69a943a4e8c2c28730b80e96193882d3d3b22a063445f460e48316b29b8a25addca2d7e5e8f75478bec @@ -29713,24 +29609,6 @@ __metadata: languageName: node linkType: hard -"mkdirp@npm:0.3.0": - version: 0.3.0 - resolution: "mkdirp@npm:0.3.0" - checksum: 3ec9cda8bd89b64892728e5092bc79e88382e444d4bbde040c2fb8d7034dc70682cfdd729e93241fd5243d2397324c420ef68c717d806db51bf96c0fc80f4b1d - languageName: node - linkType: hard - -"mkdirp@npm:^0.5.1": - version: 0.5.5 - resolution: "mkdirp@npm:0.5.5" - dependencies: - minimist: ^1.2.5 - bin: - mkdirp: bin/cmd.js - checksum: 3bce20ea525f9477befe458ab85284b0b66c8dc3812f94155af07c827175948cdd8114852ac6c6d82009b13c1048c37f6d98743eb019651ee25c39acc8aabe7d - languageName: node - linkType: hard - "mkdirp@npm:^1.0.3, mkdirp@npm:^1.0.4": version: 1.0.4 resolution: "mkdirp@npm:1.0.4" @@ -29767,6 +29645,15 @@ __metadata: languageName: node linkType: hard +"move-file@npm:2.1.0": + version: 2.1.0 + resolution: "move-file@npm:2.1.0" + dependencies: + path-exists: ^4.0.0 + checksum: 2b92bbe047a302b593fcb2c0bf1131bb090ec80b3264569fc80d782c8ff829eecc13573943fa4d804c9747dec612ef2d1e84a5cfcf29cbc64a69ffcbb7ea09b3 + languageName: node + linkType: hard + "mri@npm:1.1.4": version: 1.1.4 resolution: "mri@npm:1.1.4" @@ -30073,6 +29960,15 @@ __metadata: languageName: node linkType: hard +"node-addon-api@npm:^5.0.0": + version: 5.1.0 + resolution: "node-addon-api@npm:5.1.0" + dependencies: + node-gyp: latest + checksum: 2508bd2d2981945406243a7bd31362fc7af8b70b8b4d65f869c61731800058fb818cc2fd36c8eac714ddd0e568cc85becf5e165cebbdf7b5024d5151bbc75ea1 + languageName: node + linkType: hard + "node-cache@npm:^5.1.2": version: 5.1.2 resolution: "node-cache@npm:5.1.2" @@ -30112,6 +30008,16 @@ __metadata: languageName: node linkType: hard +"node-fetch@npm:^1.0.1": + version: 1.7.3 + resolution: "node-fetch@npm:1.7.3" + dependencies: + encoding: ^0.1.11 + is-stream: ^1.0.1 + checksum: 3bb0528c05d541316ebe52770d71ee25a6dce334df4231fd55df41a644143e07f068637488c18a5b0c43f05041dbd3346752f9e19b50df50569a802484544d5b + languageName: node + linkType: hard + "node-fetch@npm:^2.6.0, node-fetch@npm:^2.6.1, node-fetch@npm:^2.6.5, node-fetch@npm:^2.6.7": version: 2.6.9 resolution: "node-fetch@npm:2.6.9" @@ -30258,17 +30164,6 @@ __metadata: languageName: node linkType: hard -"nopt@npm:1.0.10, nopt@npm:~1.0.10": - version: 1.0.10 - resolution: "nopt@npm:1.0.10" - dependencies: - abbrev: 1 - bin: - nopt: ./bin/nopt.js - checksum: f62575aceaa3be43f365bf37a596b89bbac2e796b001b6d2e2a85c2140a4e378ff919e2753ccba959c4fd344776fc88c29b393bc167fa939fb1513f126f4cd45 - languageName: node - linkType: hard - "nopt@npm:^5.0.0": version: 5.0.0 resolution: "nopt@npm:5.0.0" @@ -30291,6 +30186,17 @@ __metadata: languageName: node linkType: hard +"nopt@npm:~1.0.10": + version: 1.0.10 + resolution: "nopt@npm:1.0.10" + dependencies: + abbrev: 1 + bin: + nopt: ./bin/nopt.js + checksum: f62575aceaa3be43f365bf37a596b89bbac2e796b001b6d2e2a85c2140a4e378ff919e2753ccba959c4fd344776fc88c29b393bc167fa939fb1513f126f4cd45 + languageName: node + linkType: hard + "normalize-package-data@npm:^2.3.2, normalize-package-data@npm:^2.5.0": version: 2.5.0 resolution: "normalize-package-data@npm:2.5.0" @@ -30319,6 +30225,13 @@ __metadata: languageName: node linkType: hard +"normalize-scroll-left@npm:^0.1.2": + version: 0.1.2 + resolution: "normalize-scroll-left@npm:0.1.2" + checksum: 817d5a659ba6f14f458cd03a5a0f98ec2a9d7e6c63c160f2db164827b87bb77c0237752890a18d11054fe628907493c5f4d8914907585ec9fbba3de26d2a6815 + languageName: node + linkType: hard + "normalize-url@npm:^6.0.1": version: 6.1.0 resolution: "normalize-url@npm:6.1.0" @@ -30450,7 +30363,7 @@ __metadata: languageName: node linkType: hard -"npmlog@npm:^4.0.1, npmlog@npm:^4.1.2": +"npmlog@npm:^4.1.2": version: 4.1.2 resolution: "npmlog@npm:4.1.2" dependencies: @@ -30637,7 +30550,7 @@ __metadata: languageName: node linkType: hard -"object.values@npm:^1.1.5, object.values@npm:^1.1.6": +"object.values@npm:^1.1.6": version: 1.1.6 resolution: "object.values@npm:1.1.6" dependencies: @@ -30687,13 +30600,6 @@ __metadata: languageName: node linkType: hard -"omggif@npm:^1.0.9": - version: 1.0.10 - resolution: "omggif@npm:1.0.10" - checksum: 15102e46b6fa0fba32d7e948f702623cdc3cdcdfd64b2d33c6e29a61f366ffd0f250da55d66f5217dce5b93ba9c67763fa998652791a5c7f2201a3bde2c4db45 - languageName: node - linkType: hard - "on-finished@npm:2.4.1, on-finished@npm:^2.3.0": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -30933,15 +30839,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^1.1.0": - version: 1.3.0 - resolution: "p-limit@npm:1.3.0" - dependencies: - p-try: ^1.0.0 - checksum: 281c1c0b8c82e1ac9f81acd72a2e35d402bf572e09721ce5520164e9de07d8274451378a3470707179ad13240535558f4b277f02405ad752e08c7d5b0d54fbfd - languageName: node - linkType: hard - "p-limit@npm:^2.0.0, p-limit@npm:^2.2.0": version: 2.3.0 resolution: "p-limit@npm:2.3.0" @@ -30960,15 +30857,6 @@ __metadata: languageName: node linkType: hard -"p-locate@npm:^2.0.0": - version: 2.0.0 - resolution: "p-locate@npm:2.0.0" - dependencies: - p-limit: ^1.1.0 - checksum: e2dceb9b49b96d5513d90f715780f6f4972f46987dc32a0e18bc6c3fc74a1a5d73ec5f81b1398af5e58b99ea1ad03fd41e9181c01fa81b4af2833958696e3081 - languageName: node - linkType: hard - "p-locate@npm:^3.0.0": version: 3.0.0 resolution: "p-locate@npm:3.0.0" @@ -31051,13 +30939,6 @@ __metadata: languageName: node linkType: hard -"p-try@npm:^1.0.0": - version: 1.0.0 - resolution: "p-try@npm:1.0.0" - checksum: 3b5303f77eb7722144154288bfd96f799f8ff3e2b2b39330efe38db5dd359e4fb27012464cd85cb0a76e9b7edd1b443568cb3192c22e7cffc34989df0bafd605 - languageName: node - linkType: hard - "p-try@npm:^2.0.0": version: 2.2.0 resolution: "p-try@npm:2.2.0" @@ -31101,7 +30982,7 @@ __metadata: languageName: node linkType: hard -"pako@npm:^1.0.10, pako@npm:^1.0.5, pako@npm:~1.0.5": +"pako@npm:^1.0.10, pako@npm:~1.0.5": version: 1.0.11 resolution: "pako@npm:1.0.11" checksum: 1be2bfa1f807608c7538afa15d6f25baa523c30ec870a3228a89579e474a4d992f4293859524e46d5d87fd30fa17c5edf34dbef0671251d9749820b488660b16 @@ -31148,30 +31029,6 @@ __metadata: languageName: node linkType: hard -"parse-bmfont-ascii@npm:^1.0.3": - version: 1.0.6 - resolution: "parse-bmfont-ascii@npm:1.0.6" - checksum: de3f6671f183c3e9d64bb4812b0407693b5fd0d24e9d16b2e106bb9eef809d64a6cc061f39ca29bb10c5c2e47e241e91b7aeefa587391fff7ccb27ab9db5012e - languageName: node - linkType: hard - -"parse-bmfont-binary@npm:^1.0.5": - version: 1.0.6 - resolution: "parse-bmfont-binary@npm:1.0.6" - checksum: ca37fb1e92f5941fddc5342b45857fafd27f00d2bd5fa44dd504bec6faeab97536c95ad45260c2dd5fc4c63de71e525663d3cdac09d038cbca803d97c669add5 - languageName: node - linkType: hard - -"parse-bmfont-xml@npm:^1.1.4": - version: 1.1.4 - resolution: "parse-bmfont-xml@npm:1.1.4" - dependencies: - xml-parse-from-string: ^1.0.0 - xml2js: ^0.4.5 - checksum: 879e5435be44f22b8c4934e2e1d2754a6d90a9ddb16309360daff965e1428d877b673f3d1fafaab4fef437c912a0db9f85545e0dd375ec62df7d4d328450d257 - languageName: node - linkType: hard - "parse-conflict-json@npm:^2.0.1": version: 2.0.1 resolution: "parse-conflict-json@npm:2.0.1" @@ -31222,13 +31079,6 @@ __metadata: languageName: node linkType: hard -"parse-headers@npm:^2.0.0": - version: 2.0.4 - resolution: "parse-headers@npm:2.0.4" - checksum: 29519ac013e100c11a67d0fc64eb33ae86523abf547f71dba36d484dcd16a2835dd11f31303f4ded27c40133dca5a5fe4d15b77f49091e470a6f74a023c59c4a - languageName: node - linkType: hard - "parse-json@npm:^2.2.0": version: 2.2.0 resolution: "parse-json@npm:2.2.0" @@ -31238,16 +31088,6 @@ __metadata: languageName: node linkType: hard -"parse-json@npm:^4.0.0": - version: 4.0.0 - resolution: "parse-json@npm:4.0.0" - dependencies: - error-ex: ^1.3.1 - json-parse-better-errors: ^1.0.1 - checksum: 0fe227d410a61090c247e34fa210552b834613c006c2c64d9a05cfe9e89cf8b4246d1246b1a99524b53b313e9ac024438d0680f67e33eaed7e6f38db64cfe7b5 - languageName: node - linkType: hard - "parse-json@npm:^5.0.0, parse-json@npm:^5.2.0": version: 5.2.0 resolution: "parse-json@npm:5.2.0" @@ -31260,13 +31100,6 @@ __metadata: languageName: node linkType: hard -"parse-package-name@npm:^0.1.0": - version: 0.1.0 - resolution: "parse-package-name@npm:0.1.0" - checksum: 7d69b1deeaf82d5a59536c5a8772c8db003b1415dd2465073343dfab5caf90917ecfffdd71d1c004f5cf963c3881ccb8899d0ff820eac705a272e817e246de14 - languageName: node - linkType: hard - "parse-path@npm:^7.0.0": version: 7.0.0 resolution: "parse-path@npm:7.0.0" @@ -31301,20 +31134,6 @@ __metadata: languageName: node linkType: hard -"parseqs@npm:0.0.6": - version: 0.0.6 - resolution: "parseqs@npm:0.0.6" - checksum: 7fc4ff4ba59764060bb8529875f6d4313056ea6939ff579b22dd7bd6f6033035e1fd2d6a559ab48ef0a7fa29a9d7731c982bfd1594e9115141fe1c328485ce9e - languageName: node - linkType: hard - -"parseuri@npm:0.0.6": - version: 0.0.6 - resolution: "parseuri@npm:0.0.6" - checksum: fa430e40f0c75293a28e5f1023da5f51a5038d5e34c48c517b0d5187143f6bcc67d3091a062b68765db4a22757e488c7d15854f9d1921f2c2b9afa5ca0629a84 - languageName: node - linkType: hard - "parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" @@ -31553,7 +31372,7 @@ __metadata: languageName: node linkType: hard -"path-is-inside@npm:1.0.2, path-is-inside@npm:^1.0.2": +"path-is-inside@npm:1.0.2": version: 1.0.2 resolution: "path-is-inside@npm:1.0.2" checksum: 0b5b6c92d3018b82afb1f74fe6de6338c4c654de4a96123cb343f2b747d5606590ac0c890f956ed38220a4ab59baddfd7b713d78a62d240b20b14ab801fa02cb @@ -31652,15 +31471,6 @@ __metadata: languageName: node linkType: hard -"path-type@npm:^3.0.0": - version: 3.0.0 - resolution: "path-type@npm:3.0.0" - dependencies: - pify: ^3.0.0 - checksum: 735b35e256bad181f38fa021033b1c33cfbe62ead42bb2222b56c210e42938eecb272ae1949f3b6db4ac39597a61b44edd8384623ec4d79bfdc9a9c0f12537a6 - languageName: node - linkType: hard - "path-type@npm:^4.0.0": version: 4.0.0 resolution: "path-type@npm:4.0.0" @@ -31805,10 +31615,10 @@ __metadata: languageName: node linkType: hard -"phin@npm:^2.9.1": - version: 2.9.3 - resolution: "phin@npm:2.9.3" - checksum: 7e2abd7be74a54eb7be92dccb1d7a019725c8adaa79ac22a38f25220f9a859393e654ea753a559d326aed7bbc966fadac88270cc8c39d78896f7784219560c47 +"photoswipe@npm:^5.3.5": + version: 5.3.5 + resolution: "photoswipe@npm:5.3.5" + checksum: bd3eafebffc114210b8e5403746b48a9bfe436cca05a65f8d5efd6eacea785a51f0f124d7326c8f651d754d76124c9768a920383843c4a619da582a86e535b27 languageName: node linkType: hard @@ -31842,13 +31652,6 @@ __metadata: languageName: node linkType: hard -"pify@npm:^3.0.0": - version: 3.0.0 - resolution: "pify@npm:3.0.0" - checksum: 6cdcbc3567d5c412450c53261a3f10991665d660961e06605decf4544a61a97a54fefe70a68d5c37080ff9d6f4cf51444c90198d1ba9f9309a6c0d6e9f5c4fde - languageName: node - linkType: hard - "pify@npm:^4.0.1": version: 4.0.1 resolution: "pify@npm:4.0.1" @@ -31928,14 +31731,14 @@ __metadata: languageName: node linkType: hard -"pixelmatch@npm:^4.0.2": - version: 4.0.2 - resolution: "pixelmatch@npm:4.0.2" +"pixelmatch@npm:5.3.0": + version: 5.3.0 + resolution: "pixelmatch@npm:5.3.0" dependencies: - pngjs: ^3.0.0 + pngjs: ^6.0.0 bin: pixelmatch: bin/pixelmatch - checksum: 9c5c1329001938cae6d01e2bb84a909ba767f8256bcafc075422cea2a4dbaa8bebd44fceaa4b4ce7cdc36d11f20d4f1ba0cf669851d5649b32d8d1d27e4f5a36 + checksum: f542713d89536551181ad9ddb666a1792ba00a8632d831093232a075cb3ccac05856e7a453ed7d0a41aaef64dcb5962e8ae5cbe646dd2761790d8ee51b0a0743 languageName: node linkType: hard @@ -31987,10 +31790,10 @@ __metadata: languageName: node linkType: hard -"pngjs@npm:^3.0.0, pngjs@npm:^3.3.3": - version: 3.4.0 - resolution: "pngjs@npm:3.4.0" - checksum: 8bd40bd698abd16b72c97b85cb858c80894fbedc76277ce72a784aa441e14795d45d9856e97333ca469b34b67528860ffc8a7317ca6beea349b645366df00bcd +"pngjs@npm:6.0.0, pngjs@npm:^6.0.0": + version: 6.0.0 + resolution: "pngjs@npm:6.0.0" + checksum: ab6c285086060087097eab9fe6b5a528a24f9e79c03dea2b4fd6264ed4fdb5beff4a3257eeeaf2a9dc18249b539609c2a4e4013c567164a1f6b5ba2c974d5ecb languageName: node linkType: hard @@ -32001,6 +31804,13 @@ __metadata: languageName: node linkType: hard +"popper.js@npm:^1.14.1": + version: 1.16.1 + resolution: "popper.js@npm:1.16.1" + checksum: c56ae5001ec50a77ee297a8061a0221d99d25c7348d2e6bcd3e45a0d0f32a1fd81bca29d46cb0d4bdf13efb77685bd6a0ce93f9eb3c608311a461f945fffedbe + languageName: node + linkType: hard + "postcss-calc@npm:^8.0.0": version: 8.0.0 resolution: "postcss-calc@npm:8.0.0" @@ -32410,13 +32220,13 @@ __metadata: linkType: hard "postcss@npm:^8.1.0, postcss@npm:^8.4.19": - version: 8.4.20 - resolution: "postcss@npm:8.4.20" + version: 8.4.21 + resolution: "postcss@npm:8.4.21" dependencies: nanoid: ^3.3.4 picocolors: ^1.0.0 source-map-js: ^1.0.2 - checksum: 1a5609ea1c1b204f9c2974a0019ae9eef2d99bf645c2c9aac675166c4cb1005be7b5e2ba196160bc771f5d9ac896ed883f236f888c891e835e59d28fff6651aa + checksum: e39ac60ccd1542d4f9d93d894048aac0d686b3bb38e927d8386005718e6793dbbb46930f0a523fe382f1bbd843c6d980aaea791252bf5e176180e5a4336d9679 languageName: node linkType: hard @@ -32450,9 +32260,9 @@ __metadata: languageName: node linkType: hard -"prebuild-install@npm:^7.1.0": - version: 7.1.0 - resolution: "prebuild-install@npm:7.1.0" +"prebuild-install@npm:^7.1.0, prebuild-install@npm:^7.1.1": + version: 7.1.1 + resolution: "prebuild-install@npm:7.1.1" dependencies: detect-libc: ^2.0.0 expand-template: ^2.0.3 @@ -32461,7 +32271,6 @@ __metadata: mkdirp-classic: ^0.5.3 napi-build-utils: ^1.0.1 node-abi: ^3.3.0 - npmlog: ^4.0.1 pump: ^3.0.0 rc: ^1.2.7 simple-get: ^4.0.0 @@ -32469,7 +32278,7 @@ __metadata: tunnel-agent: ^0.6.0 bin: prebuild-install: bin.js - checksum: 204f2d89c6d6179fa1039036514aa72f7d0b537e421ef72c40840286e318f41489f00f22c6acc725cce6e10d43825b69dcabeaadfc917db781c58cd56fc25f90 + checksum: dbf96d0146b6b5827fc8f67f72074d2e19c69628b9a7a0a17d0fad1bf37e9f06922896972e074197fc00a52eae912993e6ef5a0d471652f561df5cb516f3f467 languageName: node linkType: hard @@ -32506,21 +32315,12 @@ __metadata: languageName: node linkType: hard -"prettier@npm:^1.16.4": - version: 1.19.1 - resolution: "prettier@npm:1.19.1" - bin: - prettier: ./bin-prettier.js - checksum: bc78219e0f8173a808f4c6c8e0a137dd8ebd4fbe013e63fe1a37a82b48612f17b8ae8e18a992adf802ee2cf7428f14f084e7c2846ca5759cf4013c6e54810e1f - languageName: node - linkType: hard - "prettier@npm:^2.2.1, prettier@npm:^2.7.1": - version: 2.8.1 - resolution: "prettier@npm:2.8.1" + version: 2.8.4 + resolution: "prettier@npm:2.8.4" bin: prettier: bin-prettier.js - checksum: 4f21a0f1269f76fb36f54e9a8a1ea4c11e27478958bf860661fb4b6d7ac69aac1581f8724fa98ea3585e56d42a2ea317a17ff6e3324f40cb11ff9e20b73785cc + checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e languageName: node linkType: hard @@ -32685,7 +32485,7 @@ __metadata: languageName: node linkType: hard -"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": +"prop-types@npm:^15.0.0, prop-types@npm:^15.5.10, prop-types@npm:^15.5.7, prop-types@npm:^15.5.8, prop-types@npm:^15.6.0, prop-types@npm:^15.6.2, prop-types@npm:^15.7.2, prop-types@npm:^15.8.1": version: 15.8.1 resolution: "prop-types@npm:15.8.1" dependencies: @@ -33263,6 +33063,19 @@ __metadata: languageName: node linkType: hard +"react-event-listener@npm:^0.6.2": + version: 0.6.6 + resolution: "react-event-listener@npm:0.6.6" + dependencies: + "@babel/runtime": ^7.2.0 + prop-types: ^15.6.0 + warning: ^4.0.1 + peerDependencies: + react: ^16.3.0 + checksum: 0287e0ae8cbf0a4c03889ffc2b745f5494b3edea0f4667357d709f5646dd78c7289ce305b6f473ec6ac5789f8a937c86b7e543d0249f0bf787bccdb1eab7ecd0 + languageName: node + linkType: hard + "react-fast-compare@npm:^3.1.1": version: 3.2.0 resolution: "react-fast-compare@npm:3.2.0" @@ -33303,15 +33116,6 @@ __metadata: languageName: node linkType: hard -"react-icons@npm:^4.4.0": - version: 4.4.0 - resolution: "react-icons@npm:4.4.0" - peerDependencies: - react: "*" - checksum: dd93a1dcc8ac8a3cf46a2b33e80b586fa967331fa5fcb90d061abf276155a8363a331643e203d67cb9bab4261d00a757da854bcce1fce2c6e4258bf3e33f711b - languageName: node - linkType: hard - "react-immutable-proptypes@npm:2.2.0": version: 2.2.0 resolution: "react-immutable-proptypes@npm:2.2.0" @@ -33343,7 +33147,7 @@ __metadata: languageName: node linkType: hard -"react-is@npm:^16.10.2, react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.7.0, react-is@npm:^16.8.0, react-is@npm:^16.8.6, react-is@npm:^16.9.0": +"react-is@npm:^16.10.2, react-is@npm:^16.12.0, react-is@npm:^16.13.1, react-is@npm:^16.6.3, react-is@npm:^16.7.0, react-is@npm:^16.8.0, react-is@npm:^16.8.6, react-is@npm:^16.9.0": version: 16.13.1 resolution: "react-is@npm:16.13.1" checksum: f7a19ac3496de32ca9ae12aa030f00f14a3d45374f1ceca0af707c831b2a6098ef0d6bdae51bd437b0a306d7f01d4677fcc8de7c0d331eb47ad0f46130e53c5f @@ -33364,7 +33168,7 @@ __metadata: languageName: node linkType: hard -"react-lifecycles-compat@npm:^3.0.4": +"react-lifecycles-compat@npm:^3.0.2, react-lifecycles-compat@npm:^3.0.4": version: 3.0.4 resolution: "react-lifecycles-compat@npm:3.0.4" checksum: a904b0fc0a8eeb15a148c9feb7bc17cec7ef96e71188280061fc340043fd6d8ee3ff233381f0e8f95c1cf926210b2c4a31f38182c8f35ac55057e453d6df204f @@ -33565,7 +33369,7 @@ __metadata: languageName: node linkType: hard -"react-transition-group@npm:2.9.0": +"react-transition-group@npm:2.9.0, react-transition-group@npm:^2.2.1": version: 2.9.0 resolution: "react-transition-group@npm:2.9.0" dependencies: @@ -33877,6 +33681,22 @@ __metadata: languageName: node linkType: hard +"recompose@npm:0.28.0 - 0.30.0": + version: 0.30.0 + resolution: "recompose@npm:0.30.0" + dependencies: + "@babel/runtime": ^7.0.0 + change-emitter: ^0.1.2 + fbjs: ^0.8.1 + hoist-non-react-statics: ^2.3.1 + react-lifecycles-compat: ^3.0.2 + symbol-observable: ^1.0.4 + peerDependencies: + react: ^0.14.0 || ^15.0.0 || ^16.0.0 + checksum: 18e58252336d0628b22db1e38407d32e836648e6d5c9453ba37c9f8030138b3429ee3952b053a13b60311f8b60893b207a761466bb293083542db0cf317b7a41 + languageName: node + linkType: hard + "recursive-readdir@npm:^2.2.2": version: 2.2.3 resolution: "recursive-readdir@npm:2.2.3" @@ -33988,7 +33808,7 @@ __metadata: languageName: node linkType: hard -"regenerator-runtime@npm:^0.13.11, regenerator-runtime@npm:^0.13.3, regenerator-runtime@npm:^0.13.4": +"regenerator-runtime@npm:^0.13.11, regenerator-runtime@npm:^0.13.4": version: 0.13.11 resolution: "regenerator-runtime@npm:0.13.11" checksum: 27481628d22a1c4e3ff551096a683b424242a216fee44685467307f14d58020af1e19660bf2e26064de946bad7eff28950eae9f8209d55723e2d9351e632bbb4 @@ -34319,7 +34139,7 @@ __metadata: languageName: node linkType: hard -"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.0, resolve@npm:~1.22.1": +"resolve@npm:^1.1.6, resolve@npm:^1.10.0, resolve@npm:^1.14.2, resolve@npm:^1.19.0, resolve@npm:^1.20.0, resolve@npm:^1.22.1, resolve@npm:~1.22.1": version: 1.22.1 resolution: "resolve@npm:1.22.1" dependencies: @@ -34364,7 +34184,7 @@ __metadata: languageName: node linkType: hard -"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.0#~builtin, resolve@patch:resolve@~1.22.1#~builtin": +"resolve@patch:resolve@^1.1.6#~builtin, resolve@patch:resolve@^1.10.0#~builtin, resolve@patch:resolve@^1.14.2#~builtin, resolve@patch:resolve@^1.19.0#~builtin, resolve@patch:resolve@^1.20.0#~builtin, resolve@patch:resolve@^1.22.1#~builtin, resolve@patch:resolve@~1.22.1#~builtin": version: 1.22.1 resolution: "resolve@patch:resolve@npm%3A1.22.1#~builtin::version=1.22.1&hash=07638b" dependencies: @@ -34826,15 +34646,6 @@ __metadata: languageName: node linkType: hard -"sanitize-filename@npm:^1.6.1": - version: 1.6.3 - resolution: "sanitize-filename@npm:1.6.3" - dependencies: - truncate-utf8-bytes: ^1.0.0 - checksum: aa733c012b7823cf65730603cf3b503c641cee6b239771d3164ca482f22d81a50e434a713938d994071db18e4202625669cc56bccc9d13d818b4c983b5f47fde - languageName: node - linkType: hard - "sax@npm:1.2.1": version: 1.2.1 resolution: "sax@npm:1.2.1" @@ -34956,6 +34767,13 @@ __metadata: languageName: node linkType: hard +"select@npm:^1.1.2": + version: 1.1.2 + resolution: "select@npm:1.1.2" + checksum: 4346151e94f226ea6131e44e68e6d837f3fdee64831b756dd657cc0b02f4cb5107f867cb34a1d1216ab7737d0bf0645d44546afb030bbd8d64e891f5e4c4814e + languageName: node + linkType: hard + "selfsigned@npm:^2.0.0, selfsigned@npm:^2.1.1": version: 2.1.1 resolution: "selfsigned@npm:2.1.1" @@ -35206,6 +35024,23 @@ __metadata: languageName: node linkType: hard +"sharp@npm:0.31.2": + version: 0.31.2 + resolution: "sharp@npm:0.31.2" + dependencies: + color: ^4.2.3 + detect-libc: ^2.0.1 + node-addon-api: ^5.0.0 + node-gyp: latest + prebuild-install: ^7.1.1 + semver: ^7.3.8 + simple-get: ^4.0.1 + tar-fs: ^2.1.1 + tunnel-agent: ^0.6.0 + checksum: 076717b7a073ea47bb522ff2931b74b6608daeb6f7ae334e4848d47fdf4d23bcb18cd49044fd5fb27ef27a1a4aa87d141894d67d1c4bb15a6e2e63cf4dbe329e + languageName: node + linkType: hard + "shebang-command@npm:^1.2.0": version: 1.2.0 resolution: "shebang-command@npm:1.2.0" @@ -35363,13 +35198,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^1.0.0": - version: 1.0.0 - resolution: "slash@npm:1.0.0" - checksum: 4b6e21b1fba6184a7e2efb1dd173f692d8a845584c1bbf9dc818ff86f5a52fc91b413008223d17cc684604ee8bb9263a420b1182027ad9762e35388434918860 - languageName: node - linkType: hard - "slash@npm:^2.0.0": version: 2.0.0 resolution: "slash@npm:2.0.0" @@ -35416,6 +35244,13 @@ __metadata: languageName: node linkType: hard +"slugify@npm:^1.6.4": + version: 1.6.5 + resolution: "slugify@npm:1.6.5" + checksum: a955a1b600201030f4c1daa9bb74a17d4402a0693fc40978bbd17e44e64fd72dad3bac4037422aa8aed55b5170edd57f3f4cd8f59ba331f5cf0f10f1a7795609 + languageName: node + linkType: hard + "smart-buffer@npm:^4.2.0": version: 4.2.0 resolution: "smart-buffer@npm:4.2.0" @@ -35449,68 +35284,6 @@ __metadata: languageName: node linkType: hard -"socket.io-adapter@npm:~1.1.0": - version: 1.1.2 - resolution: "socket.io-adapter@npm:1.1.2" - checksum: 8e18df7f8c471001b65e43542c5c743b63f041781bca097b4a407dd199dd757c14c0d470992f84923f48e75729a2ac0ae634fec820f20881cadae8a495bf68e9 - languageName: node - linkType: hard - -"socket.io-client@npm:2.5.0, socket.io-client@npm:^2.2.0": - version: 2.5.0 - resolution: "socket.io-client@npm:2.5.0" - dependencies: - backo2: 1.0.2 - component-bind: 1.0.0 - component-emitter: ~1.3.0 - debug: ~3.1.0 - engine.io-client: ~3.5.0 - has-binary2: ~1.0.2 - indexof: 0.0.1 - parseqs: 0.0.6 - parseuri: 0.0.6 - socket.io-parser: ~3.3.0 - to-array: 0.1.4 - checksum: a33accaf02c4a4c47ac9b09018fa4b9fa67a648a0cc92602e520ec884520050796786eb0df78fc1a9bf51e7b3d7b50ba4ffd3bda6e44df2c253a77100124669c - languageName: node - linkType: hard - -"socket.io-parser@npm:~3.3.0": - version: 3.3.3 - resolution: "socket.io-parser@npm:3.3.3" - dependencies: - component-emitter: ~1.3.0 - debug: ~3.1.0 - isarray: 2.0.1 - checksum: 6cf464e324e4207811dbf994822c0aa18ba01d615fbed8e3f069927e0e402fdc9b3ac3d2657f18d481fb8c584b1fd40152ced15b3686982a51ec843c6a76dbc2 - languageName: node - linkType: hard - -"socket.io-parser@npm:~3.4.0": - version: 3.4.1 - resolution: "socket.io-parser@npm:3.4.1" - dependencies: - component-emitter: 1.2.1 - debug: ~4.1.0 - isarray: 2.0.1 - checksum: f8bac61298375680aceb6c72ffbb47a2d950daa05e01a3b67d9330945073ae01e35aa131dcddd7e0b354ca302c51742bebaae57a50c188d4c1fe8af9461aa38d - languageName: node - linkType: hard - -"socket.io@npm:^2.2.0": - version: 2.5.0 - resolution: "socket.io@npm:2.5.0" - dependencies: - debug: ~4.1.0 - engine.io: ~3.6.0 - has-binary2: ~1.0.2 - socket.io-adapter: ~1.1.0 - socket.io-client: 2.5.0 - socket.io-parser: ~3.4.0 - checksum: 6f20a413352cb0a8bb13063249fe425c81fb54beca1bdd71221262305cf21232e6a37fdd4a232b5695dbd4d3204281a9313829c9c373c5ba01c5bd7218edd094 - languageName: node - linkType: hard - "sockjs@npm:^0.3.24": version: 0.3.24 resolution: "sockjs@npm:0.3.24" @@ -35588,7 +35361,7 @@ __metadata: languageName: node linkType: hard -"source-map-support@npm:^0.5.10, source-map-support@npm:^0.5.16, source-map-support@npm:~0.5.20": +"source-map-support@npm:^0.5.16, source-map-support@npm:^0.5.21, source-map-support@npm:~0.5.20": version: 0.5.21 resolution: "source-map-support@npm:0.5.21" dependencies: @@ -36147,6 +35920,13 @@ __metadata: languageName: node linkType: hard +"string.prototype.repeat@npm:^0.2.0": + version: 0.2.0 + resolution: "string.prototype.repeat@npm:0.2.0" + checksum: 5127ce1b5bce45258905877048ac28bae71be18f1c3421011bd3979cfbcacf9c16f6b7f42daa3b999c9d90f5da39bd7136982c8e64439f02666dd6c0778ed262 + languageName: node + linkType: hard + "string.prototype.trimend@npm:^1.0.5": version: 1.0.5 resolution: "string.prototype.trimend@npm:1.0.5" @@ -36494,6 +36274,13 @@ __metadata: languageName: node linkType: hard +"svg-pan-zoom@npm:^3.6.0": + version: 3.6.1 + resolution: "svg-pan-zoom@npm:3.6.1" + checksum: 500b99378d321315e1668067ef6a73af3f61f9adfab75a575b21d2d216cb446def61d60ad12265203463cca9bef56b090f00e48118fa9b7f0ffa1c691e4c1621 + languageName: node + linkType: hard + "svg-parser@npm:^2.0.4": version: 2.0.4 resolution: "svg-parser@npm:2.0.4" @@ -36614,7 +36401,7 @@ __metadata: languageName: node linkType: hard -"symbol-observable@npm:1.2.0, symbol-observable@npm:^1.0.4": +"symbol-observable@npm:1.2.0, symbol-observable@npm:^1.0.4, symbol-observable@npm:^1.1.0": version: 1.2.0 resolution: "symbol-observable@npm:1.2.0" checksum: 48ffbc22e3d75f9853b3ff2ae94a44d84f386415110aea5effc24d84c502e03a4a6b7a8f75ebaf7b585780bda34eb5d6da3121f826a6f93398429d30032971b6 @@ -36953,13 +36740,6 @@ __metadata: languageName: node linkType: hard -"timm@npm:^1.6.1": - version: 1.7.1 - resolution: "timm@npm:1.7.1" - checksum: c80df538ec7fae50a0e3183931b20fbe97f6f2c06907d9675eb7b9d90b3f788af7742285c730192db3b066c4ab22ebae75f8d21970c5b03f38d928d5bb2a0339 - languageName: node - linkType: hard - "timsort@npm:^0.3.0": version: 0.3.0 resolution: "timsort@npm:0.3.0" @@ -36967,6 +36747,13 @@ __metadata: languageName: node linkType: hard +"tiny-emitter@npm:^2.0.0": + version: 2.1.0 + resolution: "tiny-emitter@npm:2.1.0" + checksum: fbcfb5145751a0e3b109507a828eb6d6d4501352ab7bb33eccef46e22e9d9ad3953158870a6966a59e57ab7c3f9cfac7cab8521db4de6a5e757012f4677df2dd + languageName: node + linkType: hard + "tiny-invariant@npm:^1.0.6": version: 1.1.0 resolution: "tiny-invariant@npm:1.1.0" @@ -36988,13 +36775,6 @@ __metadata: languageName: node linkType: hard -"tinycolor2@npm:^1.4.1": - version: 1.4.2 - resolution: "tinycolor2@npm:1.4.2" - checksum: 57ed262e08815a4ab0ed933edafdbc6555a17081781766149813b44a080ecbe58b3ee281e81c0e75b42e4d41679f138cfa98eabf043f829e0683c04adb12c031 - languageName: node - linkType: hard - "title-case@npm:^3.0.3": version: 3.0.3 resolution: "title-case@npm:3.0.3" @@ -37038,13 +36818,6 @@ __metadata: languageName: node linkType: hard -"to-array@npm:0.1.4": - version: 0.1.4 - resolution: "to-array@npm:0.1.4" - checksum: 396a04df5a5e74df5c8891ebb819d575d626e2929312fec3d649b4d38203bbf7185c270cb31fcad903564743895e935f2e965770bd1c6eb137dde48c4d60788d - languageName: node - linkType: hard - "to-arraybuffer@npm:^1.0.0": version: 1.0.1 resolution: "to-arraybuffer@npm:1.0.1" @@ -37218,15 +36991,6 @@ __metadata: languageName: node linkType: hard -"truncate-utf8-bytes@npm:^1.0.0": - version: 1.0.2 - resolution: "truncate-utf8-bytes@npm:1.0.2" - dependencies: - utf8-byte-length: ^1.0.1 - checksum: ad097314709ea98444ad9c80c03aac8da805b894f37ceb5685c49ad297483afe3a5ec9572ebcaff699dda72b6cd447a2ba2a3fd10e96c2628cd16d94abeb328a - languageName: node - linkType: hard - "tryer@npm:^1.0.1": version: 1.0.1 resolution: "tryer@npm:1.0.1" @@ -37561,7 +37325,7 @@ __metadata: languageName: node linkType: hard -"ua-parser-js@npm:^0.7.18": +"ua-parser-js@npm:^0.7.18, ua-parser-js@npm:^0.7.30": version: 0.7.33 resolution: "ua-parser-js@npm:0.7.33" checksum: 1510e9ec26fcaf0d8c6ae8f1078a8230e8816f083e1b5f453ea19d06b8ef2b8a596601c92148fd41899e8b3e5f83fa69c42332bd5729b931a721040339831696 @@ -37690,15 +37454,6 @@ __metadata: languageName: node linkType: hard -"unidiff@npm:1.0.2": - version: 1.0.2 - resolution: "unidiff@npm:1.0.2" - dependencies: - diff: ^2.2.2 - checksum: 442d699d44a07f7511e2ec5b6433fb562d6ddce9eb91c6c28ff23bdda60595d9d7e8c6341937f18996ca03c198d93cc9b8fb448dde211ddeeb45bb45bff8ca1e - languageName: node - linkType: hard - "unified@npm:^10.0.0": version: 10.1.0 resolution: "unified@npm:10.1.0" @@ -38006,22 +37761,6 @@ __metadata: languageName: node linkType: hard -"utf8-byte-length@npm:^1.0.1": - version: 1.0.4 - resolution: "utf8-byte-length@npm:1.0.4" - checksum: f188ca076ec094d58e7009fcc32623c5830c7f0f3e15802bfa4fdd1e759454a481fc4ac05e0fa83b7736e77af628a9ee0e57dcc89683d688fde3811473e42143 - languageName: node - linkType: hard - -"utif@npm:^2.0.1": - version: 2.0.1 - resolution: "utif@npm:2.0.1" - dependencies: - pako: ^1.0.5 - checksum: 66b0bffc18f08834a34c44846f189b2223418e8bddfff3882e07fccea54436737f7334e27c86cbff46e247487dc92377b7a17428a7aafc5d5c733d61bed39038 - languageName: node - linkType: hard - "util-deprecate@npm:^1.0.1, util-deprecate@npm:^1.0.2, util-deprecate@npm:~1.0.1": version: 1.0.2 resolution: "util-deprecate@npm:1.0.2" @@ -38305,6 +38044,13 @@ __metadata: languageName: node linkType: hard +"viz.js@npm:2.1.2": + version: 2.1.2 + resolution: "viz.js@npm:2.1.2" + checksum: 299e5a8519a472e0e98197c5f39c0ca6f3f6c4642b6ae007ed888fea998d82455c7ae0118d09cb888bfd2c46ebe3b03d7ce321a4eef1c822a5f5719d3a6ef3bb + languageName: node + linkType: hard + "vm-browserify@npm:^1.0.1": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" @@ -38313,14 +38059,14 @@ __metadata: linkType: hard "vm2@npm:^3.9.11": - version: 3.9.13 - resolution: "vm2@npm:3.9.13" + version: 3.9.14 + resolution: "vm2@npm:3.9.14" dependencies: acorn: ^8.7.0 acorn-walk: ^8.2.0 bin: vm2: bin/vm2 - checksum: ee82c130a9d1b45558fac4e95133ae022e69fafd4bec8aada44bb2f0aebe6ff8ae9b1176e7cbafceffec35267e009b518c121ad81a5565da06aa7a50ed3a09bc + checksum: 1ed7481e07ce8e03055101b382bfbf0d725a5c9b9bbe8bf75f71501cb43a6bd22f6a0a151975ff7cea8cad136d47e66d64f0a3248913f6d3ca3c405db12bacc0 languageName: node linkType: hard @@ -38403,6 +38149,24 @@ __metadata: languageName: node linkType: hard +"warning@npm:^3.0.0": + version: 3.0.0 + resolution: "warning@npm:3.0.0" + dependencies: + loose-envify: ^1.0.0 + checksum: c9f99a12803aab81b29858e7dc3415bf98b41baee3a4c3acdeb680d98c47b6e17490f1087dccc54432deed5711a5ce0ebcda2b27e9b5eb054c32ae50acb4419c + languageName: node + linkType: hard + +"warning@npm:^4.0.1": + version: 4.0.3 + resolution: "warning@npm:4.0.3" + dependencies: + loose-envify: ^1.0.0 + checksum: 4f2cb6a9575e4faf71ddad9ad1ae7a00d0a75d24521c193fa464f30e6b04027bd97aa5d9546b0e13d3a150ab402eda216d59c1d0f2d6ca60124d96cd40dfa35c + languageName: node + linkType: hard + "watchpack@npm:^2.0.0-beta.10, watchpack@npm:^2.4.0": version: 2.4.0 resolution: "watchpack@npm:2.4.0" @@ -38452,9 +38216,9 @@ __metadata: linkType: hard "web-streams-polyfill@npm:^3.2.0": - version: 3.2.0 - resolution: "web-streams-polyfill@npm:3.2.0" - checksum: e23ad0649392fa0159dbfc6bb27474c308c3f332d9078cfef3c06c154165bef18732c5814126147c6c712f604216ddc950c171c854e3821f020e0d2d721a5958 + version: 3.2.1 + resolution: "web-streams-polyfill@npm:3.2.1" + checksum: b119c78574b6d65935e35098c2afdcd752b84268e18746606af149e3c424e15621b6f1ff0b42b2676dc012fc4f0d313f964b41a4b5031e525faa03997457da02 languageName: node linkType: hard @@ -38655,7 +38419,7 @@ __metadata: languageName: node linkType: hard -"whatwg-fetch@npm:^3.0.0, whatwg-fetch@npm:^3.4.1": +"whatwg-fetch@npm:>=0.10.0, whatwg-fetch@npm:^3.4.1": version: 3.6.2 resolution: "whatwg-fetch@npm:3.6.2" checksum: ee976b7249e7791edb0d0a62cd806b29006ad7ec3a3d89145921ad8c00a3a67e4be8f3fb3ec6bc7b58498724fd568d11aeeeea1f7827e7e1e5eae6c8a275afed @@ -38974,21 +38738,6 @@ __metadata: languageName: node linkType: hard -"ws@npm:~7.4.2": - version: 7.4.6 - resolution: "ws@npm:7.4.6" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 3a990b32ed08c72070d5e8913e14dfcd831919205be52a3ff0b4cdd998c8d554f167c9df3841605cde8b11d607768cacab3e823c58c96a5c08c987e093eb767a - languageName: node - linkType: hard - "xcase@npm:^2.0.1": version: 2.0.1 resolution: "xcase@npm:2.0.1" @@ -38996,18 +38745,6 @@ __metadata: languageName: node linkType: hard -"xhr@npm:^2.0.1": - version: 2.6.0 - resolution: "xhr@npm:2.6.0" - dependencies: - global: ~4.4.0 - is-function: ^1.0.1 - parse-headers: ^2.0.0 - xtend: ^4.0.0 - checksum: a1db277e37737caf3ed363d2a33ce4b4ea5b5fc190b663a6f70bc252799185b840ccaa166eaeeea4841c9c60b87741f0a24e29cbcf6708dd425986d4df186d2f - languageName: node - linkType: hard - "xml-but-prettier@npm:^1.0.1": version: 1.0.1 resolution: "xml-but-prettier@npm:1.0.1" @@ -39052,13 +38789,6 @@ __metadata: languageName: node linkType: hard -"xml-parse-from-string@npm:^1.0.0": - version: 1.0.1 - resolution: "xml-parse-from-string@npm:1.0.1" - checksum: 5155cb98e428409829f4060ce542c55438b2f7646d11fd306d850eaf12d35c06ffd9e86d76aa5230121a533b958fd1a319d6f90a5c113391853d0ff01f4da7bb - languageName: node - linkType: hard - "xml2js@npm:0.4.19": version: 0.4.19 resolution: "xml2js@npm:0.4.19" @@ -39069,7 +38799,7 @@ __metadata: languageName: node linkType: hard -"xml2js@npm:^0.4.11, xml2js@npm:^0.4.19, xml2js@npm:^0.4.23, xml2js@npm:^0.4.5": +"xml2js@npm:^0.4.11, xml2js@npm:^0.4.19, xml2js@npm:^0.4.23": version: 0.4.23 resolution: "xml2js@npm:0.4.23" dependencies: @@ -39121,13 +38851,6 @@ __metadata: languageName: node linkType: hard -"xmlhttprequest-ssl@npm:~1.6.2": - version: 1.6.3 - resolution: "xmlhttprequest-ssl@npm:1.6.3" - checksum: ac8e5de1cdd170bddb928de75393e8977e7eb80c0d8c24fe4be07f6aa1d5c8e2e42296d29abca6591ec2046cc708c220791ecfa56db43c958b8e4de8e7d39984 - languageName: node - linkType: hard - "xpath@npm:0.0.32": version: 0.0.32 resolution: "xpath@npm:0.0.32" @@ -39332,13 +39055,6 @@ __metadata: languageName: node linkType: hard -"yeast@npm:0.1.2": - version: 0.1.2 - resolution: "yeast@npm:0.1.2" - checksum: 81a250b69f601fed541e9518eb2972e75631dd81231689503d7f288612d4eec793b29c208d6807fd6bfc4c2a43614d0c6db233739a4ae6223e244aaed6a885c0 - languageName: node - linkType: hard - "yeoman-environment@npm:^3.9.1": version: 3.13.0 resolution: "yeoman-environment@npm:3.13.0"