diff --git a/.changeset/afraid-teachers-cross.md b/.changeset/afraid-teachers-cross.md new file mode 100644 index 0000000000..8fc376441f --- /dev/null +++ b/.changeset/afraid-teachers-cross.md @@ -0,0 +1,29 @@ +--- +'@backstage/create-app': patch +--- + +Pass on plugin database management instance that is now required by the scaffolder plugin. + +To apply this change to an existing application, add the following to `src/plugins/scaffolder.ts`: + +```diff +export default async function createPlugin({ + logger, + config, ++ database, +}: PluginEnvironment) { + +// ...omitted... + + return await createRouter({ + preparers, + templaters, + publishers, + logger, + config, + dockerClient, + entityClient, ++ database, + }); +} +``` diff --git a/.changeset/catalog-import-export-api-snow-fight.md b/.changeset/catalog-import-export-api-snow-fight.md deleted file mode 100644 index dce9811f10..0000000000 --- a/.changeset/catalog-import-export-api-snow-fight.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-import': patch ---- - -Export _api_ (Client, API, ref) from the catalog import plugin. diff --git a/.changeset/chilled-toys-raise.md b/.changeset/chilled-toys-raise.md deleted file mode 100644 index 282d5492ec..0000000000 --- a/.changeset/chilled-toys-raise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Migrated to new composability API, exporting the plugin as `scaffolderPlugin`. The template list page (`/create`) is exported as the `TemplateIndexPage` extension, and the templating page itself is exported as `TemplatePage`. diff --git a/.changeset/chilly-dodos-drop.md b/.changeset/chilly-dodos-drop.md deleted file mode 100644 index 2af5255e70..0000000000 --- a/.changeset/chilly-dodos-drop.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/techdocs-common': patch -'@backstage/plugin-techdocs-backend': patch ---- - -1. Added option to use Azure Blob Storage as a choice to store the static generated files for TechDocs. diff --git a/.changeset/cost-insights-tidy-geese-play.md b/.changeset/cost-insights-tidy-geese-play.md new file mode 100644 index 0000000000..f8aa503ca6 --- /dev/null +++ b/.changeset/cost-insights-tidy-geese-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cost-insights': patch +--- + +Fix snooze quarter option diff --git a/.changeset/cost-insights-tricky-moles-grin.md b/.changeset/cost-insights-tricky-moles-grin.md deleted file mode 100644 index c35769e03c..0000000000 --- a/.changeset/cost-insights-tricky-moles-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-cost-insights': minor ---- - -add alert hooks diff --git a/.changeset/curly-crabs-compete.md b/.changeset/curly-crabs-compete.md new file mode 100644 index 0000000000..552a41a9f1 --- /dev/null +++ b/.changeset/curly-crabs-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-circleci': patch +--- + +Migrated to new composability API, exporting the plugin instance as `circleCIPlugin`, the entity page content as `EntityCircleCIContent`, and entity conditional as `isCircleCIAvailable`. diff --git a/.changeset/cyan-kiwis-suffer.md b/.changeset/cyan-kiwis-suffer.md deleted file mode 100644 index b59b0e9a32..0000000000 --- a/.changeset/cyan-kiwis-suffer.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Update `create-plugin` template to use the new composability API, by switching to exporting a single routable extension component. diff --git a/.changeset/cyan-lies-flow.md b/.changeset/cyan-lies-flow.md new file mode 100644 index 0000000000..e7da9fab0a --- /dev/null +++ b/.changeset/cyan-lies-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': minor +--- + +Migrated to new composability API, exporting the plugin instance as `searchPlugin`, and page as `SearchPage`. Due to the old router component also being called `SearchPage`, this is a breaking change. The old page component is now exported as `Router`, which can be used to maintain the old behavior. diff --git a/.changeset/cyan-lions-float.md b/.changeset/cyan-lions-float.md deleted file mode 100644 index f5fa064b71..0000000000 --- a/.changeset/cyan-lions-float.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-graphiql': patch ---- - -Finalized composability API migration, now exporting the plugin as `graphiqlPlugin`. diff --git a/.changeset/dirty-carrots-invent.md b/.changeset/dirty-carrots-invent.md deleted file mode 100644 index ceb4e4d2bb..0000000000 --- a/.changeset/dirty-carrots-invent.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Add className to the SidebarItem diff --git a/.changeset/eight-carrots-talk.md b/.changeset/eight-carrots-talk.md deleted file mode 100644 index 01e38b845d..0000000000 --- a/.changeset/eight-carrots-talk.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Update `WarningPanel` component to use accordion-style expansion diff --git a/.changeset/empty-hairs-fetch.md b/.changeset/empty-hairs-fetch.md deleted file mode 100644 index aedeb8a853..0000000000 --- a/.changeset/empty-hairs-fetch.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-org': patch ---- - -Display owner and system as entity page links in the tables of the `api-docs` -plugin. - -Move `isOwnerOf` and `getEntityRelations` from `@backstage/plugin-catalog` to -`@backstage/plugin-catalog-react` and export it from there to use it by other -plugins. diff --git a/.changeset/fair-kids-laugh.md b/.changeset/fair-kids-laugh.md deleted file mode 100644 index d478508ed9..0000000000 --- a/.changeset/fair-kids-laugh.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/theme': patch ---- - -Updates warning text color to align to updated `WarningPanel` styling diff --git a/.changeset/fast-dots-camp.md b/.changeset/fast-dots-camp.md deleted file mode 100644 index 7a1f510f47..0000000000 --- a/.changeset/fast-dots-camp.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-import': patch ---- - -Migrated to new composability API, exporting the plugin instance as `catalogImportPlugin`, and the page as `CatalogImportPage`. diff --git a/.changeset/fifty-dolls-doubt.md b/.changeset/fifty-dolls-doubt.md deleted file mode 100644 index 1abd7a16b9..0000000000 --- a/.changeset/fifty-dolls-doubt.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-lighthouse': patch ---- - -Migrate to new composability API, exporting the plugin instance as `lighthousePlugin`, the top-level page as `LighthousePage`, the entity card as `EntityLastLighthouseAuditCard`, and the entity content as `EntityLighthouseContent`. diff --git a/.changeset/fluffy-hats-hug.md b/.changeset/fluffy-hats-hug.md new file mode 100644 index 0000000000..b3d06337fd --- /dev/null +++ b/.changeset/fluffy-hats-hug.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Don't respond to a request twice if an entity has not been found. diff --git a/.changeset/fluffy-nails-sort.md b/.changeset/fluffy-nails-sort.md deleted file mode 100644 index 0aa87b532f..0000000000 --- a/.changeset/fluffy-nails-sort.md +++ /dev/null @@ -1,24 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Updated the `rootLogger` in `@backstage/backend-common` to support custom logging options. This is useful when you want to make some changes without re-implementing the entire logger and calling `setRootLogger` or `logger.configure`. For example you can add additional `defaultMeta` tags to each log entry. The following changes are included: - -- Added `createRootLogger` which accepts winston `LoggerOptions`. These options allow overriding the default keys. - -Example Usage: - -```ts -// Create the logger -const logger = createRootLogger({ - defaultMeta: { appName: 'backstage', appEnv: 'prod' }, -}); - -// Add a custom logger transport -logger.add(new MyCustomTransport()); - -const config = await loadBackendConfig({ - argv: process.argv, - logger: getRootLogger(), // already set to new logger instance -}); -``` diff --git a/.changeset/four-rings-push.md b/.changeset/four-rings-push.md new file mode 100644 index 0000000000..500dd0e812 --- /dev/null +++ b/.changeset/four-rings-push.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-cloudbuild': patch +--- + +Migrate to new composability API, exporting the plugin instance as `cloudbuildPlugin`, the entity content as `EntityCloudbuildContent`, the entity conditional as `isCloudbuildAvailable`, and entity cards as `EntityLatestCloudbuildRunCard` and `EntityLatestCloudbuildsForBranchCard`. diff --git a/.changeset/fresh-eels-compare.md b/.changeset/fresh-eels-compare.md deleted file mode 100644 index 5d17556f7f..0000000000 --- a/.changeset/fresh-eels-compare.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Migrate to new composability API, exporting the plugin as `userSettingsPlugin` and the page as `UserSettingsPage`. diff --git a/.changeset/funny-students-shout.md b/.changeset/funny-students-shout.md new file mode 100644 index 0000000000..97b273c687 --- /dev/null +++ b/.changeset/funny-students-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-register-component': patch +--- + +Migrated to new composability API, exporting the plugin instance as `registerComponentPlugin`, and page as `RegisterComponentPage`. diff --git a/.changeset/fuzzy-pumpkins-tie.md b/.changeset/fuzzy-pumpkins-tie.md deleted file mode 100644 index 1e87972ce1..0000000000 --- a/.changeset/fuzzy-pumpkins-tie.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Adds a new optional `links` metadata field to the Entity class within the `catalog-model` package (as discussed in [[RFC] Entity Links](https://github.com/backstage/backstage/issues/3787)). This PR adds support for the entity links only. Follow up PR's will introduce the UI component to display them. diff --git a/.changeset/hot-rules-shout.md b/.changeset/hot-rules-shout.md deleted file mode 100644 index c88f4b01dc..0000000000 --- a/.changeset/hot-rules-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Add `children` option to `addPage`, which will be rendered as the children of the `Route`. diff --git a/.changeset/large-terms-invite.md b/.changeset/large-terms-invite.md new file mode 100644 index 0000000000..6653ac8e1e --- /dev/null +++ b/.changeset/large-terms-invite.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Introduced `v2` Scaffolder REST API, which uses an implementation that is database backed, making the scaffolder instances stateless. The `createRouter` function now requires a `PluginDatabaseManager` instance to be passed in, commonly available as `database` in the plugin environment in the backend. + +This API should be considered unstable until used by the scaffolder frontend. diff --git a/.changeset/little-cherries-hug.md b/.changeset/little-cherries-hug.md deleted file mode 100644 index 5fb42cbdad..0000000000 --- a/.changeset/little-cherries-hug.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Attempt to fix windows test errors in master diff --git a/.changeset/little-pets-cross.md b/.changeset/little-pets-cross.md deleted file mode 100644 index 0f4e2605cd..0000000000 --- a/.changeset/little-pets-cross.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Replace `yup` with `ajv`, for validation of catalog entities. diff --git a/.changeset/loud-walls-collect.md b/.changeset/loud-walls-collect.md deleted file mode 100644 index 78356eef38..0000000000 --- a/.changeset/loud-walls-collect.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Fixed module resolution of external libraries during backend development. Modules used to be resolved relative to the backend entrypoint, but are now resolved relative to each individual module. diff --git a/.changeset/lovely-panthers-peel.md b/.changeset/lovely-panthers-peel.md deleted file mode 100644 index 512f0ebd3f..0000000000 --- a/.changeset/lovely-panthers-peel.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/core': minor ---- - -Closes #3556 -The scroll bar of collapsed sidebar is now hidden without full screen. - -![image](https://user-images.githubusercontent.com/46953622/105390193-0bfd0080-5c19-11eb-8e86-2161bbe6e8d9.png) diff --git a/.changeset/metal-insects-compete.md b/.changeset/metal-insects-compete.md deleted file mode 100644 index ead779d6a9..0000000000 --- a/.changeset/metal-insects-compete.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -Migrate to new composability API, exporting the plugin instance as `orgPlugin`, and the entity cards as `EntityGroupProfileCard`, `EntityMembersListCard`, `EntityOwnershipCard`, and `EntityUserProfileCard`. diff --git a/.changeset/metal-pans-leave.md b/.changeset/metal-pans-leave.md deleted file mode 100644 index 16d5478e43..0000000000 --- a/.changeset/metal-pans-leave.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': patch ---- - -Bump `config-loader` to `ajv` 7, to enable v7 feature use elsewhere diff --git a/.changeset/moody-apricots-warn.md b/.changeset/moody-apricots-warn.md deleted file mode 100644 index c4d9be0ba3..0000000000 --- a/.changeset/moody-apricots-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/dev-utils': patch ---- - -Added `path` option to `addPage` that can be used to set a specific path for the page rather than a generated one. Also omit sidebar item altogether if `title` option is not set. diff --git a/.changeset/moody-buckets-visit.md b/.changeset/moody-buckets-visit.md deleted file mode 100644 index 07a960620b..0000000000 --- a/.changeset/moody-buckets-visit.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': patch ---- - -Properly forward errors that occur when looking up GitLab project IDs. diff --git a/.changeset/neat-brooms-allow.md b/.changeset/neat-brooms-allow.md deleted file mode 100644 index a4c69b0945..0000000000 --- a/.changeset/neat-brooms-allow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/config-loader': patch ---- - -Each piece of the configuration schema is now validated upfront, in order to produce more informative errors. diff --git a/.changeset/nice-bottles-battle.md b/.changeset/nice-bottles-battle.md deleted file mode 100644 index a688241caa..0000000000 --- a/.changeset/nice-bottles-battle.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-backend': patch ---- - -Failures to load the frontend configuration schema now throws an error that includes more context and instructions for how to fix the issue. diff --git a/.changeset/ninety-houses-shout.md b/.changeset/ninety-houses-shout.md new file mode 100644 index 0000000000..845decc74c --- /dev/null +++ b/.changeset/ninety-houses-shout.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Migrated to new composability API, exporting the plugin instance as `techdocsPlugin`, the top-level page as `TechdocsPage`, and the entity content as `EntityTechdocsContent`. diff --git a/.changeset/ninety-keys-serve.md b/.changeset/ninety-keys-serve.md deleted file mode 100644 index d15884b1c4..0000000000 --- a/.changeset/ninety-keys-serve.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Add a `prop` union for `SignInPage` that allows it to be used for just a single provider, with inline errors, and optionally with automatic sign-in. diff --git a/.changeset/odd-buckets-compare.md b/.changeset/odd-buckets-compare.md new file mode 100644 index 0000000000..faf68fb4a0 --- /dev/null +++ b/.changeset/odd-buckets-compare.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Set explicit content-type in error handler responses. diff --git a/.changeset/old-tools-exist.md b/.changeset/old-tools-exist.md new file mode 100644 index 0000000000..a9365cb784 --- /dev/null +++ b/.changeset/old-tools-exist.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Slight refactoring in support of a future search implementation in `UrlReader`. Mostly moving code around. diff --git a/.changeset/orange-pets-whisper.md b/.changeset/orange-pets-whisper.md deleted file mode 100644 index e96c210e43..0000000000 --- a/.changeset/orange-pets-whisper.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Add check for outdated/duplicate packages to yarn start diff --git a/.changeset/perfect-ladybugs-listen.md b/.changeset/perfect-ladybugs-listen.md new file mode 100644 index 0000000000..2f9e65c1d9 --- /dev/null +++ b/.changeset/perfect-ladybugs-listen.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-pagerduty': patch +--- + +Migrated to new composability API, exporting the plugin instance as `pagerDutyPlugin`, entity card as `EntityPagerDutyCard`, and entity conditional as `isPagerDutyAvailable`. diff --git a/.changeset/plenty-steaks-confess.md b/.changeset/plenty-steaks-confess.md deleted file mode 100644 index f9ffe0f539..0000000000 --- a/.changeset/plenty-steaks-confess.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-gcp-projects': patch ---- - -Migrate to new composability API, exporting the plugin as `gcpProjectsPlugin` and page as `GcpProjectsPage`. diff --git a/.changeset/poor-ligers-flow.md b/.changeset/poor-ligers-flow.md deleted file mode 100644 index 1c3a8a742b..0000000000 --- a/.changeset/poor-ligers-flow.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-backend': patch ---- - -Throw `NotAllowedError` when registering locations with entities of disallowed kinds diff --git a/.changeset/poor-sheep-give.md b/.changeset/poor-sheep-give.md new file mode 100644 index 0000000000..4c52594b35 --- /dev/null +++ b/.changeset/poor-sheep-give.md @@ -0,0 +1,5 @@ +--- +'@backstage/test-utils': patch +--- + +Added `mountedRoutes` option to `wrapInTestApp`, allowing routes to be associated to concrete paths to make `useRouteRef` usable in tested components. diff --git a/.changeset/popular-cars-eat.md b/.changeset/popular-cars-eat.md new file mode 100644 index 0000000000..6d3bcdfab2 --- /dev/null +++ b/.changeset/popular-cars-eat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +Migrate to new composability API, exporting the plugin instance as `jenkinsPlugin`, the entity content as `EntityJenkinsContent`, the entity conditional as `isJenkinsAvailable`, and the entity card as `EntityLatestJenkinsRunCard`. diff --git a/.changeset/pretty-melons-prove.md b/.changeset/pretty-melons-prove.md deleted file mode 100644 index 7048757e5a..0000000000 --- a/.changeset/pretty-melons-prove.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-fossa': minor ---- - -Port FOSSA plugin to new extension model. - -If you are using the FOSSA plugin adjust the plugin import from `plugin` to -`fossaPlugin` and replace `` with ``. diff --git a/.changeset/quick-apes-shop.md b/.changeset/quick-apes-shop.md deleted file mode 100644 index 4c87b9253d..0000000000 --- a/.changeset/quick-apes-shop.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch ---- - -Migrate to new composability API, exporting the plugin as `apiDocsPlugin`, index page as `ApiExplorerPage`, and entity page cards as `EntityApiDefinitionCard`, `EntityConsumedApisCard`, `EntityConsumingComponentsCard`, `EntityProvidedApisCard`, and `EntityProvidingComponentsCard`. diff --git a/.changeset/selfish-kids-know.md b/.changeset/selfish-kids-know.md deleted file mode 100644 index f434559135..0000000000 --- a/.changeset/selfish-kids-know.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Finalize migration to new composability API, with the plugin instance now exported `catalogPlugin`. diff --git a/.changeset/selfish-years-pump.md b/.changeset/selfish-years-pump.md new file mode 100644 index 0000000000..e5cc5f3bc3 --- /dev/null +++ b/.changeset/selfish-years-pump.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-github-actions': patch +--- + +Migrate to new composability API, exporting the plugin instance as `githubActionsPlugin`, the entity content as `EntityGithubActionsContent`, entity conditional as `isGithubActionsAvailable`, and entity cards as `EntityLatestGithubActionRunCard`, `EntityLatestGithubActionsForBranchCard`, and `EntityRecentGithubActionsRunsCard`. diff --git a/.changeset/shaggy-dingos-suffer.md b/.changeset/shaggy-dingos-suffer.md deleted file mode 100644 index c1e200b701..0000000000 --- a/.changeset/shaggy-dingos-suffer.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-tech-radar': patch ---- - -Fix mapping RadarEntry and Entry for moved and url attributes -Fix clicking of links in the radar legend diff --git a/.changeset/shiny-falcons-marry.md b/.changeset/shiny-falcons-marry.md new file mode 100644 index 0000000000..4dda68d693 --- /dev/null +++ b/.changeset/shiny-falcons-marry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-newrelic': patch +--- + +Migrate to new composability API, exporting the plugin instance as `newRelicPlugin`, and the root page as `NewRelicPage`. diff --git a/.changeset/shiny-rabbits-unite.md b/.changeset/shiny-rabbits-unite.md deleted file mode 100644 index dca9ee0dd9..0000000000 --- a/.changeset/shiny-rabbits-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Fix check that determines whether popup was closed or the messaging was misconfigured. diff --git a/.changeset/shy-maps-guess.md b/.changeset/shy-maps-guess.md new file mode 100644 index 0000000000..2a50d3d461 --- /dev/null +++ b/.changeset/shy-maps-guess.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Migrate to new composability API, exporting the plugin instance as `kubernetesPlugin` and entity content as `EntityKubernetesContent`. diff --git a/.changeset/silent-readers-worry.md b/.changeset/silent-readers-worry.md deleted file mode 100644 index 866485ecc0..0000000000 --- a/.changeset/silent-readers-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': patch ---- - -Migrate to new composability API, exporting the plugin as `sonarQubePlugin` and card as `EntitySonarQubeCard`. diff --git a/.changeset/six-ravens-heal.md b/.changeset/six-ravens-heal.md deleted file mode 100644 index 960c482be9..0000000000 --- a/.changeset/six-ravens-heal.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/catalog-model': patch ---- - -Introduce json schema variants of the `yup` validation schemas diff --git a/.changeset/sixty-lemons-agree.md b/.changeset/sixty-lemons-agree.md new file mode 100644 index 0000000000..7fc438b433 --- /dev/null +++ b/.changeset/sixty-lemons-agree.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kafka': patch +--- + +Migrate to new composability API, exporting the plugin instance as `kafkaPlugin`, entity content as `EntityKafkaContent`, and entity conditional as `isKafkaAvailable`. diff --git a/.changeset/sour-gorillas-fail.md b/.changeset/sour-gorillas-fail.md deleted file mode 100644 index c6394ed76e..0000000000 --- a/.changeset/sour-gorillas-fail.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -# Repo visibility for GitLab and BitBucket repos - -**NOTE: This changes default repo visibility from `private` to `public` for GitLab and BitBucket** which -is consistent with the GitHub default. If you were counting on `private` visibility, you'll need to update -your scaffolder config to use `private`. - -This adds repo visibility feature parity with GitHub for GitLab and BitBucket. - -To configure the repo visibility, set scaffolder._type_.visibility as in this example: - -```yaml -scaffolder: - github: - visibility: private # 'public' or 'internal' or 'private' (default is 'public') - gitlab: - visibility: public # 'public' or 'internal' or 'private' (default is 'public') - bitbucket: - visibility: public # 'public' or 'private' (default is 'public') -``` diff --git a/.changeset/stale-zebras-warn.md b/.changeset/stale-zebras-warn.md deleted file mode 100644 index 33b53d3f75..0000000000 --- a/.changeset/stale-zebras-warn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-welcome': patch ---- - -Migrated to new composability API, exporting the plugin as `welcomePlugin` and the page as `WelcomePage`. diff --git a/.changeset/strange-cobras-unite.md b/.changeset/strange-cobras-unite.md new file mode 100644 index 0000000000..d53a324dab --- /dev/null +++ b/.changeset/strange-cobras-unite.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Refactored route response handling to use more explicit types and throw errors. diff --git a/.changeset/techdocs-chilly-steaks-brush.md b/.changeset/techdocs-chilly-steaks-brush.md deleted file mode 100644 index 7fd4763c63..0000000000 --- a/.changeset/techdocs-chilly-steaks-brush.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -dir preparer will use URL Reader in its implementation. diff --git a/.changeset/techdocs-green-rabbits-burn.md b/.changeset/techdocs-green-rabbits-burn.md new file mode 100644 index 0000000000..5af68c2000 --- /dev/null +++ b/.changeset/techdocs-green-rabbits-burn.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Enhance API calls to support trapping 500 errors from techdocs-backend diff --git a/.changeset/tidy-news-perform.md b/.changeset/tidy-news-perform.md deleted file mode 100644 index 055f051da7..0000000000 --- a/.changeset/tidy-news-perform.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Keep the Pin Sidebar setting visible on small screens. diff --git a/.changeset/tricky-dancers-rush.md b/.changeset/tricky-dancers-rush.md new file mode 100644 index 0000000000..301b23d98e --- /dev/null +++ b/.changeset/tricky-dancers-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Fixed type inference of `createRouteRef`. diff --git a/.changeset/two-dogs-search.md b/.changeset/two-dogs-search.md new file mode 100644 index 0000000000..5f673c0cfb --- /dev/null +++ b/.changeset/two-dogs-search.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-gitops-profiles': patch +--- + +Migrated to new composability API, exporting the plugin instance as `gitopsProfilesPlugin` and pages as `GitopsProfilesClusterListPage`, `GitopsProfilesClusterPage`, and `GitopsProfilesCreatePage`. diff --git a/.changeset/wet-suits-live.md b/.changeset/wet-suits-live.md deleted file mode 100644 index 27b8df8a84..0000000000 --- a/.changeset/wet-suits-live.md +++ /dev/null @@ -1,20 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-catalog-import': patch -'@backstage/plugin-circleci': patch -'@backstage/plugin-cloudbuild': patch -'@backstage/plugin-github-actions': patch -'@backstage/plugin-jenkins': patch -'@backstage/plugin-kafka': patch -'@backstage/plugin-lighthouse': patch -'@backstage/plugin-org': patch -'@backstage/plugin-register-component': patch -'@backstage/plugin-rollbar': patch -'@backstage/plugin-scaffolder': patch -'@backstage/plugin-search': patch -'@backstage/plugin-sentry': patch -'@backstage/plugin-techdocs': patch -'@backstage/dev-utils': patch ---- - -Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. diff --git a/.changeset/wicked-beds-buy.md b/.changeset/wicked-beds-buy.md deleted file mode 100644 index ddcf4f880e..0000000000 --- a/.changeset/wicked-beds-buy.md +++ /dev/null @@ -1,61 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': minor ---- - -The scaffolder is updated to generate a unique workspace directory inside the temp folder. This directory is cleaned up by the job processor after each run. - -The prepare/template/publish steps have been refactored to operate on known directories, `template/` and `result/`, inside the temporary workspace path. - -Updated preparers to accept the template url instead of the entire template. This is done primarily to allow for backwards compatibility between v1 and v2 scaffolder templates. - -Fixes broken GitHub actions templating in the Create React App template. - -#### For those with **custom** preparers, templates, or publishers - -The preparer interface has changed, the prepare method now only takes a single argument, and doesn't return anything. As part of this change the preparers were refactored to accept a URL pointing to the target directory, rather than computing that from the template entity. - -The `workingDirectory` option was also removed, and replaced with a `workspacePath` option. The difference between the two is that `workingDirectory` was a place for the preparer to create temporary directories, while the `workspacePath` is the specific folder were the entire templating process for a single template job takes place. Instead of returning a path to the folder were the prepared contents were placed, the contents are put at the `/template` path. - -```diff -type PreparerOptions = { -- workingDirectory?: string; -+ /** -+ * Full URL to the directory containg template data -+ */ -+ url: string; -+ /** -+ * The workspace path that will eventually be the the root of the new repo -+ */ -+ workspacePath: string; - logger: Logger; -}; - --prepare(template: TemplateEntityV1alpha1, opts?: PreparerOptions): Promise -+prepare(opts: PreparerOptions): Promise; -``` - -Instead of returning a path to the folder were the templaters contents were placed, the contents are put at the `/result` path. All templaters now also expect the source template to be present in the `template` directory within the `workspacePath`. - -```diff -export type TemplaterRunOptions = { -- directory: string; -+ workspacePath: string; - values: TemplaterValues; - logStream?: Writable; - dockerClient: Docker; -}; - --public async run(options: TemplaterRunOptions): Promise -+public async run(options: TemplaterRunOptions): Promise -``` - -Just like the preparer and templaters, the publishers have also switched to using `workspacePath`. The root of the new repo is expected to be located at `/result`. - -```diff -export type PublisherOptions = { - values: TemplaterValues; -- directory: string; -+ workspacePath: string; - logger: Logger; -}; -``` diff --git a/.changeset/wild-cows-exercise.md b/.changeset/wild-cows-exercise.md deleted file mode 100644 index cbc347888f..0000000000 --- a/.changeset/wild-cows-exercise.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/plugin-catalog': minor -'@backstage/create-app': minor ---- - -`@backstage/plugin-catalog` stopped exporting hooks and helpers for other -plugins. They are migrated to `@backstage/plugin-catalog-react`. -Change both your dependencies and imports to the new package. diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 62be0a6cea..0fe1642394 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -3,6 +3,7 @@ name: E2E Test Linux on: pull_request: paths-ignore: + - '.changeset/**' - 'contrib/**' - 'docs/**' - 'microsite/**' diff --git a/app-config.yaml b/app-config.yaml index 763dd0c258..5240a050a8 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -74,8 +74,6 @@ organization: # Reference documentation http://backstage.io/docs/features/techdocs/configuration techdocs: - requestUrl: http://localhost:7000/api/techdocs - storageUrl: http://localhost:7000/api/techdocs/static/docs builder: 'local' # Alternatives - 'external' generators: techdocs: 'docker' # Alternatives - 'local' diff --git a/docs/features/techdocs/configuration.md b/docs/features/techdocs/configuration.md index bb44050dcf..20e436d8a4 100644 --- a/docs/features/techdocs/configuration.md +++ b/docs/features/techdocs/configuration.md @@ -13,15 +13,6 @@ configuration options for TechDocs. # File: app-config.yaml techdocs: - # TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc. - - requestUrl: http://localhost:7000/api/techdocs - - # Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware - # to serve files from either a local directory or an External storage provider. - - storageUrl: http://localhost:7000/api/techdocs/static/docs - # generators.techdocs can have two values: 'docker' or 'local'. This is to determine how to run the generator - whether to # spin up the techdocs-container docker image or to run mkdocs locally (assuming all the dependencies are taken care of). # You want to change this to 'local' if you are running Backstage using your own custom Docker setup and want to avoid running @@ -101,4 +92,15 @@ techdocs: # https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json accountKey: $env: TECHDOCS_AZURE_BLOB_STORAGE_ACCOUNT_KEY + + # (Optional and Legacy) TechDocs makes API calls to techdocs-backend using this URL. e.g. get docs of an entity, get metadata, etc. + # You don't have to specify this anymore. + + requestUrl: http://localhost:7000/api/techdocs + + # (Optional and Legacy) Just another route in techdocs-backend where TechDocs requests the static files from. This URL uses an HTTP middleware + # to serve files from either a local directory or an External storage provider. + # You don't have to specify this anymore. + + storageUrl: http://localhost:7000/api/techdocs/static/docs ``` diff --git a/docs/features/techdocs/creating-and-publishing.md b/docs/features/techdocs/creating-and-publishing.md index c15dc1fe69..b9340330a8 100644 --- a/docs/features/techdocs/creating-and-publishing.md +++ b/docs/features/techdocs/creating-and-publishing.md @@ -80,6 +80,10 @@ Create a `/docs` folder in the root of the project with at least an `index.md` file. _(If you add more markdown files, make sure to update the nav in the mkdocs.yml file to get a proper navigation for your documentation.)_ +> Note - Although `docs` is a popular directory name for storing documentation, +> it can be renamed to something else and can be configured by `mkdocs.yml`. See +> https://www.mkdocs.org/user-guide/configuration/#docs_dir + The `docs/index.md` can for example have the following content: ```md diff --git a/docs/features/techdocs/getting-started.md b/docs/features/techdocs/getting-started.md index 5147e17aaa..e0b323857d 100644 --- a/docs/features/techdocs/getting-started.md +++ b/docs/features/techdocs/getting-started.md @@ -150,26 +150,10 @@ app. Now let us tweak some configurations to suit your needs. **See [TechDocs Configuration Options](configuration.md) for complete configuration reference.** -### Setting TechDocs URLs - -```yaml -techdocs: - storageUrl: http://localhost:7000/api/techdocs/static/docs - requestUrl: http://localhost:7000/api/techdocs/ -``` - -`requestUrl` is used by TechDocs frontend plugin to discover `techdocs-backend` -endpoints, and the `storageUrl` is another endpoint in `techdocs-backend` which -acts as a middleware between TechDocs and the storage (where the static -generated docs site are stored). These default values should mostly work for -you. These options will soon be optional to set. - ### Should TechDocs Backend generate docs? ```yaml techdocs: - storageUrl: http://localhost:7000/api/techdocs/static/docs - requestUrl: http://localhost:7000/api/techdocs/ builder: 'local' ``` @@ -196,8 +180,6 @@ out Backstage for the first time. At a later time, review ```yaml techdocs: - storageUrl: http://localhost:7000/api/techdocs/static/docs - requestUrl: http://localhost:7000/api/techdocs/ builder: 'local' publisher: type: 'local' @@ -219,6 +201,9 @@ no config is provided. ```yaml techdocs: + builder: 'local' + publisher: + type: 'local' generators: techdocs: local ``` diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index 418a6b3dd3..e92b54d08b 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -90,7 +90,7 @@ $ docker build -t example-deployment . To run the image locally you can run: ```sh -$ docker run -p -it 7000:7000 example-deployment +$ docker run -it -p 7000:7000 example-deployment ``` You should then start to get logs in your terminal, and then you can open your diff --git a/package.json b/package.json index 758a8ba884..ad0ce2cd57 100644 --- a/package.json +++ b/package.json @@ -37,11 +37,12 @@ }, "resolutions": { "**/@roadiehq/**/@backstage/core": "*", + "**/@roadiehq/**/@backstage/plugin-catalog": "*", "**/@roadiehq/**/@backstage/catalog-model": "*" }, "version": "1.0.0", "devDependencies": { - "@changesets/cli": "^2.11.0", + "@changesets/cli": "^2.14.0", "@octokit/openapi-types": "^2.2.0", "@spotify/eslint-config-oss": "^1.0.1", "@spotify/prettier-config": "^9.0.0", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index d39f705546..fcd9b795a4 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,81 @@ # example-app +## 0.2.14 + +### Patch Changes + +- 9d6ef14bc: Migrated to new composability API, exporting the plugin instance as `rollbarPlugin`, the entity page content as `EntityRollbarContent`, and entity conditional as `isRollbarAvailable`. Updated the `EntityPage` for the `example-app` to include a composite `ErrorsSwitcher` component that works with both `Sentry` & `Rollbar`. Also removed the unused and undocumented `RollbarHome` related components. +- Updated dependencies [ceef4dd89] +- Updated dependencies [720149854] +- Updated dependencies [19172f5a9] +- Updated dependencies [4c6a6dddd] +- Updated dependencies [398e1f83e] +- Updated dependencies [87b189d00] +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [b712841d6] +- Updated dependencies [a5628df40] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [bc5082a00] +- Updated dependencies [6e612ce25] +- Updated dependencies [e44925723] +- Updated dependencies [b37501a3d] +- Updated dependencies [9d6ef14bc] +- Updated dependencies [025e122c3] +- Updated dependencies [e9aab60c7] +- Updated dependencies [21e624ba9] +- Updated dependencies [0269f4fd9] +- Updated dependencies [19fe61c27] +- Updated dependencies [da9f53c60] +- Updated dependencies [a08c4b0b0] +- Updated dependencies [bc5082a00] +- Updated dependencies [bc5082a00] +- Updated dependencies [b37501a3d] +- Updated dependencies [90c8f20b9] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [8dfdec613] +- Updated dependencies [54c7d02f7] +- Updated dependencies [de98c32ed] +- Updated dependencies [806929fe2] +- Updated dependencies [019fe39a0] +- Updated dependencies [019fe39a0] +- Updated dependencies [11cb5ef94] + - @backstage/plugin-catalog-import@0.3.7 + - @backstage/plugin-scaffolder@0.4.2 + - @backstage/plugin-cost-insights@0.8.0 + - @backstage/cli@0.6.0 + - @backstage/plugin-graphiql@0.2.7 + - @backstage/core@0.6.0 + - @backstage/plugin-api-docs@0.4.4 + - @backstage/plugin-catalog@0.3.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/plugin-org@0.3.5 + - @backstage/theme@0.2.3 + - @backstage/plugin-lighthouse@0.2.9 + - @backstage/plugin-techdocs@0.5.5 + - @backstage/plugin-user-settings@0.2.5 + - @backstage/catalog-model@0.7.1 + - @backstage/plugin-rollbar@0.2.9 + - @backstage/plugin-gcp-projects@0.2.4 + - @backstage/plugin-tech-radar@0.3.4 + - @backstage/plugin-welcome@0.2.5 + - @backstage/plugin-explore@0.2.4 + - @backstage/plugin-circleci@0.2.7 + - @backstage/plugin-cloudbuild@0.2.8 + - @backstage/plugin-github-actions@0.3.1 + - @backstage/plugin-jenkins@0.3.8 + - @backstage/plugin-kafka@0.2.1 + - @backstage/plugin-register-component@0.2.8 + - @backstage/plugin-search@0.2.7 + - @backstage/plugin-sentry@0.3.4 + - @backstage/plugin-gitops-profiles@0.2.4 + - @backstage/plugin-kubernetes@0.3.8 + - @backstage/plugin-newrelic@0.2.4 + - @backstage/plugin-pagerduty@0.2.7 + ## 0.2.13 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 9de8299d93..9b82e0bb32 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,41 +1,41 @@ { "name": "example-app", - "version": "0.2.13", + "version": "0.2.14", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/cli": "^0.5.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-api-docs": "^0.4.3", - "@backstage/plugin-catalog": "^0.2.14", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/plugin-catalog-import": "^0.3.6", - "@backstage/plugin-circleci": "^0.2.6", - "@backstage/plugin-cloudbuild": "^0.2.7", - "@backstage/plugin-cost-insights": "^0.7.0", - "@backstage/plugin-explore": "^0.2.3", - "@backstage/plugin-gcp-projects": "^0.2.3", - "@backstage/plugin-github-actions": "^0.3.0", - "@backstage/plugin-gitops-profiles": "^0.2.3", - "@backstage/plugin-graphiql": "^0.2.6", - "@backstage/plugin-org": "^0.3.4", - "@backstage/plugin-jenkins": "^0.3.6", - "@backstage/plugin-kafka": "^0.2.0", - "@backstage/plugin-kubernetes": "^0.3.7", - "@backstage/plugin-lighthouse": "^0.2.8", - "@backstage/plugin-newrelic": "^0.2.3", - "@backstage/plugin-pagerduty": "0.2.6", - "@backstage/plugin-register-component": "^0.2.7", - "@backstage/plugin-rollbar": "^0.2.8", - "@backstage/plugin-scaffolder": "^0.4.1", - "@backstage/plugin-sentry": "^0.3.3", - "@backstage/plugin-search": "^0.2.6", - "@backstage/plugin-tech-radar": "^0.3.3", - "@backstage/plugin-techdocs": "^0.5.4", - "@backstage/plugin-user-settings": "^0.2.4", - "@backstage/plugin-welcome": "^0.2.4", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/cli": "^0.6.0", + "@backstage/core": "^0.6.0", + "@backstage/plugin-api-docs": "^0.4.4", + "@backstage/plugin-catalog": "^0.3.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/plugin-catalog-import": "^0.3.7", + "@backstage/plugin-circleci": "^0.2.7", + "@backstage/plugin-cloudbuild": "^0.2.8", + "@backstage/plugin-cost-insights": "^0.8.0", + "@backstage/plugin-explore": "^0.2.4", + "@backstage/plugin-gcp-projects": "^0.2.4", + "@backstage/plugin-github-actions": "^0.3.1", + "@backstage/plugin-gitops-profiles": "^0.2.4", + "@backstage/plugin-graphiql": "^0.2.7", + "@backstage/plugin-org": "^0.3.5", + "@backstage/plugin-jenkins": "^0.3.8", + "@backstage/plugin-kafka": "^0.2.1", + "@backstage/plugin-kubernetes": "^0.3.8", + "@backstage/plugin-lighthouse": "^0.2.9", + "@backstage/plugin-newrelic": "^0.2.4", + "@backstage/plugin-pagerduty": "0.2.7", + "@backstage/plugin-register-component": "^0.2.8", + "@backstage/plugin-rollbar": "^0.2.9", + "@backstage/plugin-scaffolder": "^0.4.2", + "@backstage/plugin-sentry": "^0.3.4", + "@backstage/plugin-search": "^0.2.7", + "@backstage/plugin-tech-radar": "^0.3.4", + "@backstage/plugin-techdocs": "^0.5.5", + "@backstage/plugin-user-settings": "^0.2.5", + "@backstage/plugin-welcome": "^0.2.5", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.12", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 1b76773979..00696736d1 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -15,30 +15,30 @@ */ import { - createApp, AlertDisplay, - OAuthRequestDialog, - SignInPage, + createApp, createRouteRef, FlatRoutes, + OAuthRequestDialog, + SignInPage, } from '@backstage/core'; -import React from 'react'; -import Root from './components/Root'; -import * as plugins from './plugins'; -import { apis } from './apis'; -import { hot } from 'react-hot-loader/root'; -import { providers } from './identityProviders'; import { Router as CatalogRouter } from '@backstage/plugin-catalog'; -import { Router as DocsRouter } from '@backstage/plugin-techdocs'; +import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; +import { ExplorePage } from '@backstage/plugin-explore'; import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql'; -import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; import { Router as LighthouseRouter } from '@backstage/plugin-lighthouse'; import { Router as RegisterComponentRouter } from '@backstage/plugin-register-component'; +import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; +import { Router as DocsRouter } from '@backstage/plugin-techdocs'; import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; -import { Router as ImportComponentRouter } from '@backstage/plugin-catalog-import'; -import { Route, Navigate } from 'react-router'; - +import React from 'react'; +import { hot } from 'react-hot-loader/root'; +import { Navigate, Route } from 'react-router'; +import { apis } from './apis'; import { EntityPage } from './components/catalog/EntityPage'; +import Root from './components/Root'; +import { providers } from './identityProviders'; +import * as plugins from './plugins'; const app = createApp({ apis, @@ -78,6 +78,7 @@ const routes = ( element={} /> } /> + } /> } diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 52dd397418..80706fe459 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -20,6 +20,7 @@ import HomeIcon from '@material-ui/icons/Home'; import ExtensionIcon from '@material-ui/icons/Extension'; import RuleIcon from '@material-ui/icons/AssignmentTurnedIn'; import MapIcon from '@material-ui/icons/MyLocation'; +import LayersIcon from '@material-ui/icons/Layers'; import LibraryBooks from '@material-ui/icons/LibraryBooks'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import MoneyIcon from '@material-ui/icons/MonetizationOn'; @@ -82,6 +83,7 @@ const Root = ({ children }: PropsWithChildren<{}>) => ( + {/* End global nav */} diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 0372c8759c..5474afa72c 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ApiEntity, Entity, @@ -64,6 +65,10 @@ import { isPluginApplicableToEntity as isPagerDutyAvailable, PagerDutyCard, } from '@backstage/plugin-pagerduty'; +import { + isRollbarAvailable, + Router as RollbarRouter, +} from '@backstage/plugin-rollbar'; import { Router as SentryRouter } from '@backstage/plugin-sentry'; import { EmbeddedDocsRouter as DocsRouter } from '@backstage/plugin-techdocs'; import { Button, Grid } from '@material-ui/core'; @@ -153,6 +158,15 @@ const RecentCICDRunsSwitcher = ({ entity }: { entity: Entity }) => { ); }; +export const ErrorsSwitcher = ({ entity }: { entity: Entity }) => { + switch (true) { + case isRollbarAvailable(entity): + return ; + default: + return ; + } +}; + const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( @@ -212,9 +226,9 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( element={} /> } + path="/errors/*" + title="Errors" + element={} /> ( element={} /> } + path="/errors/*" + title="Errors" + element={} /> { - if (response.headersSent) { + if (res.headersSent) { // If the headers have already been sent, do not send the response again // as this will throw an error in the backend. next(error); @@ -80,7 +80,9 @@ export function errorHandler( logger.error(error); } - response.status(status).send(message); + res.status(status); + res.setHeader('content-type', 'text/plain'); + res.send(message); }; } diff --git a/packages/backend-common/src/reading/GithubUrlReader.ts b/packages/backend-common/src/reading/GithubUrlReader.ts index 4f1aa7c07b..99d641f2dd 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.ts @@ -15,11 +15,12 @@ */ import { - GitHubIntegrationConfig, - readGitHubIntegrationConfigs, getGitHubFileFetchUrl, GithubCredentialsProvider, + GitHubIntegrationConfig, + readGitHubIntegrationConfigs, } from '@backstage/integration'; +import { RestEndpointMethodTypes } from '@octokit/rest'; import fetch from 'cross-fetch'; import parseGitUrl from 'git-url-parse'; import { Readable } from 'stream'; @@ -98,74 +99,26 @@ export class GithubUrlReader implements UrlReader { url: string, options?: ReadTreeOptions, ): Promise { - const { ref, filepath, full_name } = parseGitUrl(url); - // Caveat: The ref will totally be incorrect if the branch name includes a / - // Thus, readTree can not work on url containing branch name that has a / - - const { headers } = await this.deps.credentialsProvider.getCredentials({ - url, - }); - - // Get GitHub API urls for the repository - const repoGitHubResponse = await fetch( - new URL(`${this.config.apiBaseUrl}/repos/${full_name}`).toString(), - { - headers, - }, - ); - if (!repoGitHubResponse.ok) { - const message = `Failed to read tree (repository) from ${url}, ${repoGitHubResponse.status} ${repoGitHubResponse.statusText}`; - if (repoGitHubResponse.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } - - const repoResponseJson = await repoGitHubResponse.json(); - - // ref is an empty string if no branch is set in provided url to readTree. - // Use GitHub API to get the default branch of the repository. - const branch = ref || repoResponseJson.default_branch; - const branchesApiUrl = repoResponseJson.branches_url; - const archiveApiUrl = repoResponseJson.archive_url; - - // Fetch the latest commit in the provided or default branch to compare against - // the provided sha. - const branchGitHubResponse = await fetch( - // branchesApiUrl looks like "https://api.github.com/repos/owner/repo/branches{/branch}" - branchesApiUrl.replace('{/branch}', `/${branch}`), - { - headers, - }, - ); - if (!branchGitHubResponse.ok) { - const message = `Failed to read tree (branch) from ${url}, ${branchGitHubResponse.status} ${branchGitHubResponse.statusText}`; - if (branchGitHubResponse.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } - const commitSha = (await branchGitHubResponse.json()).commit.sha; + const repoDetails = await this.getRepoDetails(url); + const commitSha = repoDetails.branch.commit.sha!; if (options?.etag && options.etag === commitSha) { throw new NotModifiedError(); } - const archive = await fetch( - // archiveApiUrl looks like "https://api.github.com/repos/owner/repo/{archive_format}{/ref}" - archiveApiUrl + const { headers } = await this.deps.credentialsProvider.getCredentials({ + url, + }); + + // archive_url looks like "https://api.github.com/repos/owner/repo/{archive_format}{/ref}" + const archive = await this.fetchResponse( + repoDetails.repo.archive_url .replace('{archive_format}', 'tarball') .replace('{/ref}', `/${commitSha}`), { headers }, ); - if (!archive.ok) { - const message = `Failed to read tree (archive) from ${url}, ${archive.status} ${archive.statusText}`; - if (archive.status === 404) { - throw new NotFoundError(message); - } - throw new Error(message); - } + const { filepath } = parseGitUrl(url); return await this.deps.treeResponseFactory.fromTarArchive({ // TODO(Rugvip): Underlying implementation of fetch will be node-fetch, we probably want // to stick to using that in exclusively backend code. @@ -180,4 +133,59 @@ export class GithubUrlReader implements UrlReader { const { host, token } = this.config; return `github{host=${host},authed=${Boolean(token)}}`; } + + private async getRepoDetails( + url: string, + ): Promise<{ + repo: RestEndpointMethodTypes['repos']['get']['response']['data']; + branch: RestEndpointMethodTypes['repos']['getBranch']['response']['data']; + }> { + const parsed = parseGitUrl(url); + const { ref, full_name } = parsed; + + // Caveat: The ref will totally be incorrect if the branch name includes a + // slash. Thus, some operations can not work on URLs containing branch + // names that have a slash in them. + + const { headers } = await this.deps.credentialsProvider.getCredentials({ + url, + }); + + const repo: RestEndpointMethodTypes['repos']['get']['response']['data'] = await this.fetchJson( + `${this.config.apiBaseUrl}/repos/${full_name}`, + { headers }, + ); + + // branches_url looks like "https://api.github.com/repos/owner/repo/branches{/branch}" + const branch: RestEndpointMethodTypes['repos']['getBranch']['response']['data'] = await this.fetchJson( + repo.branches_url.replace('{/branch}', `/${ref || repo.default_branch}`), + { headers }, + ); + + return { repo, branch }; + } + + private async fetchResponse( + url: string | URL, + init: RequestInit, + ): Promise { + const urlAsString = url.toString(); + + const response = await fetch(urlAsString, init); + + if (!response.ok) { + const message = `Request failed for ${urlAsString}, ${response.status} ${response.statusText}`; + if (response.status === 404) { + throw new NotFoundError(message); + } + throw new Error(message); + } + + return response; + } + + private async fetchJson(url: string | URL, init: RequestInit): Promise { + const response = await this.fetchResponse(url, init); + return await response.json(); + } } diff --git a/packages/backend-common/src/reading/integration.test.ts b/packages/backend-common/src/reading/integration.test.ts index 2145a4b0fd..16fdf3ff58 100644 --- a/packages/backend-common/src/reading/integration.test.ts +++ b/packages/backend-common/src/reading/integration.test.ts @@ -71,11 +71,17 @@ function withRetries(count: number, fn: () => Promise) { error = err; } } - throw error; + if (!error.message.match(/rate limit|Too Many Requests/)) { + throw error; + } else { + console.warn('Request was rate limited', error); + } }; } describe('UrlReaders', () => { + jest.setTimeout(30_000); + it( 'should read data from azure', withRetries(3, async () => { diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index 96d3a5d495..d22529293f 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -14,16 +14,16 @@ * limitations under the License. */ -import tar, { Parse, ParseStream, ReadEntry } from 'tar'; -import path from 'path'; -import fs from 'fs-extra'; -import { Readable, pipeline as pipelineCb } from 'stream'; -import { promisify } from 'util'; import concatStream from 'concat-stream'; +import fs from 'fs-extra'; +import platformPath from 'path'; +import { pipeline as pipelineCb, Readable } from 'stream'; +import tar, { Parse, ParseStream, ReadEntry } from 'tar'; +import { promisify } from 'util'; import { ReadTreeResponse, - ReadTreeResponseFile, ReadTreeResponseDirOptions, + ReadTreeResponseFile, } from '../types'; // Tar types for `Parse` is not a proper constructor, but it should be @@ -147,7 +147,7 @@ export class TarArchiveResponse implements ReadTreeResponse { const dir = options?.targetDir ?? - (await fs.mkdtemp(path.join(this.workDir, 'backstage-'))); + (await fs.mkdtemp(platformPath.join(this.workDir, 'backstage-'))); // Equivalent of tar --strip-components=N // When no subPath is given, remove just 1 top level directory diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 37ff32741f..2547c7b0e4 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import path from 'path'; -import fs from 'fs-extra'; -import unzipper, { Entry } from 'unzipper'; import archiver from 'archiver'; +import fs from 'fs-extra'; +import platformPath from 'path'; import { Readable } from 'stream'; +import unzipper, { Entry } from 'unzipper'; import { ReadTreeResponse, - ReadTreeResponseFile, ReadTreeResponseDirOptions, + ReadTreeResponseFile, } from '../types'; // Matches a directory name + one `/` at the start of any string, @@ -144,7 +144,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { const dir = options?.targetDir ?? - (await fs.mkdtemp(path.join(this.workDir, 'backstage-'))); + (await fs.mkdtemp(platformPath.join(this.workDir, 'backstage-'))); await this.stream .pipe(unzipper.Parse()) @@ -155,11 +155,11 @@ export class ZipArchiveResponse implements ReadTreeResponse { const entryPath = this.getInnerPath( this.stripTopDirectory(entry.path), ); - const dirname = path.dirname(entryPath); + const dirname = platformPath.dirname(entryPath); if (dirname) { - await fs.mkdirp(path.join(dir, dirname)); + await fs.mkdirp(platformPath.join(dir, dirname)); } - entry.pipe(fs.createWriteStream(path.join(dir, entryPath))); + entry.pipe(fs.createWriteStream(platformPath.join(dir, entryPath))); } else { entry.autodrain(); } diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index f9dfbe9aff..be788fc6e2 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -18,6 +18,32 @@ import { Logger } from 'winston'; import { Config } from '@backstage/config'; import { ReadTreeResponseFactory } from './tree'; +/** + * A generic interface for fetching plain data from URLs. + */ +export type UrlReader = { + read(url: string): Promise; + readTree(url: string, options?: ReadTreeOptions): Promise; +}; + +export type UrlReaderPredicateTuple = { + predicate: (url: URL) => boolean; + reader: UrlReader; +}; + +/** + * A factory function that can read config to construct zero or more + * UrlReaders along with a predicate for when it should be used. + */ +export type ReaderFactory = (options: { + config: Config; + logger: Logger; + treeResponseFactory: ReadTreeResponseFactory; +}) => UrlReaderPredicateTuple[]; + +/** + * An options object for readTree operations. + */ export type ReadTreeOptions = { /** * A filter that can be used to select which files should be included. @@ -47,39 +73,6 @@ export type ReadTreeOptions = { etag?: string; }; -/** - * A generic interface for fetching plain data from URLs. - */ -export type UrlReader = { - read(url: string): Promise; - readTree(url: string, options?: ReadTreeOptions): Promise; -}; - -export type UrlReaderPredicateTuple = { - predicate: (url: URL) => boolean; - reader: UrlReader; -}; - -/** - * A factory function that can read config to construct zero or more - * UrlReaders along with a predicate for when it should be used. - */ -export type ReaderFactory = (options: { - config: Config; - logger: Logger; - treeResponseFactory: ReadTreeResponseFactory; -}) => UrlReaderPredicateTuple[]; - -export type ReadTreeResponseFile = { - path: string; - content(): Promise; -}; - -export type ReadTreeResponseDirOptions = { - /** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */ - targetDir?: string; -}; - export type ReadTreeResponse = { /** * files() returns an array of all the files inside the tree and corresponding functions to read their content. @@ -97,3 +90,16 @@ export type ReadTreeResponse = { */ etag: string; }; + +export type ReadTreeResponseDirOptions = { + /** The directory to write files to. Defaults to the OS tmpdir or `backend.workingDirectory` if set in config */ + targetDir?: string; +}; + +/** + * Represents a single file in a readTree response. + */ +export type ReadTreeResponseFile = { + path: string; + content(): Promise; +}; diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 215e4e7296..f85c22784d 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,31 @@ # example-backend +## 0.2.14 + +### Patch Changes + +- Updated dependencies [c777df180] +- Updated dependencies [2430ee7c2] +- Updated dependencies [3149bfe63] +- Updated dependencies [6e612ce25] +- Updated dependencies [e44925723] +- Updated dependencies [9d6ef14bc] +- Updated dependencies [a26668913] +- Updated dependencies [025e122c3] +- Updated dependencies [e9aab60c7] +- Updated dependencies [24e47ef1e] +- Updated dependencies [7881f2117] +- Updated dependencies [529d16d27] +- Updated dependencies [cdea0baf1] +- Updated dependencies [11cb5ef94] + - @backstage/plugin-techdocs-backend@0.5.5 + - @backstage/backend-common@0.5.2 + - @backstage/plugin-catalog-backend@0.6.0 + - @backstage/catalog-model@0.7.1 + - example-app@0.2.14 + - @backstage/plugin-scaffolder-backend@0.6.0 + - @backstage/plugin-app-backend@0.3.6 + ## 0.2.13 ### Patch Changes diff --git a/packages/backend/package.json b/packages/backend/package.json index aa00407952..544bb976fe 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.13", + "version": "0.2.14", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -27,24 +27,24 @@ "migrate:create": "knex migrate:make -x ts" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/plugin-app-backend": "^0.3.5", + "@backstage/plugin-app-backend": "^0.3.6", "@backstage/plugin-auth-backend": "^0.2.12", - "@backstage/plugin-catalog-backend": "^0.5.5", + "@backstage/plugin-catalog-backend": "^0.6.0", "@backstage/plugin-graphql-backend": "^0.1.5", "@backstage/plugin-kubernetes-backend": "^0.2.6", "@backstage/plugin-kafka-backend": "^0.2.0", "@backstage/plugin-proxy-backend": "^0.2.4", "@backstage/plugin-rollbar-backend": "^0.1.7", - "@backstage/plugin-scaffolder-backend": "^0.5.2", - "@backstage/plugin-techdocs-backend": "^0.5.4", + "@backstage/plugin-scaffolder-backend": "^0.6.0", + "@backstage/plugin-techdocs-backend": "^0.5.5", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", "azure-devops-node-api": "^10.1.1", "dockerode": "^3.2.1", - "example-app": "^0.2.13", + "example-app": "^0.2.14", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", @@ -54,7 +54,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "@types/express-serve-static-core": "^4.17.5" diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts index 4e2257a46c..723165ff82 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -30,6 +30,7 @@ import Docker from 'dockerode'; export default async function createPlugin({ logger, config, + database, }: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); @@ -54,5 +55,6 @@ export default async function createPlugin({ config, dockerClient, entityClient, + database, }); } diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json index c1cbd6157f..00b05e37bf 100644 --- a/packages/catalog-client/package.json +++ b/packages/catalog-client/package.json @@ -29,12 +29,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "cross-fetch": "^3.0.6" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/jest": "^26.0.7", "msw": "^0.21.2" }, diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 6a081c3336..8d581dcb0c 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,14 @@ # @backstage/catalog-model +## 0.7.1 + +### Patch Changes + +- 6e612ce25: Adds a new optional `links` metadata field to the Entity class within the `catalog-model` package (as discussed in [[RFC] Entity Links](https://github.com/backstage/backstage/issues/3787)). This PR adds support for the entity links only. Follow up PR's will introduce the UI component to display them. +- 025e122c3: Replace `yup` with `ajv`, for validation of catalog entities. +- 7881f2117: Introduce json schema variants of the `yup` validation schemas +- 11cb5ef94: Implement matchEntityWithRef for client side filtering of entities by ref matching + ## 0.7.0 ### Minor Changes diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index e81bc3a9b6..af74b78a08 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.7.0", + "version": "0.7.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -39,7 +39,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index 4afb8f5c96..e80e14f7a4 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -27,6 +27,7 @@ export type { } from './Entity'; export * from './policies'; export { + compareEntityToRef, getEntityName, parseEntityName, parseEntityRef, diff --git a/packages/catalog-model/src/entity/ref.test.ts b/packages/catalog-model/src/entity/ref.test.ts index bd7cc9477d..5ca511ee03 100644 --- a/packages/catalog-model/src/entity/ref.test.ts +++ b/packages/catalog-model/src/entity/ref.test.ts @@ -16,7 +16,12 @@ import { ENTITY_DEFAULT_NAMESPACE } from './constants'; import { Entity } from './Entity'; -import { parseEntityName, parseEntityRef, serializeEntityRef } from './ref'; +import { + compareEntityToRef, + parseEntityName, + parseEntityRef, + serializeEntityRef, +} from './ref'; describe('ref', () => { describe('parseEntityName', () => { @@ -381,4 +386,320 @@ describe('ref', () => { ).toEqual({ kind: 'a', namespace: 'b', name: 'c/x' }); }); }); + + describe('compareEntityToRef', () => { + const entityWithNamespace: Entity = { + apiVersion: 'a', + kind: 'K', + metadata: { + name: 'n', + namespace: 'ns', + }, + }; + const entityWithoutNamespace: Entity = { + apiVersion: 'a', + kind: 'K', + metadata: { + name: 'n', + }, + }; + + it('handles matching string refs', () => { + expect(compareEntityToRef(entityWithNamespace, 'K:ns/n')).toBe(true); + expect(compareEntityToRef(entityWithNamespace, 'k:nS/N')).toBe(true); + expect( + compareEntityToRef(entityWithNamespace, 'K:n', { + defaultNamespace: 'ns', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithNamespace, 'K:n', { + defaultNamespace: 'Ns', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithNamespace, 'ns/n', { defaultKind: 'K' }), + ).toBe(true); + expect( + compareEntityToRef(entityWithNamespace, 'n', { + defaultKind: 'K', + defaultNamespace: 'ns', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithNamespace, 'N', { + defaultKind: 'k', + defaultNamespace: 'nS', + }), + ).toBe(true); + + expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe( + true, + ); + expect(compareEntityToRef(entityWithoutNamespace, 'K:deFault/n')).toBe( + true, + ); + expect( + compareEntityToRef(entityWithoutNamespace, 'K:n', { + defaultNamespace: 'default', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithoutNamespace, 'K:n', { + defaultNamespace: 'deFault', + }), + ).toBe(true); + expect(compareEntityToRef(entityWithoutNamespace, 'K:default/n')).toBe( + true, + ); + expect(compareEntityToRef(entityWithoutNamespace, 'K:n')).toBe(true); + expect( + compareEntityToRef(entityWithoutNamespace, 'default/n', { + defaultKind: 'K', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithoutNamespace, 'n', { + defaultKind: 'K', + defaultNamespace: 'default', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithoutNamespace, 'n', { + defaultKind: 'K', + }), + ).toBe(true); + }); + + it('handles mismatching string refs', () => { + expect(compareEntityToRef(entityWithNamespace, 'X:ns/n')).toBe(false); + expect( + compareEntityToRef(entityWithoutNamespace, 'ns/n', { + defaultKind: 'X', + }), + ).toBe(false); + + expect(compareEntityToRef(entityWithNamespace, 'K:xx/n')).toBe(false); + expect( + compareEntityToRef(entityWithoutNamespace, 'K:n', { + defaultNamespace: 'xx', + }), + ).toBe(false); + + expect(compareEntityToRef(entityWithNamespace, 'K:ns/x')).toBe(false); + expect( + compareEntityToRef(entityWithoutNamespace, 'x', { + defaultKind: 'K', + defaultNamespace: 'ns', + }), + ).toBe(false); + }); + + it('handles matching compound refs', () => { + expect( + compareEntityToRef(entityWithNamespace, { + kind: 'K', + namespace: 'ns', + name: 'n', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithNamespace, { + kind: 'k', + namespace: 'Ns', + name: 'N', + }), + ).toBe(true); + expect( + compareEntityToRef( + entityWithNamespace, + { kind: 'K', name: 'n' }, + { + defaultNamespace: 'ns', + }, + ), + ).toBe(true); + expect( + compareEntityToRef( + entityWithNamespace, + { namespace: 'ns', name: 'n' }, + { defaultKind: 'K' }, + ), + ).toBe(true); + expect( + compareEntityToRef(entityWithNamespace, 'n', { + defaultKind: 'K', + defaultNamespace: 'ns', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithNamespace, 'N', { + defaultKind: 'k', + defaultNamespace: 'nS', + }), + ).toBe(true); + + expect( + compareEntityToRef(entityWithoutNamespace, { + kind: 'K', + namespace: 'default', + name: 'n', + }), + ).toBe(true); + expect( + compareEntityToRef(entityWithoutNamespace, { + kind: 'k', + namespace: 'deFault', + name: 'N', + }), + ).toBe(true); + expect( + compareEntityToRef( + entityWithoutNamespace, + { kind: 'K', name: 'n' }, + { + defaultNamespace: 'default', + }, + ), + ).toBe(true); + expect( + compareEntityToRef(entityWithoutNamespace, { kind: 'K', name: 'n' }), + ).toBe(true); + expect( + compareEntityToRef( + entityWithoutNamespace, + { namespace: 'default', name: 'n' }, + { + defaultKind: 'K', + }, + ), + ).toBe(true); + expect( + compareEntityToRef( + entityWithoutNamespace, + { name: 'n' }, + { + defaultKind: 'K', + defaultNamespace: 'default', + }, + ), + ).toBe(true); + expect( + compareEntityToRef( + entityWithoutNamespace, + { name: 'N' }, + { + defaultKind: 'k', + defaultNamespace: 'defAult', + }, + ), + ).toBe(true); + expect( + compareEntityToRef( + entityWithoutNamespace, + { name: 'n' }, + { + defaultKind: 'K', + }, + ), + ).toBe(true); + }); + + it('handles mismatching compound refs', () => { + expect( + compareEntityToRef(entityWithNamespace, { + kind: 'X', + namespace: 'ns', + name: 'n', + }), + ).toBe(false); + expect( + compareEntityToRef( + entityWithNamespace, + { + namespace: 'ns', + name: 'n', + }, + { defaultKind: 'X' }, + ), + ).toBe(false); + expect( + compareEntityToRef(entityWithoutNamespace, { + kind: 'X', + namespace: 'default', + name: 'n', + }), + ).toBe(false); + expect( + compareEntityToRef( + entityWithoutNamespace, + { + namespace: 'default', + name: 'n', + }, + { defaultKind: 'X' }, + ), + ).toBe(false); + + expect( + compareEntityToRef(entityWithNamespace, { + kind: 'K', + namespace: 'xx', + name: 'n', + }), + ).toBe(false); + expect( + compareEntityToRef( + entityWithNamespace, + { + kind: 'K', + name: 'n', + }, + { defaultNamespace: 'xx' }, + ), + ).toBe(false); + expect( + compareEntityToRef(entityWithoutNamespace, { + kind: 'K', + namespace: 'xx', + name: 'n', + }), + ).toBe(false); + expect( + compareEntityToRef( + entityWithoutNamespace, + { + kind: 'K', + name: 'n', + }, + { defaultNamespace: 'xx' }, + ), + ).toBe(false); + + expect( + compareEntityToRef(entityWithNamespace, { + kind: 'K', + namespace: 'ns', + name: 'x', + }), + ).toBe(false); + expect( + compareEntityToRef(entityWithoutNamespace, { + kind: 'K', + namespace: 'default', + name: 'x', + }), + ).toBe(false); + expect( + compareEntityToRef( + entityWithoutNamespace, + { + kind: 'K', + name: 'x', + }, + { defaultNamespace: 'default' }, + ), + ).toBe(false); + }); + }); }); diff --git a/packages/catalog-model/src/entity/ref.ts b/packages/catalog-model/src/entity/ref.ts index 9f6f175d8d..bf34962d01 100644 --- a/packages/catalog-model/src/entity/ref.ts +++ b/packages/catalog-model/src/entity/ref.ts @@ -18,6 +18,27 @@ import { EntityName, EntityRef } from '../types'; import { ENTITY_DEFAULT_NAMESPACE } from './constants'; import { Entity } from './Entity'; +function parseRefString( + ref: string, +): { + kind?: string; + namespace?: string; + name: string; +} { + const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref.trim()); + if (!match) { + throw new TypeError( + `Entity reference "${ref}" was not on the form [:][/]`, + ); + } + + return { + kind: match[1]?.slice(0, -1), + namespace: match[2]?.slice(0, -1), + name: match[3], + }; +} + /** * Extracts the kind, namespace and name that form the name triplet of the * given entity. @@ -121,17 +142,11 @@ export function parseEntityRef( } if (typeof ref === 'string') { - const match = /^([^:/]+:)?([^:/]+\/)?([^:/]+)$/.exec(ref.trim()); - if (!match) { - throw new Error( - `Entity reference "${ref}" was not on the form [:][/]`, - ); - } - + const parsed = parseRefString(ref); return { - kind: match[1]?.slice(0, -1) ?? context.defaultKind, - namespace: match[2]?.slice(0, -1) ?? context.defaultNamespace, - name: match[3], + kind: parsed.kind ?? context.defaultKind, + namespace: parsed.namespace ?? context.defaultNamespace, + name: parsed.name, }; } @@ -196,3 +211,53 @@ export function serializeEntityRef( return `${kind ? `${kind}:` : ''}${namespace ? `${namespace}/` : ''}${name}`; } + +/** + * Compares an entity to either a string reference or a compound reference. + * + * The comparison is case insensitive, and all of kind, namespace, and name + * must match (after applying the optional context to the ref). + * + * @param entity The entity to match + * @param ref A string or compound entity ref + * @param context An optional context of default kind and namespace, that apply + * to the ref if given + * @returns True if matching, false otherwise + */ +export function compareEntityToRef( + entity: Entity, + ref: EntityRef | EntityName, + context?: EntityRefContext, +): boolean { + const entityKind = entity.kind; + const entityNamespace = entity.metadata.namespace || ENTITY_DEFAULT_NAMESPACE; + const entityName = entity.metadata.name; + + let refKind: string | undefined; + let refNamespace: string | undefined; + let refName: string; + if (typeof ref === 'string') { + const parsed = parseRefString(ref); + refKind = parsed.kind || context?.defaultKind; + refNamespace = + parsed.namespace || context?.defaultNamespace || ENTITY_DEFAULT_NAMESPACE; + refName = parsed.name; + } else { + refKind = ref.kind || context?.defaultKind; + refNamespace = + ref.namespace || context?.defaultNamespace || ENTITY_DEFAULT_NAMESPACE; + refName = ref.name; + } + + if (!refKind || !refNamespace) { + throw new Error( + `Entity reference or context did not contain kind and namespace`, + ); + } + + return ( + entityKind.toLowerCase() === refKind.toLowerCase() && + entityNamespace.toLowerCase() === refNamespace.toLowerCase() && + entityName.toLowerCase() === refName.toLowerCase() + ); +} diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 29cce97ae9..c40f7605eb 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,48 @@ # @backstage/cli +## 0.6.0 + +### Minor Changes + +- 19fe61c27: We have updated the default `eslint` rules in the `@backstage/cli` package. + + ```diff + -'@typescript-eslint/no-shadow': 'off', + -'@typescript-eslint/no-redeclare': 'off', + +'no-shadow': 'off', + +'no-redeclare': 'off', + +'@typescript-eslint/no-shadow': 'error', + +'@typescript-eslint/no-redeclare': 'error', + ``` + + The rules are documented [here](https://eslint.org/docs/rules/no-shadow) and [here](https://eslint.org/docs/rules/no-redeclare). + + This involved a large number of small changes to the code base. When you compile your own code using the CLI, you may also be + affected. We consider these rules important, and the primary recommendation is to try to update your code according to the + documentation above. But those that prefer to not enable the rules, or need time to perform the updates, may update their + local `.eslintrc.js` file(s) in the repo root and/or in individual plugins as they see fit: + + ```js + module.exports = { + // ... other declarations + rules: { + '@typescript-eslint/no-shadow': 'off', + '@typescript-eslint/no-redeclare': 'off', + }, + }; + ``` + + Because of the nature of this change, we're unable to provide a grace period for the update :( + +### Patch Changes + +- 398e1f83e: Update `create-plugin` template to use the new composability API, by switching to exporting a single routable extension component. +- e9aab60c7: Fixed module resolution of external libraries during backend development. Modules used to be resolved relative to the backend entrypoint, but are now resolved relative to each individual module. +- a08c4b0b0: Add check for outdated/duplicate packages to yarn start +- Updated dependencies [062df71db] +- Updated dependencies [e9aab60c7] + - @backstage/config-loader@0.5.1 + ## 0.5.0 ### Minor Changes diff --git a/packages/cli/config/eslint.backend.js b/packages/cli/config/eslint.backend.js index 7acb538d38..e7fd4cb688 100644 --- a/packages/cli/config/eslint.backend.js +++ b/packages/cli/config/eslint.backend.js @@ -38,8 +38,10 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], rules: { - '@typescript-eslint/no-shadow': 'off', - '@typescript-eslint/no-redeclare': 'off', + 'no-shadow': 'off', + 'no-redeclare': 'off', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-redeclare': 'error', 'no-console': 0, // Permitted in console programs 'new-cap': ['error', { capIsNew: false }], // Because Express constructs things e.g. like 'const r = express.Router()' diff --git a/packages/cli/config/eslint.js b/packages/cli/config/eslint.js index 044d737d15..37bd7bed94 100644 --- a/packages/cli/config/eslint.js +++ b/packages/cli/config/eslint.js @@ -45,8 +45,10 @@ module.exports = { }, ignorePatterns: ['.eslintrc.js', '**/dist/**', '**/dist-types/**'], rules: { - '@typescript-eslint/no-shadow': 'off', - '@typescript-eslint/no-redeclare': 'off', + 'no-shadow': 'off', + 'no-redeclare': 'off', + '@typescript-eslint/no-shadow': 'error', + '@typescript-eslint/no-redeclare': 'error', 'no-undef': 'off', 'import/newline-after-import': 'error', 'import/no-duplicates': 'warn', diff --git a/packages/cli/package.json b/packages/cli/package.json index ed373f6058..9aaeb00459 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.5.0", + "version": "0.6.0", "private": false, "publishConfig": { "access": "public" @@ -30,7 +30,7 @@ "dependencies": { "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.2", - "@backstage/config-loader": "^0.5.0", + "@backstage/config-loader": "^0.5.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^3.18.5", "@lerna/project": "^3.18.0", @@ -93,7 +93,7 @@ "rollup-plugin-esbuild": "2.6.x", "rollup-plugin-peer-deps-external": "^2.2.2", "rollup-plugin-postcss": "^3.1.1", - "rollup-plugin-typescript2": "^0.27.3", + "rollup-plugin-typescript2": "^0.29.0", "rollup-pluginutils": "^2.8.2", "semver": "^7.3.2", "start-server-webpack-plugin": "^2.2.5", @@ -113,12 +113,12 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.5.1", + "@backstage/backend-common": "^0.5.2", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/core": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", - "@backstage/theme": "^0.2.2", + "@backstage/theme": "^0.2.3", "@types/diff": "^4.0.2", "@types/express": "^4.17.6", "@types/fs-extra": "^9.0.1", diff --git a/packages/cli/src/commands/config/print.ts b/packages/cli/src/commands/config/print.ts index 56dcd3f753..930f2c98ca 100644 --- a/packages/cli/src/commands/config/print.ts +++ b/packages/cli/src/commands/config/print.ts @@ -63,8 +63,8 @@ function serializeConfigData( } const sanitizedConfigs = schema.process(appConfigs, { - valueTransform: (value, { visibility }) => - visibility === 'secret' ? '' : value, + valueTransform: (value, context) => + context.visibility === 'secret' ? '' : value, }); return ConfigReader.fromConfigs(sanitizedConfigs).get(); diff --git a/packages/cli/src/commands/versions/bump.test.ts b/packages/cli/src/commands/versions/bump.test.ts index 987d4f6f31..31803d4827 100644 --- a/packages/cli/src/commands/versions/bump.test.ts +++ b/packages/cli/src/commands/versions/bump.test.ts @@ -108,7 +108,7 @@ describe('bump', () => { paths.targetDir = '/'; jest .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...paths) => resolvePath('/', ...paths)); + .mockImplementation((...path) => resolvePath('/', ...path)); jest.spyOn(runObj, 'runPlain').mockImplementation(async (...[, , , name]) => JSON.stringify({ type: 'inspect', @@ -204,7 +204,7 @@ describe('bump', () => { paths.targetDir = '/'; jest .spyOn(paths, 'resolveTargetRoot') - .mockImplementation((...paths) => resolvePath('/', ...paths)); + .mockImplementation((...path) => resolvePath('/', ...path)); jest.spyOn(runObj, 'runPlain').mockImplementation(async () => ''); jest.spyOn(runObj, 'run').mockResolvedValue(undefined); diff --git a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts index 0478337c61..fed8b39a19 100644 --- a/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts +++ b/packages/cli/src/lib/bundler/LinkedPackageResolvePlugin.ts @@ -41,7 +41,7 @@ export class LinkedPackageResolvePlugin implements ResolvePlugin { callback: () => void, ) => { const pkg = this.packages.find( - pkg => data.path && isChildPath(pkg.location, data.path), + pkge => data.path && isChildPath(pkge.location, data.path), ); if (!pkg) { callback(); diff --git a/packages/cli/src/lib/bundler/paths.ts b/packages/cli/src/lib/bundler/paths.ts index f38416ca4e..c8d48a7199 100644 --- a/packages/cli/src/lib/bundler/paths.ts +++ b/packages/cli/src/lib/bundler/paths.ts @@ -42,14 +42,14 @@ export type BundlingPathsOptions = { export function resolveBundlingPaths(options: BundlingPathsOptions) { const { entry } = options; - const resolveTargetModule = (path: string) => { + const resolveTargetModule = (pathString: string) => { for (const ext of ['mjs', 'js', 'ts', 'tsx', 'jsx']) { - const filePath = paths.resolveTarget(`${path}.${ext}`); + const filePath = paths.resolveTarget(`${pathString}.${ext}`); if (fs.pathExistsSync(filePath)) { return filePath; } } - return paths.resolveTarget(`${path}.js`); + return paths.resolveTarget(`${pathString}.js`); }; let targetPublic = undefined; diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md index 2079c85096..cbc6473286 100644 --- a/packages/config-loader/CHANGELOG.md +++ b/packages/config-loader/CHANGELOG.md @@ -1,5 +1,12 @@ # @backstage/config-loader +## 0.5.1 + +### Patch Changes + +- 062df71db: Bump `config-loader` to `ajv` 7, to enable v7 feature use elsewhere +- e9aab60c7: Each piece of the configuration schema is now validated upfront, in order to produce more informative errors. + ## 0.5.0 ### Minor Changes diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 3387477231..0b024b2c84 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config-loader", "description": "Config loading functionality used by Backstage backend, and CLI", - "version": "0.5.0", + "version": "0.5.1", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config-loader/src/lib/schema/collect.ts b/packages/config-loader/src/lib/schema/collect.ts index 0e53562875..611413931e 100644 --- a/packages/config-loader/src/lib/schema/collect.ts +++ b/packages/config-loader/src/lib/schema/collect.ts @@ -118,7 +118,9 @@ export async function collectConfigSchemas( } await Promise.all( - depNames.map(name => processItem({ name, parentPath: pkgPath })), + depNames.map(depName => + processItem({ name: depName, parentPath: pkgPath }), + ), ); } diff --git a/packages/core-api/package.json b/packages/core-api/package.json index bbea58e846..927eca3490 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -30,7 +30,7 @@ }, "dependencies": { "@backstage/config": "^0.1.2", - "@backstage/theme": "^0.2.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@types/react": "^16.9", @@ -42,7 +42,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@backstage/test-utils": "^0.1.6", "@backstage/test-utils-core": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", diff --git a/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts index 517bf82ae7..e7764fcbaa 100644 --- a/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/DirectAuthConnector.ts @@ -13,12 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { - AuthProvider, - ProfileInfo, - BackstageIdentity, - DiscoveryApi, -} from '../../apis/definitions'; +import { AuthProvider, DiscoveryApi } from '../../apis/definitions'; import { showLoginPopup } from '../loginPopup'; type Options = { @@ -26,13 +21,6 @@ type Options = { environment?: string; provider: AuthProvider & { id: string }; }; - -export type DirectAuthResponse = { - userId: string; - profile: ProfileInfo; - backstageIdentity: BackstageIdentity; -}; - export class DirectAuthConnector { private readonly discoveryApi: DiscoveryApi; private readonly environment: string | undefined; diff --git a/packages/core-api/src/routing/FlatRoutes.tsx b/packages/core-api/src/routing/FlatRoutes.tsx index a6783964a7..cefb347290 100644 --- a/packages/core-api/src/routing/FlatRoutes.tsx +++ b/packages/core-api/src/routing/FlatRoutes.tsx @@ -25,8 +25,8 @@ type RouteObject = { // Similar to the same function from react-router, this collects routes from the // children, but only the first level of routes -function createRoutesFromChildren(children: ReactNode): RouteObject[] { - return Children.toArray(children) +function createRoutesFromChildren(childrenNode: ReactNode): RouteObject[] { + return Children.toArray(childrenNode) .flatMap(child => { if (!isValidElement(child)) { return []; diff --git a/packages/core-api/src/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts index 4c2da34098..0a3df49b58 100644 --- a/packages/core-api/src/routing/RouteRef.ts +++ b/packages/core-api/src/routing/RouteRef.ts @@ -14,7 +14,16 @@ * limitations under the License. */ -import { RouteRefConfig, RouteRef } from './types'; +import { RouteRef } from './types'; +import { IconComponent } from '../icons'; + +export type RouteRefConfig = { + params?: Array; + /** @deprecated Route refs no longer decide their own path */ + path?: string; + icon?: IconComponent; + title: string; +}; export class AbsoluteRouteRef { constructor(private readonly config: RouteRefConfig) {} @@ -38,9 +47,20 @@ export class AbsoluteRouteRef { } export function createRouteRef< - ParamKeys extends string, - Params extends { [param in string]: string } = { [name in ParamKeys]: string } ->(config: RouteRefConfig): RouteRef { + // Params is the type that we care about and the one to be embedded in the route ref. + // For example, given the params ['name', 'kind'], Params will be {name: string, kind: string} + Params extends { [param in ParamKey]: string }, + // ParamKey is here to make sure the Params type properly has its keys narrowed down + // to only the elements of params. Defaulting to never makes sure we end up with + // Param = {} if the params array is empty. + ParamKey extends string = never +>(config: { + params?: ParamKey[]; + /** @deprecated Route refs no longer decide their own path */ + path?: string; + icon?: IconComponent; + title: string; +}): RouteRef { return new AbsoluteRouteRef(config); } diff --git a/packages/core-api/src/routing/hooks.test.tsx b/packages/core-api/src/routing/hooks.test.tsx index 3dbeaf8e3e..84a8314807 100644 --- a/packages/core-api/src/routing/hooks.test.tsx +++ b/packages/core-api/src/routing/hooks.test.tsx @@ -39,8 +39,9 @@ import { createRouteRef, createExternalRouteRef, ExternalRouteRef, + RouteRefConfig, } from './RouteRef'; -import { RouteRef, RouteRefConfig } from './types'; +import { RouteRef } from './types'; const mockConfig = (extra?: Partial>) => ({ path: '/unused', diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index 9564b3b225..af71e0c3e9 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -16,11 +16,11 @@ export type { RouteRef, - RouteRefConfig, AbsoluteRouteRef, ConcreteRoute, MutableRouteRef, } from './types'; export { FlatRoutes } from './FlatRoutes'; export { createRouteRef } from './RouteRef'; +export type { RouteRefConfig } from './RouteRef'; export { useRouteRef } from './hooks'; diff --git a/packages/core-api/src/routing/types.ts b/packages/core-api/src/routing/types.ts index 99a20c31e9..5f9376c1d2 100644 --- a/packages/core-api/src/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -45,14 +45,6 @@ export type AbsoluteRouteRef = RouteRef<{}>; */ export type MutableRouteRef = RouteRef<{}>; -export type RouteRefConfig = { - params?: Array; - /** @deprecated Route refs no longer decide their own path */ - path?: string; - icon?: IconComponent; - title: string; -}; - // A duplicate of the react-router RouteObject, but with routeRef added export interface BackstageRouteObject { caseSensitive: boolean; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index 0fea08e539..60a6e43654 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/core +## 0.6.0 + +### Minor Changes + +- 21e624ba9: Closes #3556 + The scroll bar of collapsed sidebar is now hidden without full screen. + + ![image](https://user-images.githubusercontent.com/46953622/105390193-0bfd0080-5c19-11eb-8e86-2161bbe6e8d9.png) + +### Patch Changes + +- 12ece98cd: Add className to the SidebarItem +- d82246867: Update `WarningPanel` component to use accordion-style expansion +- 5fa3bdb55: Add `href` in addition to `onClick` to `ItemCard`. Ensure that the height of a + `ItemCard` with and without tags is equal. +- da9f53c60: Add a `prop` union for `SignInPage` that allows it to be used for just a single provider, with inline errors, and optionally with automatic sign-in. +- 32c95605f: Fix check that determines whether popup was closed or the messaging was misconfigured. +- 54c7d02f7: Introduce `TabbedLayout` for creating tabs that are routed. + + ```typescript + + +
This is rendered under /example/anything-here route
+
+
+ ``` + +- Updated dependencies [c810082ae] + - @backstage/theme@0.2.3 + ## 0.5.0 ### Minor Changes diff --git a/packages/core/package.json b/packages/core/package.json index 49090e1362..41dbc8ef14 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core", "description": "Core API used by Backstage plugins and apps", - "version": "0.5.0", + "version": "0.6.0", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "dependencies": { "@backstage/config": "^0.1.2", "@backstage/core-api": "^0.2.8", - "@backstage/theme": "^0.2.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -66,7 +66,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/catalog/src/components/EntityLayout/TabbedLayout.test.tsx b/packages/core/src/components/TabbedLayout/RoutedTabs.test.tsx similarity index 92% rename from plugins/catalog/src/components/EntityLayout/TabbedLayout.test.tsx rename to packages/core/src/components/TabbedLayout/RoutedTabs.test.tsx index 065265cea4..549aa55eff 100644 --- a/plugins/catalog/src/components/EntityLayout/TabbedLayout.test.tsx +++ b/packages/core/src/components/TabbedLayout/RoutedTabs.test.tsx @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { TabbedLayout } from './TabbedLayout'; import { renderInTestApp } from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; +import React from 'react'; import { act } from 'react-dom/test-utils'; -import { Routes, Route } from 'react-router'; +import { Route, Routes } from 'react-router'; +import { RoutedTabs } from './RoutedTabs'; const testRoute1 = { path: '', @@ -31,10 +31,10 @@ const testRoute2 = { children:
tabbed-test-content-2
, }; -describe('TabbedLayout', () => { +describe('RoutedTabs', () => { it('renders simplest case', async () => { const rendered = await renderInTestApp( - , + , ); expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); @@ -46,7 +46,7 @@ describe('TabbedLayout', () => { } + element={} /> , ); @@ -70,7 +70,7 @@ describe('TabbedLayout', () => { { it('shows only one tab contents at a time', async () => { const rendered = await renderInTestApp( - , + , { routeEntries: ['/some-other-path'] }, ); @@ -135,7 +135,7 @@ describe('TabbedLayout', () => { it('redirects to the top level when no route is matching the url', async () => { const rendered = await renderInTestApp( - , + , { routeEntries: ['/non-existing-path'] }, ); diff --git a/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx b/packages/core/src/components/TabbedLayout/RoutedTabs.tsx similarity index 84% rename from plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx rename to packages/core/src/components/TabbedLayout/RoutedTabs.tsx index a2209ba492..b17c8afb3b 100644 --- a/plugins/catalog/src/components/EntityLayout/TabbedLayout.tsx +++ b/packages/core/src/components/TabbedLayout/RoutedTabs.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ import React, { useMemo } from 'react'; -import { useParams, useNavigate, matchRoutes, useRoutes } from 'react-router'; -import { HeaderTabs, Content as LayoutContent } from '@backstage/core'; import { Helmet } from 'react-helmet'; +import { matchRoutes, useNavigate, useParams, useRoutes } from 'react-router'; +import { Content, HeaderTabs } from '../../layout'; import { SubRoute } from './types'; export function useSelectedSubRoute( @@ -44,7 +44,7 @@ export function useSelectedSubRoute( }; } -export const TabbedLayout = ({ routes }: { routes: SubRoute[] }) => { +export const RoutedTabs = ({ routes }: { routes: SubRoute[] }) => { const navigate = useNavigate(); const { index, route, element } = useSelectedSubRoute(routes); const headerTabs = useMemo( @@ -52,12 +52,12 @@ export const TabbedLayout = ({ routes }: { routes: SubRoute[] }) => { [routes], ); - const onTabChange = (index: number) => + const onTabChange = (tabIndex: number) => // Remove trailing /* // And remove leading / for relative navigation // Note! route resolves relative to the position in the React tree, // not relative to current location - navigate(routes[index].path.replace(/\/\*$/, '').replace(/^\//, '')); + navigate(routes[tabIndex].path.replace(/\/\*$/, '').replace(/^\//, '')); return ( <> @@ -66,10 +66,10 @@ export const TabbedLayout = ({ routes }: { routes: SubRoute[] }) => { selectedIndex={index} onChange={onTabChange} /> - + {element} - + ); }; diff --git a/packages/core/src/components/TabbedLayout/TabbedLayout.stories.tsx b/packages/core/src/components/TabbedLayout/TabbedLayout.stories.tsx new file mode 100644 index 0000000000..bf6175a9a2 --- /dev/null +++ b/packages/core/src/components/TabbedLayout/TabbedLayout.stories.tsx @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { PropsWithChildren } from 'react'; +import { MemoryRouter, Route, Routes } from 'react-router'; +import { TabbedLayout } from './TabbedLayout'; + +export default { + title: 'Navigation/TabbedLayout', + component: TabbedLayout, +}; + +const Wrapper = ({ children }: PropsWithChildren<{}>) => ( + + + {children}} /> + + +); + +export const Default = () => ( + + + +
tabbed-test-content
+
+ +
tabbed-test-content-2
+
+
+
+); diff --git a/packages/core/src/components/TabbedLayout/TabbedLayout.test.tsx b/packages/core/src/components/TabbedLayout/TabbedLayout.test.tsx new file mode 100644 index 0000000000..77230ab6cd --- /dev/null +++ b/packages/core/src/components/TabbedLayout/TabbedLayout.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; +import { fireEvent } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; +import { Route, Routes } from 'react-router'; +import { TabbedLayout } from './TabbedLayout'; + +describe('TabbedLayout', () => { + it('renders simplest case', async () => { + const { getByText } = await renderInTestApp( + + +
tabbed-test-content
+
+
, + ); + + expect(getByText('tabbed-test-title')).toBeInTheDocument(); + expect(getByText('tabbed-test-content')).toBeInTheDocument(); + }); + + it('throws if any other component is a child of TabbedLayout', async () => { + const { error } = await withLogCollector(async () => { + await expect( + renderInTestApp( + + +
tabbed-test-content
+
+
This will cause app to throw
+
, + ), + ).rejects.toThrow(/Child of TabbedLayout must be an TabbedLayout.Route/); + }); + + expect(error).toEqual([ + expect.stringMatching( + /Child of TabbedLayout must be an TabbedLayout.Route/, + ), + expect.stringMatching( + /The above error occurred in the component/, + ), + ]); + }); + + it('navigates when user clicks different tab', async () => { + const { getByText, queryByText, queryAllByRole } = await renderInTestApp( + + + +
tabbed-test-content
+
+ +
tabbed-test-content-2
+
+
+ } + /> + , + ); + + const secondTab = queryAllByRole('tab')[1]; + act(() => { + fireEvent.click(secondTab); + }); + + expect(getByText('tabbed-test-title')).toBeInTheDocument(); + expect(queryByText('tabbed-test-content')).not.toBeInTheDocument(); + + expect(getByText('tabbed-test-title-2')).toBeInTheDocument(); + expect(queryByText('tabbed-test-content-2')).toBeInTheDocument(); + }); +}); diff --git a/packages/core/src/components/TabbedLayout/TabbedLayout.tsx b/packages/core/src/components/TabbedLayout/TabbedLayout.tsx new file mode 100644 index 0000000000..f181ee5980 --- /dev/null +++ b/packages/core/src/components/TabbedLayout/TabbedLayout.tsx @@ -0,0 +1,89 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { attachComponentData } from '@backstage/core-api'; +import React, { + Children, + Fragment, + isValidElement, + PropsWithChildren, + ReactNode, +} from 'react'; +import { RoutedTabs } from './RoutedTabs'; + +type SubRoute = { + path: string; + title: string; + children: JSX.Element; +}; + +const Route: (props: SubRoute) => null = () => null; + +// This causes all mount points that are discovered within this route to use the path of the route itself +attachComponentData(Route, 'core.gatherMountPoints', true); + +export function createSubRoutesFromChildren( + childrenProps: ReactNode, +): SubRoute[] { + // Directly comparing child.type with Route will not work with in + // combination with react-hot-loader in storybook + // https://github.com/gaearon/react-hot-loader/issues/304 + const routeType = ( + +
+ + ).type; + + return Children.toArray(childrenProps).flatMap(child => { + if (!isValidElement(child)) { + return []; + } + + if (child.type === Fragment) { + return createSubRoutesFromChildren(child.props.children); + } + + if (child.type !== routeType) { + throw new Error('Child of TabbedLayout must be an TabbedLayout.Route'); + } + + const { path, title, children } = child.props; + return [{ path, title, children }]; + }); +} + +/** + * TabbedLayout is a compound component, which allows you to define a layout for + * pages using a sub-navigation mechanism. + * + * Consists of two parts: TabbedLayout and TabbedLayout.Route + * + * @example + * ```jsx + * + * + *
This is rendered under /example/anything-here route
+ *
+ *
+ * ``` + */ +export const TabbedLayout = ({ children }: PropsWithChildren<{}>) => { + const routes = createSubRoutesFromChildren(children); + + return ; +}; + +TabbedLayout.Route = Route; diff --git a/packages/core/src/components/TabbedLayout/index.ts b/packages/core/src/components/TabbedLayout/index.ts new file mode 100644 index 0000000000..744b56959e --- /dev/null +++ b/packages/core/src/components/TabbedLayout/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { TabbedLayout } from './TabbedLayout'; diff --git a/plugins/catalog/src/components/EntityLayout/types.ts b/packages/core/src/components/TabbedLayout/types.ts similarity index 100% rename from plugins/catalog/src/components/EntityLayout/types.ts rename to packages/core/src/components/TabbedLayout/types.ts diff --git a/packages/core/src/components/index.ts b/packages/core/src/components/index.ts index 85ebba7a58..77eba9759a 100644 --- a/packages/core/src/components/index.ts +++ b/packages/core/src/components/index.ts @@ -21,10 +21,13 @@ export * from './CodeSnippet'; export * from './CopyTextButton'; export * from './DependencyGraph'; export * from './DismissableBanner'; +export * from './EmptyState'; export * from './FeatureDiscovery'; +export * from './HeaderIconLinkRow'; export * from './HorizontalScrollGrid'; export * from './Lifecycle'; export * from './Link'; +export * from './MarkdownContent'; export * from './OAuthRequestDialog'; export * from './Progress'; export * from './ProgressBars'; @@ -32,10 +35,8 @@ export * from './SimpleStepper'; export * from './Status'; export * from './StructuredMetadataTable'; export * from './SupportButton'; +export * from './TabbedLayout'; export * from './Table'; export * from './Tabs'; export * from './TrendLine'; export * from './WarningPanel'; -export * from './EmptyState'; -export * from './MarkdownContent'; -export * from './HeaderIconLinkRow'; diff --git a/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx b/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx index a1b4e1a97f..718a103f00 100644 --- a/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx +++ b/packages/core/src/layout/HeaderTabs/HeaderTabs.tsx @@ -18,7 +18,7 @@ // This is just a temporary solution to implementing tabs for now import React, { useState, useEffect } from 'react'; -import { makeStyles, Tabs, Tab } from '@material-ui/core'; +import { makeStyles, Tabs, Tab as TabUI } from '@material-ui/core'; const useStyles = makeStyles(theme => ({ tabsWrapper: { @@ -81,7 +81,7 @@ export const HeaderTabs = ({ value={selectedTab} > {tabs.map((tab, index) => ( - ( label="Button" /> + + + ); + +export const Link = () => ( + + + + + + + + + + +); diff --git a/packages/core/src/layout/ItemCard/ItemCard.tsx b/packages/core/src/layout/ItemCard/ItemCard.tsx index 991a05708f..4242cd6381 100644 --- a/packages/core/src/layout/ItemCard/ItemCard.tsx +++ b/packages/core/src/layout/ItemCard/ItemCard.tsx @@ -13,8 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Button, Card, Chip, makeStyles, Typography } from '@material-ui/core'; +import clsx from 'clsx'; import React from 'react'; -import { Button, Card, Chip, Typography, makeStyles } from '@material-ui/core'; +import { Link } from '../../components'; const useStyles = makeStyles(theme => ({ header: { @@ -30,6 +32,9 @@ const useStyles = makeStyles(theme => ({ overflow: 'hidden', textOverflow: 'ellipsis', }, + withTags: { + height: 'calc(175px - 32px - 8px)', + }, footer: { display: 'flex', flexDirection: 'row-reverse', @@ -43,7 +48,9 @@ type ItemCardProps = { type?: string; label: string; onClick?: () => void; + href?: string; }; + export const ItemCard = ({ description, tags, @@ -51,6 +58,7 @@ export const ItemCard = ({ type, label, onClick, + href, }: ItemCardProps) => { const classes = useStyles(); @@ -64,13 +72,27 @@ export const ItemCard = ({ {tags?.map((tag, i) => ( ))} - + 0 && classes.withTags, + )} + > {description}
- + {!href && ( + + )} + {href && ( + + )}
diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md index 6c449b33b5..13b854972a 100644 --- a/packages/create-app/CHANGELOG.md +++ b/packages/create-app/CHANGELOG.md @@ -1,5 +1,78 @@ # @backstage/create-app +## 0.3.8 + +### Patch Changes + +- 019fe39a0: **BREAKING CHANGE**: The `useEntity` hook has been moved from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. + To apply this change to an existing app, add `@backstage/plugin-catalog-react` to your dependencies in `packages/app/package.json`, and update + the import inside `packages/app/src/components/catalog/EntityPage.tsx` as well as any other places you were using `useEntity` or any other functions that were moved to `@backstage/plugin-catalog-react`. +- 436ca3f62: Remove techdocs.requestUrl and techdocs.storageUrl from app-config.yaml +- Updated dependencies [ceef4dd89] +- Updated dependencies [720149854] +- Updated dependencies [c777df180] +- Updated dependencies [398e1f83e] +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [b712841d6] +- Updated dependencies [a5628df40] +- Updated dependencies [2430ee7c2] +- Updated dependencies [3149bfe63] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [bc5082a00] +- Updated dependencies [6e612ce25] +- Updated dependencies [e44925723] +- Updated dependencies [b37501a3d] +- Updated dependencies [a26668913] +- Updated dependencies [025e122c3] +- Updated dependencies [e9aab60c7] +- Updated dependencies [21e624ba9] +- Updated dependencies [19fe61c27] +- Updated dependencies [e9aab60c7] +- Updated dependencies [da9f53c60] +- Updated dependencies [a08c4b0b0] +- Updated dependencies [24e47ef1e] +- Updated dependencies [bc5082a00] +- Updated dependencies [b37501a3d] +- Updated dependencies [90c8f20b9] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [529d16d27] +- Updated dependencies [54c7d02f7] +- Updated dependencies [de98c32ed] +- Updated dependencies [806929fe2] +- Updated dependencies [019fe39a0] +- Updated dependencies [cdea0baf1] +- Updated dependencies [019fe39a0] +- Updated dependencies [11cb5ef94] + - @backstage/plugin-catalog-import@0.3.7 + - @backstage/plugin-scaffolder@0.4.2 + - @backstage/plugin-techdocs-backend@0.5.5 + - @backstage/cli@0.6.0 + - @backstage/core@0.6.0 + - @backstage/plugin-api-docs@0.4.4 + - @backstage/plugin-catalog@0.3.0 + - @backstage/theme@0.2.3 + - @backstage/plugin-lighthouse@0.2.9 + - @backstage/backend-common@0.5.2 + - @backstage/plugin-catalog-backend@0.6.0 + - @backstage/plugin-techdocs@0.5.5 + - @backstage/plugin-user-settings@0.2.5 + - @backstage/catalog-model@0.7.1 + - @backstage/plugin-scaffolder-backend@0.6.0 + - @backstage/plugin-app-backend@0.3.6 + - @backstage/plugin-tech-radar@0.3.4 + - @backstage/plugin-explore@0.2.4 + - @backstage/plugin-circleci@0.2.7 + - @backstage/plugin-github-actions@0.3.1 + - @backstage/plugin-search@0.2.7 + - @backstage/test-utils@0.1.6 + - @backstage/plugin-auth-backend@0.2.12 + - @backstage/plugin-proxy-backend@0.2.4 + - @backstage/plugin-rollbar-backend@0.1.7 + ## 0.3.7 ### Patch Changes @@ -52,7 +125,7 @@ - @backstage/plugin-catalog@0.2.13 - @backstage/plugin-scaffolder-backend@0.5.1 -## 1.0.0 +## 0.3.6 ### Minor Changes diff --git a/packages/create-app/package.json b/packages/create-app/package.json index 475b61dde8..5f32a5d13d 100644 --- a/packages/create-app/package.json +++ b/packages/create-app/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/create-app", "description": "Create app package for Backstage", - "version": "0.3.7", + "version": "0.3.8", "private": false, "publishConfig": { "access": "public" @@ -44,32 +44,32 @@ "ts-node": "^8.6.2" }, "peerDependencies": { - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", - "@backstage/cli": "^0.5.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/cli": "^0.6.0", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.5.0", - "@backstage/plugin-api-docs": "^0.4.3", - "@backstage/plugin-app-backend": "^0.3.5", + "@backstage/core": "^0.6.0", + "@backstage/plugin-api-docs": "^0.4.4", + "@backstage/plugin-app-backend": "^0.3.6", "@backstage/plugin-auth-backend": "^0.2.12", - "@backstage/plugin-catalog": "^0.2.14", - "@backstage/plugin-catalog-backend": "^0.5.5", - "@backstage/plugin-catalog-import": "^0.3.6", - "@backstage/plugin-circleci": "^0.2.6", - "@backstage/plugin-explore": "^0.2.3", - "@backstage/plugin-github-actions": "^0.3.0", - "@backstage/plugin-lighthouse": "^0.2.8", + "@backstage/plugin-catalog": "^0.3.0", + "@backstage/plugin-catalog-backend": "^0.6.0", + "@backstage/plugin-catalog-import": "^0.3.7", + "@backstage/plugin-circleci": "^0.2.7", + "@backstage/plugin-explore": "^0.2.4", + "@backstage/plugin-github-actions": "^0.3.1", + "@backstage/plugin-lighthouse": "^0.2.9", "@backstage/plugin-proxy-backend": "^0.2.4", "@backstage/plugin-rollbar-backend": "^0.1.7", - "@backstage/plugin-scaffolder": "^0.4.1", - "@backstage/plugin-search": "^0.2.6", - "@backstage/plugin-scaffolder-backend": "^0.5.2", - "@backstage/plugin-tech-radar": "^0.3.3", - "@backstage/plugin-techdocs": "^0.5.4", - "@backstage/plugin-techdocs-backend": "^0.5.4", - "@backstage/plugin-user-settings": "^0.2.4", + "@backstage/plugin-scaffolder": "^0.4.2", + "@backstage/plugin-search": "^0.2.7", + "@backstage/plugin-scaffolder-backend": "^0.6.0", + "@backstage/plugin-tech-radar": "^0.3.4", + "@backstage/plugin-techdocs": "^0.5.5", + "@backstage/plugin-techdocs-backend": "^0.5.5", + "@backstage/plugin-user-settings": "^0.2.5", "@backstage/test-utils": "^0.1.6", - "@backstage/theme": "^0.2.2" + "@backstage/theme": "^0.2.3" }, "nodemonConfig": { "watch": "./src", diff --git a/packages/create-app/templates/default-app/app-config.yaml.hbs b/packages/create-app/templates/default-app/app-config.yaml.hbs index d253102f71..495b30dd6f 100644 --- a/packages/create-app/templates/default-app/app-config.yaml.hbs +++ b/packages/create-app/templates/default-app/app-config.yaml.hbs @@ -56,14 +56,13 @@ proxy: target: 'https://example.com' changeOrigin: true +# Reference documentation http://backstage.io/docs/features/techdocs/configuration techdocs: - requestUrl: http://localhost:7000/api/techdocs - storageUrl: http://localhost:7000/api/techdocs/static/docs - builder: 'local' + builder: 'local' # Alternatives - 'external' generators: - techdocs: 'docker' + techdocs: 'docker' # Alternatives - 'local' publisher: - type: 'local' + type: 'local' # Alternatives - 'googleGcs' or 'awsS3'. Read documentation for using alternatives. auth: # see https://backstage.io/docs/tutorials/quickstart-app-auth to know more about enabling auth providers diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts index c8bd3e5012..d68f90ce08 100644 --- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts +++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts @@ -14,6 +14,7 @@ import Docker from 'dockerode'; export default async function createPlugin({ logger, config, + database, }: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); @@ -38,5 +39,6 @@ export default async function createPlugin({ config, dockerClient, entityClient, + database, }); } diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md index c6c740bf39..251a4db7ee 100644 --- a/packages/dev-utils/CHANGELOG.md +++ b/packages/dev-utils/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/dev-utils +## 0.1.9 + +### Patch Changes + +- 720149854: Added `path` option to `addPage` that can be used to set a specific path for the page rather than a generated one. Also omit sidebar item altogether if `title` option is not set. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.1.8 ### Patch Changes diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index eff2d6e218..3a01306b46 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": "0.1.8", + "version": "0.1.9", "private": false, "publishConfig": { "access": "public", @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/catalog-model": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.0.1", + "@backstage/core": "^0.6.0", + "@backstage/catalog-model": "^0.7.1", + "@backstage/plugin-catalog-react": "^0.0.2", "@backstage/test-utils": "^0.1.5", - "@backstage/theme": "^0.2.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@testing-library/jest-dom": "^5.10.1", @@ -47,7 +47,7 @@ "react-router-dom": "6.0.0-beta.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, diff --git a/packages/e2e-test/src/commands/run.ts b/packages/e2e-test/src/commands/run.ts index 2ddcc333d2..5f4894f9e9 100644 --- a/packages/e2e-test/src/commands/run.ts +++ b/packages/e2e-test/src/commands/run.ts @@ -104,11 +104,11 @@ async function buildDistWorkspace(workspaceName: string, rootDir: string) { { helpers: { version(name: string) { - const pkg = require(`${name}/package.json`); - if (!pkg) { + const pkge = require(`${name}/package.json`); + if (!pkge) { throw new Error(`No version available for package ${name}`); } - return pkg.version; + return pkge.version; }, }, }, diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md index 11eb164535..11839d733b 100644 --- a/packages/integration/CHANGELOG.md +++ b/packages/integration/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/integration +## 0.3.2 + +### Patch Changes + +- c4abcdb60: Fix GitLab handling of paths with spaces +- 064c513e1: Properly forward errors that occur when looking up GitLab project IDs. +- 3149bfe63: Add a `resolveUrl` method to integrations, that works like the two-argument URL + constructor. The reason for using this is that Azure have their paths in a + query parameter, rather than the pathname of the URL. + + The implementation is optional (when not present, the URL constructor is used), + so this does not imply a breaking change. + +- 2e62aea6f: #4322 Bitbucket own hosted v5.11.1 branchUrl fix and enabled error tracing… #4347 + ## 0.3.1 ### Patch Changes diff --git a/packages/integration/package.json b/packages/integration/package.json index 06a83ea883..f82472ca97 100644 --- a/packages/integration/package.json +++ b/packages/integration/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/integration", - "version": "0.3.1", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -37,7 +37,7 @@ "luxon": "^1.25.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@backstage/test-utils": "^0.1.5", "@types/jest": "^26.0.7", "@types/luxon": "^1.25.0", diff --git a/packages/integration/src/ScmIntegrations.test.ts b/packages/integration/src/ScmIntegrations.test.ts index b43e69eba4..44e427a5d0 100644 --- a/packages/integration/src/ScmIntegrations.test.ts +++ b/packages/integration/src/ScmIntegrations.test.ts @@ -43,10 +43,10 @@ describe('ScmIntegrations', () => { } as GitLabIntegrationConfig); const i = new ScmIntegrations({ - azure: basicIntegrations([azure], i => i.config.host), - bitbucket: basicIntegrations([bitbucket], i => i.config.host), - github: basicIntegrations([github], i => i.config.host), - gitlab: basicIntegrations([gitlab], i => i.config.host), + azure: basicIntegrations([azure], item => item.config.host), + bitbucket: basicIntegrations([bitbucket], item => item.config.host), + github: basicIntegrations([github], item => item.config.host), + gitlab: basicIntegrations([gitlab], item => item.config.host), }); it('can get the specifics', () => { @@ -73,4 +73,19 @@ describe('ScmIntegrations', () => { expect(i.byHost('github.local')).toBe(github); expect(i.byHost('gitlab.local')).toBe(gitlab); }); + + it('can resolveUrl using fallback', () => { + expect( + i.resolveUrl({ + url: '../b.yaml', + base: 'https://no-matching-integration.com/x/a.yaml', + }), + ).toBe('https://no-matching-integration.com/b.yaml'); + expect( + i.resolveUrl({ + url: 'https://absolute.com/path', + base: 'https://no-matching-integration.com/x/a.yaml', + }), + ).toBe('https://absolute.com/path'); + }); }); diff --git a/packages/integration/src/ScmIntegrations.ts b/packages/integration/src/ScmIntegrations.ts index 102273a03b..c20fdcef9c 100644 --- a/packages/integration/src/ScmIntegrations.ts +++ b/packages/integration/src/ScmIntegrations.ts @@ -81,4 +81,13 @@ export class ScmIntegrations implements ScmIntegrationRegistry { .map(i => i.byHost(host)) .find(Boolean); } + + resolveUrl(options: { url: string; base: string }): string { + const resolve = this.byUrl(options.base)?.resolveUrl; + if (!resolve) { + return new URL(options.url, options.base).toString(); + } + + return resolve(options); + } } diff --git a/packages/integration/src/azure/AzureIntegration.test.ts b/packages/integration/src/azure/AzureIntegration.test.ts index 90c098b637..8a10e40bcf 100644 --- a/packages/integration/src/azure/AzureIntegration.test.ts +++ b/packages/integration/src/azure/AzureIntegration.test.ts @@ -41,4 +41,52 @@ describe('AzureIntegration', () => { expect(integration.type).toBe('azure'); expect(integration.title).toBe('h.com'); }); + + describe('resolveUrl', () => { + it('works for valid urls', () => { + const integration = new AzureIntegration({ + host: 'dev.azure.com', + } as any); + + expect( + integration.resolveUrl({ + url: '../a.yaml', + base: + 'https://dev.azure.com/organization/project/_git/repository?path=%2Ffolder%2Fcatalog-info.yaml', + }), + ).toBe( + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml', + ); + + expect( + integration.resolveUrl({ + url: './a.yaml', + base: 'https://dev.azure.com/organization/project/_git/repository', + }), + ).toBe( + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml', + ); + + expect( + integration.resolveUrl({ + url: 'https://absolute.com/path', + base: + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml', + }), + ).toBe('https://absolute.com/path'); + }); + + it('falls back to regular URL resolution if not in a repo', () => { + const integration = new AzureIntegration({ + host: 'dev.azure.com', + } as any); + + expect( + integration.resolveUrl({ + url: './test', + base: 'https://dev.azure.com/organization/project/_git', + }), + ).toBe('https://dev.azure.com/organization/project/test'); + }); + }); }); diff --git a/packages/integration/src/azure/AzureIntegration.ts b/packages/integration/src/azure/AzureIntegration.ts index 446e6a9480..1413b7b343 100644 --- a/packages/integration/src/azure/AzureIntegration.ts +++ b/packages/integration/src/azure/AzureIntegration.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +import parseGitUrl from 'git-url-parse'; import { basicIntegrations } from '../helpers'; import { ScmIntegration, ScmIntegrationsFactory } from '../types'; import { AzureIntegrationConfig, readAzureIntegrationConfigs } from './config'; @@ -42,4 +43,39 @@ export class AzureIntegration implements ScmIntegration { get config(): AzureIntegrationConfig { return this.integrationConfig; } + + /* + * Azure repo URLs on the form with a `path` query param are treated specially. + * + * Example base URL: https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml + */ + resolveUrl(options: { url: string; base: string }): string { + const { url, base } = options; + + // If we can parse the url, it is absolute - then return it verbatim + try { + // eslint-disable-next-line no-new + new URL(url); + return url; + } catch { + // Ignore intentionally - looks like a relative path + } + + const parsed = parseGitUrl(base); + const { organization, owner, name, filepath } = parsed; + + // If not an actual file path within a repo, treat the URL as raw + if (!organization || !owner || !name) { + return new URL(url, base).toString(); + } + + const path = filepath?.replace(/^\//, '') || ''; + const mockBaseUrl = new URL(`https://a.com/${path}`); + const updatedPath = new URL(url, mockBaseUrl).pathname; + + const newUrl = new URL(base); + newUrl.searchParams.set('path', updatedPath); + + return newUrl.toString(); + } } diff --git a/packages/integration/src/bitbucket/core.test.ts b/packages/integration/src/bitbucket/core.test.ts index 1287ab4d66..22b17d51fa 100644 --- a/packages/integration/src/bitbucket/core.test.ts +++ b/packages/integration/src/bitbucket/core.test.ts @@ -125,6 +125,7 @@ describe('bitbucket core', () => { ), ), ); + const config: BitbucketIntegrationConfig = { host: 'bitbucket.mycompany.net', apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', @@ -250,5 +251,40 @@ describe('bitbucket core', () => { ); expect(defaultBranch).toEqual('main'); }); + + it('return default branch for Bitbucket Server for bitbucket version 5.11', async () => { + const defaultBranchResponse = { + displayId: 'main', + }; + worker.use( + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/default-branch', + (_, res, ctx) => + res( + ctx.status(404), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + rest.get( + 'https://api.bitbucket.mycompany.net/rest/api/1.0/projects/backstage/repos/mock/branches/default', + (_, res, ctx) => + res( + ctx.status(200), + ctx.set('Content-Type', 'application/json'), + ctx.json(defaultBranchResponse), + ), + ), + ); + const config: BitbucketIntegrationConfig = { + host: 'bitbucket.mycompany.net', + apiBaseUrl: 'https://api.bitbucket.mycompany.net/rest/api/1.0', + }; + const defaultBranch = await getBitbucketDefaultBranch( + 'https://bitbucket.mycompany.net/projects/backstage/repos/mock/browse/README.md', + config, + ); + expect(defaultBranch).toEqual('main'); + }); }); }); diff --git a/packages/integration/src/bitbucket/core.ts b/packages/integration/src/bitbucket/core.ts index f5235d8189..8b1d7d6b18 100644 --- a/packages/integration/src/bitbucket/core.ts +++ b/packages/integration/src/bitbucket/core.ts @@ -32,11 +32,19 @@ export async function getBitbucketDefaultBranch( const isHosted = resource === 'bitbucket.org'; // Bitbucket Server https://docs.atlassian.com/bitbucket-server/rest/7.9.0/bitbucket-rest.html#idp184 - const branchUrl = isHosted + let branchUrl = isHosted ? `${config.apiBaseUrl}/repositories/${project}/${repoName}` : `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/default-branch`; - const response = await fetch(branchUrl, getBitbucketRequestOptions(config)); + let response = await fetch(branchUrl, getBitbucketRequestOptions(config)); + + if (response.status === 404 && !isHosted) { + // First try the new format, and then if it gets specifically a 404 it should try the old format + // (to support old Atlassian Bitbucket v5.11.1 format ) + branchUrl = `${config.apiBaseUrl}/projects/${project}/repos/${repoName}/branches/default`; + response = await fetch(branchUrl, getBitbucketRequestOptions(config)); + } + if (!response.ok) { const message = `Failed to retrieve default branch from ${branchUrl}, ${response.status} ${response.statusText}`; throw new Error(message); diff --git a/packages/integration/src/github/GithubCredentialsProvider.test.ts b/packages/integration/src/github/GithubCredentialsProvider.test.ts index f708f75184..d295677c55 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.test.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.test.ts @@ -215,21 +215,21 @@ describe('GithubCredentialsProvider tests', () => { }); it('should return the default token if no app is configured', async () => { - const github = GithubCredentialsProvider.create({ + const githubProvider = GithubCredentialsProvider.create({ host: 'github.com', apps: [], token: 'fallback_token', }); await expect( - github.getCredentials({ + githubProvider.getCredentials({ url: 'https://github.com/404/foobar', }), ).resolves.toEqual(expect.objectContaining({ token: 'fallback_token' })); }); it('should return the configured token if listing installations throws', async () => { - const github = GithubCredentialsProvider.create({ + const githubProvider = GithubCredentialsProvider.create({ host: 'github.com', apps: [ { @@ -245,19 +245,19 @@ describe('GithubCredentialsProvider tests', () => { octokit.apps.listInstallations.mockRejectedValue({ status: 304 }); await expect( - github.getCredentials({ + githubProvider.getCredentials({ url: 'https://github.com/backstage', }), ).resolves.toEqual(expect.objectContaining({ token: 'hardcoded_token' })); }); it('should return undefined if no token or apps are configured', async () => { - const github = GithubCredentialsProvider.create({ + const githubProvider = GithubCredentialsProvider.create({ host: 'github.com', }); await expect( - github.getCredentials({ + githubProvider.getCredentials({ url: 'https://github.com/backstage', }), ).resolves.toEqual({ headers: undefined, token: undefined }); diff --git a/packages/integration/src/github/GithubCredentialsProvider.ts b/packages/integration/src/github/GithubCredentialsProvider.ts index dfb809ee5f..0bf217c824 100644 --- a/packages/integration/src/github/GithubCredentialsProvider.ts +++ b/packages/integration/src/github/GithubCredentialsProvider.ts @@ -53,7 +53,7 @@ class Cache { /** * This accept header is required when calling App APIs in GitHub Enterprise. - * It has no effect on calls to github.com and can probably be removed entierly + * It has no effect on calls to github.com and can probably be removed entirely * once GitHub Apps is out of preview. */ const HEADERS = { @@ -177,7 +177,7 @@ export class GithubAppCredentialsMux { ), ); - const result = results.find(result => result.credentials); + const result = results.find(resultItem => resultItem.credentials); if (result) { return result.credentials!.accessToken; } diff --git a/packages/integration/src/gitlab/core.test.ts b/packages/integration/src/gitlab/core.test.ts index 43fea72e0b..2b4713604c 100644 --- a/packages/integration/src/gitlab/core.test.ts +++ b/packages/integration/src/gitlab/core.test.ts @@ -49,23 +49,23 @@ describe('gitlab core', () => { { config: configWithNoToken, url: - 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + 'https://gitlab.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml', result: - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch', }, { config: configWithToken, url: - 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path/to/file.yaml', + 'https://gitlab.example.com/groupA/teams/teamA/subgroupA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml', result: - 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + 'https://gitlab.example.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch', }, { config: configWithNoToken, url: - 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path/to/file.yaml', // Repo not in subgroup + 'https://gitlab.com/groupA/teams/teamA/repoA/-/blob/branch/my/path%20with%20spaces/to/file.yaml', // Repo not in subgroup result: - 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%2Fto%2Ffile.yaml/raw?ref=branch', + 'https://gitlab.com/api/v4/projects/12345/repository/files/my%2Fpath%20with%20spaces%2Fto%2Ffile.yaml/raw?ref=branch', }, // Raw URLs { diff --git a/packages/integration/src/gitlab/core.ts b/packages/integration/src/gitlab/core.ts index c1d43c3a46..66b6733f56 100644 --- a/packages/integration/src/gitlab/core.ts +++ b/packages/integration/src/gitlab/core.ts @@ -109,7 +109,7 @@ export function buildProjectUrl(target: string, projectID: Number): URL { '/api/v4/projects', projectID, 'repository/files', - encodeURIComponent(filePath.join('/')), + encodeURIComponent(decodeURIComponent(filePath.join('/'))), 'raw', ].join('/'); url.search = `?ref=${branch}`; diff --git a/packages/integration/src/types.ts b/packages/integration/src/types.ts index d8fb7a14e2..7d5b0fbe74 100644 --- a/packages/integration/src/types.ts +++ b/packages/integration/src/types.ts @@ -34,6 +34,18 @@ export interface ScmIntegration { * differentiate between different integrations. */ title: string; + + /** + * Works like the two-argument form of the URL constructor, resolving an + * absolute or relative URL in relation to a base URL. + * + * If this method is not implemented, the URL constructor is used instead for + * URLs that match this integration. + * + * @param options.url The (absolute or relative) URL or path to resolve + * @param options.base The base URL onto which this resolution happens + */ + resolveUrl?(options: { url: string; base: string }): string; } /** @@ -69,6 +81,15 @@ export interface ScmIntegrationRegistry bitbucket: ScmIntegrationsGroup; github: ScmIntegrationsGroup; gitlab: ScmIntegrationsGroup; + + /** + * Works like the two-argument form of the URL constructor, resolving an + * absolute or relative URL in relation to a base URL. + * + * @param options.url The (absolute or relative) URL or path to resolve + * @param options.base The base URL onto which this resolution happens + */ + resolveUrl(options: { url: string; base: string }): string; } export type ScmIntegrationsFactory = (options: { diff --git a/packages/techdocs-common/CHANGELOG.md b/packages/techdocs-common/CHANGELOG.md index 568df3157c..baf7a380a2 100644 --- a/packages/techdocs-common/CHANGELOG.md +++ b/packages/techdocs-common/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/techdocs-common +## 0.3.7 + +### Patch Changes + +- c777df180: 1. Added option to use Azure Blob Storage as a choice to store the static generated files for TechDocs. +- e44925723: `techdocs.requestUrl` and `techdocs.storageUrl` are now optional configs and the discovery API will be used to get the URL where techdocs plugin is hosted. +- f0320190d: dir preparer will use URL Reader in its implementation. +- Updated dependencies [c4abcdb60] +- Updated dependencies [2430ee7c2] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [064c513e1] +- Updated dependencies [7881f2117] +- Updated dependencies [3149bfe63] +- Updated dependencies [2e62aea6f] +- Updated dependencies [11cb5ef94] + - @backstage/integration@0.3.2 + - @backstage/backend-common@0.5.2 + - @backstage/catalog-model@0.7.1 + ## 0.3.6 ### Patch Changes diff --git a/packages/techdocs-common/package.json b/packages/techdocs-common/package.json index 2a065600a1..9b0f7fbf9f 100644 --- a/packages/techdocs-common/package.json +++ b/packages/techdocs-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/techdocs-common", "description": "Common functionalities for TechDocs, to be shared between techdocs-backend plugin and techdocs-cli", - "version": "0.3.6", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -39,10 +39,10 @@ "@aws-sdk/client-s3": "^3.1.0", "@azure/identity": "^1.2.2", "@azure/storage-blob": "^12.4.0", - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.1", + "@backstage/integration": "^0.3.2", "@google-cloud/storage": "^5.6.0", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", @@ -61,7 +61,7 @@ }, "devDependencies": { "@aws-sdk/types": "3.1.0", - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/fs-extra": "^9.0.5", "@types/git-url-parse": "^9.0.0", "@types/js-yaml": "^3.12.5", diff --git a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts index 1b4e59a714..a02639e63f 100644 --- a/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts +++ b/packages/techdocs-common/src/stages/publish/azureBlobStorage.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import path from 'path'; +import platformPath from 'path'; import express from 'express'; import { BlobServiceClient, @@ -135,7 +135,7 @@ export class AzureBlobStoragePublish implements PublisherBase { // e.g. ['index.html', 'sub-page/index.html', 'assets/images/favicon.png'] const relativeFilePath = filePath.replace(`${directory}/`, ''); const entityRootDir = `${entity.metadata.namespace}/${entity.kind}/${entity.metadata.name}`; - const destination = path.normalize( + const destination = platformPath.normalize( `${entityRootDir}/${relativeFilePath}`, ); // Azure Blob Storage Container file relative path @@ -217,7 +217,7 @@ export class AzureBlobStoragePublish implements PublisherBase { // filePath example - /default/Component/documented-component/index.html const filePath = req.path.replace(/^\//, ''); // Files with different extensions (CSS, HTML) need to be served with different headers - const fileExtension = path.extname(filePath); + const fileExtension = platformPath.extname(filePath); const responseHeaders = getHeadersForFileExtension(fileExtension); try { diff --git a/packages/techdocs-common/src/stages/publish/local.test.ts b/packages/techdocs-common/src/stages/publish/local.test.ts index efd56285ef..cd1a17ab5f 100644 --- a/packages/techdocs-common/src/stages/publish/local.test.ts +++ b/packages/techdocs-common/src/stages/publish/local.test.ts @@ -13,34 +13,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - -/* eslint-disable no-restricted-syntax */ -import fs from 'fs-extra'; -import path from 'path'; import { getVoidLogger, PluginEndpointDiscovery, } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; +import mockFs from 'mock-fs'; +import * as os from 'os'; import { LocalPublish } from './local'; -jest.mock('fs-extra', () => { - const fsOriginal = jest.requireActual('fs-extra'); - return { - ...fsOriginal, - access: jest.fn().mockImplementation((path, checkType, callback) => { - if ( - path.includes('http://localhost:7000/static') && - checkType === fs.constants.F_OK - ) { - callback(); - } else { - callback(new Error()); - } - }), - }; -}); - const createMockEntity = (annotations = {}) => { return { apiVersion: 'version', @@ -56,43 +37,33 @@ const createMockEntity = (annotations = {}) => { const logger = getVoidLogger(); +const tmpDir = + os.platform() === 'win32' ? 'C:\\tmp\\generatedDir' : '/tmp/generatedDir'; + describe('local publisher', () => { it('should publish generated documentation dir', async () => { - const testDiscovery: jest.Mocked = { - getBaseUrl: jest.fn().mockResolvedValue('http://localhost:7000'), - getExternalBaseUrl: jest.fn(), - }; - - const mockConfig = new ConfigReader({ - techdocs: { - requestUrl: 'http://localhost:7000', - storageUrl: 'http://localhost:7000/static/docs', + mockFs({ + [tmpDir]: { + 'index.html': '', }, }); + const testDiscovery: jest.Mocked = { + getBaseUrl: jest + .fn() + .mockResolvedValue('http://localhost:7000/api/techdocs'), + getExternalBaseUrl: jest.fn(), + }; + + const mockConfig = new ConfigReader({}); + const publisher = new LocalPublish(mockConfig, logger, testDiscovery); const mockEntity = createMockEntity(); - const tempDir = fs.mkdtempSync(`${__dirname}/test-component-folder-`); - expect(tempDir).toBeTruthy(); - fs.closeSync(fs.openSync(path.join(tempDir, '/mock-file'), 'w')); - await publisher.publish({ entity: mockEntity, directory: tempDir }); - - const publishDir = path.resolve( - __dirname, - `../../../../../plugins/techdocs-backend/static/docs/${mockEntity.metadata.name}`, - ); - const resultDir = path.resolve( - __dirname, - `../../../../../plugins/techdocs-backend/static/docs/default/${mockEntity.kind}/${mockEntity.metadata.name}`, - ); - - expect(fs.existsSync(resultDir)).toBeTruthy(); - expect(fs.existsSync(path.join(resultDir, '/mock-file'))).toBeTruthy(); + await publisher.publish({ entity: mockEntity, directory: tmpDir }); expect(await publisher.hasDocsBeenGenerated(mockEntity)).toBe(true); - fs.removeSync(publishDir); - fs.removeSync(tempDir); + mockFs.restore(); }); }); diff --git a/packages/techdocs-common/src/stages/publish/local.ts b/packages/techdocs-common/src/stages/publish/local.ts index d07472e17d..e349a2119a 100644 --- a/packages/techdocs-common/src/stages/publish/local.ts +++ b/packages/techdocs-common/src/stages/publish/local.ts @@ -13,18 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import fetch from 'cross-fetch'; +import { + PluginEndpointDiscovery, + resolvePackagePath, +} from '@backstage/backend-common'; +import { Entity, EntityName } from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; import express from 'express'; import fs from 'fs-extra'; -import path from 'path'; import os from 'os'; +import path from 'path'; import { Logger } from 'winston'; -import { Entity, EntityName } from '@backstage/catalog-model'; -import { - resolvePackagePath, - PluginEndpointDiscovery, -} from '@backstage/backend-common'; -import { Config } from '@backstage/config'; import { PublisherBase, PublishRequest, @@ -52,17 +51,14 @@ try { * called "static" at the root of techdocs-backend plugin. */ export class LocalPublish implements PublisherBase { - private readonly config: Config; - private readonly logger: Logger; - private readonly discovery: PluginEndpointDiscovery; - // TODO: Use a static fromConfig method to create a LocalPublish instance, similar to aws/gcs publishers. // Move the logic of setting staticDocsDir based on config over to fromConfig, // and set the value as a class parameter. constructor( - config: Config, - logger: Logger, - discovery: PluginEndpointDiscovery, + // @ts-ignore + private readonly config: Config, + private readonly logger: Logger, + private readonly discovery: PluginEndpointDiscovery, ) { this.config = config; this.logger = logger; @@ -107,34 +103,25 @@ export class LocalPublish implements PublisherBase { }); } - fetchTechDocsMetadata(entityName: EntityName): Promise { - return new Promise((resolve, reject) => { - this.discovery.getBaseUrl('techdocs').then(techdocsApiUrl => { - const storageUrl = new URL( - new URL(this.config.getString('techdocs.storageUrl')).pathname, - techdocsApiUrl, - ).toString(); + async fetchTechDocsMetadata( + entityName: EntityName, + ): Promise { + const metadataPath = path.join( + staticDocsDir, + entityName.namespace, + entityName.kind, + entityName.name, + 'techdocs_metadata.json', + ); - const entityRootDir = `${entityName.namespace}/${entityName.kind}/${entityName.name}`; - const metadataURL = `${storageUrl}/${entityRootDir}/techdocs_metadata.json`; - fetch(metadataURL) - .then(response => - response - .json() - .then(techdocsMetadata => resolve(techdocsMetadata)) - .catch(err => { - reject( - `Unable to parse metadata JSON for ${entityRootDir}. Error: ${err}`, - ); - }), - ) - .catch(err => { - reject( - `Unable to fetch metadata for ${entityRootDir}. Error ${err}`, - ); - }); - }); - }); + try { + return await fs.readJson(metadataPath); + } catch (err) { + this.logger.error( + `Unable to read techdocs_metadata.json at ${metadataPath}. Error: ${err}`, + ); + throw new Error(err.message); + } } docsRouter(): express.Handler { @@ -143,24 +130,21 @@ export class LocalPublish implements PublisherBase { async hasDocsBeenGenerated(entity: Entity): Promise { const namespace = entity.metadata.namespace ?? 'default'; - return new Promise(resolve => { - this.discovery.getBaseUrl('techdocs').then(techdocsApiUrl => { - const storageUrl = new URL( - new URL(this.config.getString('techdocs.storageUrl')).pathname, - techdocsApiUrl, - ).toString(); - const entityRootDir = `${namespace}/${entity.kind}/${entity.metadata.name}`; - const indexHtmlUrl = `${storageUrl}/${entityRootDir}/index.html`; - // Check if the file exists - fs.access(indexHtmlUrl, fs.constants.F_OK, err => { - if (err) { - resolve(false); - } else { - resolve(true); - } - }); - }); - }); + const indexHtmlPath = path.join( + staticDocsDir, + namespace, + entity.kind, + entity.metadata.name, + 'index.html', + ); + + // Check if the file exists + try { + fs.access(indexHtmlPath, fs.constants.F_OK); + return true; + } catch (err) { + return false; + } } } diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 10f0e89c14..5cea83fa03 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -31,7 +31,7 @@ "dependencies": { "@backstage/core-api": "^0.2.7", "@backstage/test-utils-core": "^0.1.1", - "@backstage/theme": "^0.2.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", @@ -45,7 +45,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/jest": "^26.0.7", "@types/node": "^12.0.0" }, diff --git a/packages/test-utils/src/testUtils/appWrappers.test.tsx b/packages/test-utils/src/testUtils/appWrappers.test.tsx index 29f8d97793..4899f07061 100644 --- a/packages/test-utils/src/testUtils/appWrappers.test.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.test.tsx @@ -21,9 +21,11 @@ import { Route, Routes } from 'react-router'; import { withLogCollector } from '@backstage/test-utils-core'; import { useApi, + useRouteRef, errorApiRef, ApiProvider, ApiRegistry, + createRouteRef, } from '@backstage/core-api'; import { MockErrorApi } from './apis'; @@ -113,4 +115,29 @@ describe('wrapInTestApp', () => { expect(rendered.getByText('foo')).toBeInTheDocument(); expect(mockErrorApi.getErrors()).toEqual([{ error: new Error('NOPE') }]); }); + + it('should allow route refs to be mounted on specific paths', async () => { + const aRouteRef = createRouteRef({ title: 'A' }); + const bRouteRef = createRouteRef({ title: 'B', params: ['name'] }); + + const MyComponent = () => { + const a = useRouteRef(aRouteRef); + const b = useRouteRef(bRouteRef); + return ( +
+
Link A: {a()}
+
Link B: {b({ name: 'x' })}
+
+ ); + }; + + const rendered = await renderInTestApp(, { + mountedRoutes: { + '/my-a-path': aRouteRef, + '/my-b-path/:name': bRouteRef, + }, + }); + expect(rendered.getByText('Link A: /my-a-path')).toBeInTheDocument(); + expect(rendered.getByText('Link B: /my-b-path/x')).toBeInTheDocument(); + }); }); diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx index 1f6c7622b8..cc67a5b707 100644 --- a/packages/test-utils/src/testUtils/appWrappers.tsx +++ b/packages/test-utils/src/testUtils/appWrappers.tsx @@ -21,6 +21,9 @@ import { lightTheme } from '@backstage/theme'; import privateExports, { defaultSystemIcons, BootErrorPageProps, + RouteRef, + createPlugin, + createRoutableExtension, } from '@backstage/core-api'; import { RenderResult } from '@testing-library/react'; import { renderWithEffects } from '@backstage/test-utils-core'; @@ -44,6 +47,22 @@ type TestAppOptions = { * Initial route entries to pass along as `initialEntries` to the router. */ routeEntries?: string[]; + + /** + * An object of paths to mount route ref on, with the key being the path and the value + * being the RouteRef that the path will be bound to. This allows the route refs to be + * used by `useRouteRef` in the rendered elements. + * + * @example + * wrapInTestApp(, { + * mountedRoutes: { + * '/my-path': myRouteRef, + * } + * }) + * // ... + * const link = useRouteRef(myRouteRef) + */ + mountedRoutes?: { [path: string]: RouteRef }; }; /** @@ -90,12 +109,27 @@ export function wrapInTestApp( Wrapper = () => Component as React.ReactElement; } + const routePlugin = createPlugin({ id: 'mock-route-plugin' }); + const routeElements = Object.entries(options.mountedRoutes ?? {}).map( + ([path, routeRef]) => { + const PageComponent = () =>
Mounted at {path}
; + const Page = routePlugin.provide( + createRoutableExtension({ + component: async () => PageComponent, + mountPoint: routeRef, + }), + ); + return } />; + }, + ); + const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); return ( + {routeElements} {/* The path of * here is needed to be set as a catch all, so it will render the wrapper element * and work with nested routes if they exist too */} } /> diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md index 13d95ab856..4ee58b2201 100644 --- a/packages/theme/CHANGELOG.md +++ b/packages/theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @backstage/theme +## 0.2.3 + +### Patch Changes + +- c810082ae: Updates warning text color to align to updated `WarningPanel` styling + ## 0.2.2 ### Patch Changes diff --git a/packages/theme/package.json b/packages/theme/package.json index 24edf81bb6..31abd9acff 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/theme", "description": "material-ui theme for use with Backstage.", - "version": "0.2.2", + "version": "0.2.3", "private": false, "publishConfig": { "access": "public", @@ -31,7 +31,7 @@ "@material-ui/core": "^4.11.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0" + "@backstage/cli": "^0.6.0" }, "files": [ "dist" diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md index 01e34d8e1d..4735beb150 100644 --- a/plugins/api-docs/CHANGELOG.md +++ b/plugins/api-docs/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-api-docs +## 0.4.4 + +### Patch Changes + +- 7fc89bae2: Display owner and system as entity page links in the tables of the `api-docs` + plugin. + + Move `isOwnerOf` and `getEntityRelations` from `@backstage/plugin-catalog` to + `@backstage/plugin-catalog-react` and export it from there to use it by other + plugins. + +- bc5082a00: Migrate to new composability API, exporting the plugin as `apiDocsPlugin`, index page as `ApiExplorerPage`, and entity page cards as `EntityApiDefinitionCard`, `EntityConsumedApisCard`, `EntityConsumingComponentsCard`, `EntityProvidedApisCard`, and `EntityProvidingComponentsCard`. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.4.3 ### Patch Changes diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index 4bd7bd1727..175b831ada 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-api-docs", - "version": "0.4.3", + "version": "0.4.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ }, "dependencies": { "@asyncapi/react-component": "^0.18.2", - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-icons/font": "^1.0.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -49,8 +49,8 @@ "swagger-ui-react": "^3.37.2" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx index 0d2648ab5f..0107472acf 100644 --- a/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx +++ b/plugins/api-docs/src/components/ApiDefinitionCard/ApiDefinitionCard.test.tsx @@ -75,8 +75,8 @@ paths: type: 'openapi', title: 'OpenAPI', rawLanguage: 'yaml', - component: definition => ( - + component: definitionString => ( + ), }); diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md index 514e32633a..b57e0d25fb 100644 --- a/plugins/app-backend/CHANGELOG.md +++ b/plugins/app-backend/CHANGELOG.md @@ -1,5 +1,16 @@ # @backstage/plugin-app-backend +## 0.3.6 + +### Patch Changes + +- e9aab60c7: Failures to load the frontend configuration schema now throws an error that includes more context and instructions for how to fix the issue. +- Updated dependencies [2430ee7c2] +- Updated dependencies [062df71db] +- Updated dependencies [e9aab60c7] + - @backstage/backend-common@0.5.2 + - @backstage/config-loader@0.5.1 + ## 0.3.5 ### Patch Changes diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json index f397ea1475..cca36a51f8 100644 --- a/plugins/app-backend/package.json +++ b/plugins/app-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-app-backend", - "version": "0.3.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", - "@backstage/config-loader": "^0.5.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/config-loader": "^0.5.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -40,7 +40,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/supertest": "^2.0.8", "msw": "^0.20.5", "supertest": "^4.0.2" diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index b2198165a5..e87c2b4a90 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", + "@backstage/backend-common": "^0.5.2", "@backstage/catalog-client": "^0.3.5", - "@backstage/catalog-model": "^0.7.0", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "compression": "^1.7.4", @@ -65,7 +65,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/body-parser": "^1.19.0", "@types/cookie-parser": "^1.4.2", "@types/express-session": "^1.17.2", diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md index 0e446bf0f7..eb256d2c08 100644 --- a/plugins/catalog-backend/CHANGELOG.md +++ b/plugins/catalog-backend/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-catalog-backend +## 0.6.0 + +### Minor Changes + +- 3149bfe63: Make use of the `resolveUrl` facility of the `integration` package. + + Also rename the `LocationRefProcessor` to `LocationEntityProcessor`, to match the file name. This constitutes an interface change since the class is exported, but it is unlikely to be consumed outside of the package since it sits comfortably with the other default processors inside the catalog builder. + +### Patch Changes + +- 24e47ef1e: Throw `NotAllowedError` when registering locations with entities of disallowed kinds +- Updated dependencies [c4abcdb60] +- Updated dependencies [2430ee7c2] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [064c513e1] +- Updated dependencies [7881f2117] +- Updated dependencies [3149bfe63] +- Updated dependencies [2e62aea6f] +- Updated dependencies [11cb5ef94] + - @backstage/integration@0.3.2 + - @backstage/backend-common@0.5.2 + - @backstage/catalog-model@0.7.1 + ## 0.5.5 ### Patch Changes diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 264bda0556..bb76441f3f 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-backend", - "version": "0.5.5", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,10 @@ "dependencies": { "@aws-sdk/client-organizations": "^3.2.0", "@azure/msal-node": "^1.0.0-beta.3", - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", + "@backstage/integration": "^0.3.2", "@octokit/graphql": "^4.5.8", "@types/express": "^4.17.6", "@types/ldapjs": "^1.0.9", @@ -57,7 +58,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@backstage/test-utils": "^0.1.6", "@types/core-js": "^2.5.4", "@types/git-url-parse": "^9.0.0", diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 76fc63c9b3..b03183cf34 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -124,7 +124,7 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { // produce tens of thousands of entities, and those are too large batch // sizes to reasonably send to the database. const batches = Object.values(requestsByKindAndNamespace) - .map(requests => chunk(requests, BATCH_SIZE)) + .map(request => chunk(request, BATCH_SIZE)) .flat(); // Bound the number of concurrent batches. We want a bit of concurrency for diff --git a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts index 6eb7a45c63..28200021cc 100644 --- a/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/AwsOrganizationCloudAccountProcessor.test.ts @@ -65,7 +65,10 @@ describe('AwsOrganizationCloudAccountProcessor', () => { }); it('filters out accounts not in specified location target', async () => { - const location = { type: 'aws-cloud-accounts', target: 'o-1vl18kc5a3' }; + const locationTest = { + type: 'aws-cloud-accounts', + target: 'o-1vl18kc5a3', + }; listAccounts.mockImplementation(() => Promise.resolve({ Accounts: [ @@ -83,11 +86,11 @@ describe('AwsOrganizationCloudAccountProcessor', () => { NextToken: undefined, }), ); - await processor.readLocation(location, false, emit); + await processor.readLocation(locationTest, false, emit); expect(emit).toBeCalledTimes(1); expect(emit).toBeCalledWith({ type: 'entity', - location, + location: locationTest, entity: { apiVersion: 'backstage.io/v1alpha1', kind: 'Resource', diff --git a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts index c75a46874d..ef94c54010 100644 --- a/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/BuiltinKindsEntityProcessor.ts @@ -66,8 +66,8 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor { async validateEntityKind(entity: Entity): Promise { for (const validator of this.validators) { - const result = await validator.check(entity); - if (result) { + const results = await validator.check(entity); + if (results) { return true; } } diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts b/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts index 998dfb62a2..476510d757 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts +++ b/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.test.ts @@ -15,30 +15,73 @@ */ import { LocationSpec } from '@backstage/catalog-model'; -import { toAbsoluteUrl } from './LocationEntityProcessor'; +import { ConfigReader } from '@backstage/config'; +import { + ScmIntegrations, + ScmIntegrationRegistry, +} from '@backstage/integration'; import path from 'path'; +import { toAbsoluteUrl } from './LocationEntityProcessor'; describe('LocationEntityProcessor', () => { describe('toAbsoluteUrl', () => { it('handles files', () => { + const integrations = ({} as unknown) as ScmIntegrationRegistry; const base: LocationSpec = { type: 'file', target: `some${path.sep}path${path.sep}catalog-info.yaml`, }; - expect(toAbsoluteUrl(base, `.${path.sep}c`)).toBe( + expect(toAbsoluteUrl(integrations, base, `.${path.sep}c`)).toBe( `some${path.sep}path${path.sep}c`, ); - expect(toAbsoluteUrl(base, `${path.sep}c`)).toBe(`${path.sep}c`); + expect(toAbsoluteUrl(integrations, base, `${path.sep}c`)).toBe( + `${path.sep}c`, + ); }); it('handles urls', () => { + const integrations = ScmIntegrations.fromConfig(new ConfigReader({})); const base: LocationSpec = { type: 'url', target: 'http://a.com/b/catalog-info.yaml', }; - expect(toAbsoluteUrl(base, './c/d')).toBe('http://a.com/b/c/d'); - expect(toAbsoluteUrl(base, 'c/d')).toBe('http://a.com/b/c/d'); - expect(toAbsoluteUrl(base, 'http://b.com/z')).toBe('http://b.com/z'); + jest.spyOn(integrations, 'resolveUrl'); + + expect(toAbsoluteUrl(integrations, base, './c/d')).toBe( + 'http://a.com/b/c/d', + ); + expect(toAbsoluteUrl(integrations, base, 'c/d')).toBe( + 'http://a.com/b/c/d', + ); + expect(toAbsoluteUrl(integrations, base, 'http://b.com/z')).toBe( + 'http://b.com/z', + ); + + expect(integrations.resolveUrl).toBeCalledTimes(3); + }); + + it('handles azure urls specifically', () => { + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + azure: [{ host: 'dev.azure.com' }], + }, + }), + ); + + expect( + toAbsoluteUrl( + integrations, + { + type: 'url', + target: + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fcatalog-info.yaml', + }, + './a.yaml', + ), + ).toBe( + 'https://dev.azure.com/organization/project/_git/repository?path=%2Fa.yaml', + ); }); }); }); diff --git a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts b/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts index 1c7e13f1c4..f4c8c82957 100644 --- a/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts +++ b/plugins/catalog-backend/src/ingestion/processors/LocationEntityProcessor.ts @@ -15,11 +15,16 @@ */ import { Entity, LocationEntity, LocationSpec } from '@backstage/catalog-model'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import path from 'path'; import * as result from './results'; import { CatalogProcessor, CatalogProcessorEmit } from './types'; -import path from 'path'; -export function toAbsoluteUrl(base: LocationSpec, target: string): string { +export function toAbsoluteUrl( + integrations: ScmIntegrationRegistry, + base: LocationSpec, + target: string, +): string { try { if (base.type === 'file') { if (target.startsWith('.')) { @@ -27,13 +32,19 @@ export function toAbsoluteUrl(base: LocationSpec, target: string): string { } return target; } - return new URL(target, base.target).toString(); + return integrations.resolveUrl({ url: target, base: base.target }); } catch (e) { return target; } } -export class LocationRefProcessor implements CatalogProcessor { +type Options = { + integrations: ScmIntegrationRegistry; +}; + +export class LocationEntityProcessor implements CatalogProcessor { + constructor(private readonly options: Options) {} + async postProcessEntity( entity: Entity, location: LocationSpec, @@ -47,7 +58,7 @@ export class LocationRefProcessor implements CatalogProcessor { emit( result.inputError( location, - `LocationRefProcessor cannot handle ${type} type location with target ${location.target} that ends with a path separator`, + `LocationEntityProcessor cannot handle ${type} type location with target ${location.target} that ends with a path separator`, ), ); } @@ -61,7 +72,11 @@ export class LocationRefProcessor implements CatalogProcessor { } for (const maybeRelativeTarget of targets) { - const target = toAbsoluteUrl(location, maybeRelativeTarget); + const target = toAbsoluteUrl( + this.options.integrations, + location, + maybeRelativeTarget, + ); emit(result.location({ type, target }, false)); } } diff --git a/plugins/catalog-backend/src/ingestion/processors/index.ts b/plugins/catalog-backend/src/ingestion/processors/index.ts index a7b00d7065..05dd327f0e 100644 --- a/plugins/catalog-backend/src/ingestion/processors/index.ts +++ b/plugins/catalog-backend/src/ingestion/processors/index.ts @@ -23,7 +23,7 @@ export { CodeOwnersProcessor } from './CodeOwnersProcessor'; export { FileReaderProcessor } from './FileReaderProcessor'; export { GithubOrgReaderProcessor } from './GithubOrgReaderProcessor'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; -export { LocationRefProcessor } from './LocationEntityProcessor'; +export { LocationEntityProcessor } from './LocationEntityProcessor'; export { MicrosoftGraphOrgReaderProcessor } from './MicrosoftGraphOrgReaderProcessor'; export { PlaceholderProcessor } from './PlaceholderProcessor'; export type { PlaceholderResolver } from './PlaceholderProcessor'; diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts index 938a406e56..f7544ef42e 100644 --- a/plugins/catalog-backend/src/service/CatalogBuilder.ts +++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts @@ -26,6 +26,7 @@ import { Validators, } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; import lodash from 'lodash'; import { Logger } from 'winston'; import { @@ -46,8 +47,8 @@ import { HigherOrderOperation, HigherOrderOperations, LdapOrgReaderProcessor, + LocationEntityProcessor, LocationReaders, - LocationRefProcessor, MicrosoftGraphOrgReaderProcessor, PlaceholderProcessor, PlaceholderResolver, @@ -280,6 +281,7 @@ export class CatalogBuilder { private buildProcessors(): CatalogProcessor[] { const { config, logger, reader } = this.env; + const integrations = ScmIntegrations.fromConfig(config); this.checkDeprecatedReaderProcessors(); @@ -306,7 +308,7 @@ export class CatalogBuilder { MicrosoftGraphOrgReaderProcessor.fromConfig(config, { logger }), new UrlReaderProcessor({ reader, logger }), new CodeOwnersProcessor({ reader, logger }), - new LocationRefProcessor(), + new LocationEntityProcessor({ integrations }), new AnnotateLocationEntityProcessor(), ); } diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 43c2967233..17cfc77d8b 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { errorHandler, NotFoundError } from '@backstage/backend-common'; import { locationSpecSchema, analyzeLocationSchema, @@ -57,7 +57,7 @@ export async function createRouter( const filter = EntityFilters.ofQuery(req.query); const fieldMapper = translateQueryToFieldMapper(req.query); const entities = await entitiesCatalog.entities(filter); - res.status(200).send(entities.map(fieldMapper)); + res.status(200).json(entities.map(fieldMapper)); }) .post('/entities', async (req, res) => { const body = await requireRequestBody(req); @@ -67,7 +67,7 @@ export async function createRouter( const [entity] = await entitiesCatalog.entities( EntityFilters.ofMatchers({ 'metadata.uid': result.entityId }), ); - res.status(200).send(entity); + res.status(200).json(entity); }) .get('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; @@ -75,14 +75,14 @@ export async function createRouter( EntityFilters.ofMatchers({ 'metadata.uid': uid }), ); if (!entities.length) { - res.status(404).send(`No entity with uid ${uid}`); + throw new NotFoundError(`No entity with uid ${uid}`); } - res.status(200).send(entities[0]); + res.status(200).json(entities[0]); }) .delete('/entities/by-uid/:uid', async (req, res) => { const { uid } = req.params; await entitiesCatalog.removeEntityByUid(uid); - res.status(204).send(); + res.status(204).end(); }) .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { const { kind, namespace, name } = req.params; @@ -94,13 +94,11 @@ export async function createRouter( }), ); if (!entities.length) { - res - .status(404) - .send( - `No entity with kind ${kind} namespace ${namespace} name ${name}`, - ); + throw new NotFoundError( + `No entity with kind ${kind} namespace ${namespace} name ${name}`, + ); } - res.status(200).send(entities[0]); + res.status(200).json(entities[0]); }); } @@ -109,7 +107,7 @@ export async function createRouter( const input = await validateRequestBody(req, locationSpecSchema); const dryRun = yn(req.query.dryRun, { default: false }); const output = await higherOrderOperation.addLocation(input, { dryRun }); - res.status(201).send(output); + res.status(201).json(output); }); } @@ -117,22 +115,22 @@ export async function createRouter( router .get('/locations', async (_req, res) => { const output = await locationsCatalog.locations(); - res.status(200).send(output); + res.status(200).json(output); }) .get('/locations/:id/history', async (req, res) => { const { id } = req.params; const output = await locationsCatalog.locationHistory(id); - res.status(200).send(output); + res.status(200).json(output); }) .get('/locations/:id', async (req, res) => { const { id } = req.params; const output = await locationsCatalog.location(id); - res.status(200).send(output); + res.status(200).json(output); }) .delete('/locations/:id', async (req, res) => { const { id } = req.params; await locationsCatalog.removeLocation(id); - res.status(204).send(); + res.status(204).end(); }); } @@ -140,7 +138,7 @@ export async function createRouter( router.post('/analyze-location', async (req, res) => { const input = await validateRequestBody(req, analyzeLocationSchema); const output = await locationAnalyzer.analyzeLocation(input); - res.status(200).send(output); + res.status(200).json(output); }); } diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json index c81c29fc3e..9d76545909 100644 --- a/plugins/catalog-graphql/package.json +++ b/plugins/catalog-graphql/package.json @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@graphql-modules/core": "^0.7.17", "apollo-server": "^2.16.1", @@ -42,7 +42,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@backstage/test-utils": "^0.1.5", "@graphql-codegen/cli": "^1.17.7", "@graphql-codegen/typescript": "^1.17.7", diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md index c6e4995c70..0311d865a9 100644 --- a/plugins/catalog-import/CHANGELOG.md +++ b/plugins/catalog-import/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-catalog-import +## 0.3.7 + +### Patch Changes + +- ceef4dd89: Export _api_ (Client, API, ref) from the catalog import plugin. +- b712841d6: Migrated to new composability API, exporting the plugin instance as `catalogImportPlugin`, and the page as `CatalogImportPage`. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [c4abcdb60] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [064c513e1] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [3149bfe63] +- Updated dependencies [54c7d02f7] +- Updated dependencies [2e62aea6f] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/integration@0.3.2 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.3.6 ### Patch Changes diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json index 7f3cc3d1e3..2b2b016716 100644 --- a/plugins/catalog-import/package.json +++ b/plugins/catalog-import/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-import", - "version": "0.3.6", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,11 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/integration": "^0.3.1", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/integration": "^0.3.2", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -50,8 +50,8 @@ }, "devDependencies": { "@backstage/catalog-client": "^0.3.5", - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/catalog-import/src/components/ImportComponentForm.tsx b/plugins/catalog-import/src/components/ImportComponentForm.tsx index a3d355b33f..b3ec050ea3 100644 --- a/plugins/catalog-import/src/components/ImportComponentForm.tsx +++ b/plugins/catalog-import/src/components/ImportComponentForm.tsx @@ -71,8 +71,8 @@ export const RegisterComponentForm = ({ const onSubmit = async (formData: Record) => { const { componentLocation: target } = formData; - async function saveCatalogFileConfig(target: string) { - const data = await catalogApi.addLocation({ target }); + async function saveCatalogFileConfig(targetString: string) { + const data = await catalogApi.addLocation({ target: targetString }); saveConfig({ type: 'file', location: data.location.target, @@ -80,12 +80,12 @@ export const RegisterComponentForm = ({ }); } - async function trySaveRepositoryConfig(target: string) { - const existingCatalog = await checkForExistingCatalogInfo(target); + async function trySaveRepositoryConfig(targetString: string) { + const existingCatalog = await checkForExistingCatalogInfo(targetString); if (existingCatalog.exists) { - const targetUrl = target.endsWith('/') - ? `${target}${existingCatalog.url}` - : `${target}/${existingCatalog.url}`; + const targetUrl = targetString.endsWith('/') + ? `${targetString}${existingCatalog.url}` + : `${targetString}/${existingCatalog.url}`; await saveCatalogFileConfig(targetUrl); } else { saveConfig({ diff --git a/plugins/catalog-import/src/components/ImportComponentPage.tsx b/plugins/catalog-import/src/components/ImportComponentPage.tsx index e284e95456..adae137a50 100644 --- a/plugins/catalog-import/src/components/ImportComponentPage.tsx +++ b/plugins/catalog-import/src/components/ImportComponentPage.tsx @@ -46,20 +46,20 @@ function manifestGenerationAvailable(configApi: ConfigApi): boolean { function repositories(configApi: ConfigApi): string[] { const integrations = configApi.getConfig('integrations'); - const repositories = []; + const repos = []; if (integrations.has('github')) { - repositories.push('GitHub'); + repos.push('GitHub'); } if (integrations.has('bitbucket')) { - repositories.push('Bitbucket'); + repos.push('Bitbucket'); } if (integrations.has('gitlab')) { - repositories.push('GitLab'); + repos.push('GitLab'); } if (integrations.has('azure')) { - repositories.push('Azure'); + repos.push('Azure'); } - return repositories; + return repos; } export const ImportComponentPage = ({ diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md new file mode 100644 index 0000000000..90b7c80caf --- /dev/null +++ b/plugins/catalog-react/CHANGELOG.md @@ -0,0 +1,26 @@ +# @backstage/plugin-catalog-react + +## 0.0.2 + +### Patch Changes + +- 7fc89bae2: Display owner and system as entity page links in the tables of the `api-docs` + plugin. + + Move `isOwnerOf` and `getEntityRelations` from `@backstage/plugin-catalog` to + `@backstage/plugin-catalog-react` and export it from there to use it by other + plugins. + +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/catalog-model@0.7.1 diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index 94dc835d14..64307f6846 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog-react", - "version": "0.0.1", + "version": "0.0.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,8 +29,8 @@ }, "dependencies": { "@backstage/catalog-client": "^0.3.5", - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", "@material-ui/core": "^4.11.0", "@types/react": "^16.9", "react": "^16.13.1", @@ -39,8 +39,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/catalog-react/src/utils/isOwnerOf.ts b/plugins/catalog-react/src/utils/isOwnerOf.ts index 455e06dc13..d135e4030a 100644 --- a/plugins/catalog-react/src/utils/isOwnerOf.ts +++ b/plugins/catalog-react/src/utils/isOwnerOf.ts @@ -34,13 +34,13 @@ export function isOwnerOf(owner: Entity, owned: Entity) { const owners = getEntityRelations(owned, RELATION_OWNED_BY); - for (const owner of owners) { + for (const ownerItem of owners) { if ( possibleOwners.find( o => - owner.kind.toLowerCase() === o.kind.toLowerCase() && - owner.namespace.toLowerCase() === o.namespace.toLowerCase() && - owner.name.toLowerCase() === o.name.toLowerCase(), + ownerItem.kind.toLowerCase() === o.kind.toLowerCase() && + ownerItem.namespace.toLowerCase() === o.namespace.toLowerCase() && + ownerItem.name.toLowerCase() === o.name.toLowerCase(), ) !== undefined ) { return true; diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md index 3920d8696d..eb4c9a30d7 100644 --- a/plugins/catalog/CHANGELOG.md +++ b/plugins/catalog/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-catalog +## 0.3.0 + +### Minor Changes + +- 019fe39a0: `@backstage/plugin-catalog` stopped exporting hooks and helpers for other + plugins. They are migrated to `@backstage/plugin-catalog-react`. + Change both your dependencies and imports to the new package. + +### Patch Changes + +- 7fc89bae2: Display owner and system as entity page links in the tables of the `api-docs` + plugin. + + Move `isOwnerOf` and `getEntityRelations` from `@backstage/plugin-catalog` to + `@backstage/plugin-catalog-react` and export it from there to use it by other + plugins. + +- b37501a3d: Add `children` option to `addPage`, which will be rendered as the children of the `Route`. +- b37501a3d: Finalize migration to new composability API, with the plugin instance now exported `catalogPlugin`. +- 54c7d02f7: Introduce `TabbedLayout` for creating tabs that are routed. + + ```typescript + + +
This is rendered under /example/anything-here route
+
+
+ ``` + +- Updated dependencies [720149854] +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [019fe39a0] +- Updated dependencies [11cb5ef94] + - @backstage/plugin-scaffolder@0.4.2 + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.14 ### Patch Changes diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index bf30e28571..c806c58e67 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-catalog", - "version": "0.2.14", + "version": "0.3.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ }, "dependencies": { "@backstage/catalog-client": "^0.3.5", - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/plugin-scaffolder": "^0.4.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/plugin-scaffolder": "^0.4.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -52,8 +52,8 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@microsoft/microsoft-graph-types": "^1.25.0", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index 0d8ae56262..7356ccc27b 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -22,7 +22,7 @@ import { ApiRegistry, } from '@backstage/core'; import { catalogApiRef, EntityContext } from '@backstage/plugin-catalog-react'; -import { renderInTestApp, withLogCollector } from '@backstage/test-utils'; +import { renderInTestApp } from '@backstage/test-utils'; import { fireEvent } from '@testing-library/react'; import React from 'react'; import { act } from 'react-dom/test-utils'; @@ -59,34 +59,11 @@ describe('EntityLayout', () => { , ); + expect(rendered.getByText('my-entity')).toBeInTheDocument(); expect(rendered.getByText('tabbed-test-title')).toBeInTheDocument(); expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); }); - it('throws if any other component is a child of TabbedLayout', async () => { - const { error } = await withLogCollector(async () => { - await expect( - renderInTestApp( - - -
tabbed-test-content
-
-
This will cause app to throw
-
, - ), - ).rejects.toThrow(/Child of EntityLayout must be an EntityLayout.Route/); - }); - - expect(error).toEqual([ - expect.stringMatching( - /Child of EntityLayout must be an EntityLayout.Route/, - ), - expect.stringMatching( - /The above error occurred in the component/, - ), - ]); - }); - it('navigates when user clicks different tab', async () => { const rendered = await renderInTestApp( diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index 4f7cb09cb6..c4cd4c5c71 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -16,12 +16,12 @@ import { Entity, ENTITY_DEFAULT_NAMESPACE } from '@backstage/catalog-model'; import { - attachComponentData, Content, Header, HeaderLabel, Page, Progress, + TabbedLayout, } from '@backstage/core'; import { EntityContext, @@ -29,12 +29,9 @@ import { } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; -import React, { - Children, - Fragment, - isValidElement, +import { + default as React, PropsWithChildren, - ReactNode, useContext, useState, } from 'react'; @@ -42,37 +39,6 @@ import { useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; import { FavouriteEntity } from '../FavouriteEntity/FavouriteEntity'; import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; -import { TabbedLayout } from './TabbedLayout'; - -type SubRoute = { - path: string; - title: string; - children: JSX.Element; -}; - -const Route: (props: SubRoute) => null = () => null; - -// This causes all mount points that are discovered within this route to use the path of the route itself -attachComponentData(Route, 'core.gatherMountPoints', true); - -export function createSubRoutesFromChildren(children: ReactNode): SubRoute[] { - return Children.toArray(children).flatMap(child => { - if (!isValidElement(child)) { - return []; - } - - if (child.type === Fragment) { - return createSubRoutesFromChildren(child.props.children); - } - - if (child.type !== Route) { - throw new Error('Child of EntityLayout must be an EntityLayout.Route'); - } - - const { path, title, children } = child.props; - return [{ path, title, children }]; - }); -} const EntityLayoutTitle = ({ entity, @@ -132,7 +98,6 @@ export const EntityLayout = ({ children }: PropsWithChildren<{}>) => { const { kind, namespace, name } = useEntityCompoundName(); const { entity, loading, error } = useContext(EntityContext); - const routes = createSubRoutesFromChildren(children); const { headerTitle, headerType } = headerProps( kind, namespace, @@ -174,7 +139,7 @@ export const EntityLayout = ({ children }: PropsWithChildren<{}>) => { {loading && } - {entity && } + {entity && {children}} {error && ( @@ -191,4 +156,4 @@ export const EntityLayout = ({ children }: PropsWithChildren<{}>) => { ); }; -EntityLayout.Route = Route; +EntityLayout.Route = TabbedLayout.Route; diff --git a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx index d5593edcc2..a3ede42288 100644 --- a/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx +++ b/plugins/catalog/src/components/EntitySwitch/EntitySwitch.tsx @@ -35,8 +35,8 @@ type SwitchCase = { children: JSX.Element; }; -function createSwitchCasesFromChildren(children: ReactNode): SwitchCase[] { - return Children.toArray(children).flatMap(child => { +function createSwitchCasesFromChildren(childrenNode: ReactNode): SwitchCase[] { + return Children.toArray(childrenNode).flatMap(child => { if (!isValidElement(child)) { return []; } diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 45025a94e3..99715821a6 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-circleci +## 0.2.7 + +### Patch Changes + +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.6 ### Patch Changes diff --git a/plugins/circleci/dev/index.tsx b/plugins/circleci/dev/index.tsx index 812a5585d4..19a32c0d04 100644 --- a/plugins/circleci/dev/index.tsx +++ b/plugins/circleci/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { circleCIPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(circleCIPlugin).render(); diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index f128540784..37e4425b23 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -50,8 +50,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/circleci/src/components/Router.tsx b/plugins/circleci/src/components/Router.tsx index 5fe9a20eff..d18b6c1e61 100644 --- a/plugins/circleci/src/components/Router.tsx +++ b/plugins/circleci/src/components/Router.tsx @@ -21,15 +21,25 @@ import { BuildWithStepsPage } from './BuildWithStepsPage/'; import { BuildsPage } from './BuildsPage'; import { CIRCLECI_ANNOTATION } from '../constants'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { MissingAnnotationEmptyState } from '@backstage/core'; -export const isPluginApplicableToEntity = (entity: Entity) => +export const isCircleCIAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[CIRCLECI_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => - !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isCircleCIAvailable(entity)) { + return ; + } + + return ( } /> /> ); +}; diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts index c6cb140208..2a37cbdee0 100644 --- a/plugins/circleci/src/index.ts +++ b/plugins/circleci/src/index.ts @@ -14,8 +14,16 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + circleCIPlugin, + circleCIPlugin as plugin, + EntityCircleCIContent, +} from './plugin'; export * from './api'; export * from './route-refs'; -export { Router, isPluginApplicableToEntity } from './components/Router'; +export { + Router, + isCircleCIAvailable, + isCircleCIAvailable as isPluginApplicableToEntity, +} from './components/Router'; export { CIRCLECI_ANNOTATION } from './constants'; diff --git a/plugins/circleci/src/plugin.test.ts b/plugins/circleci/src/plugin.test.ts index 821a503257..bdd637027c 100644 --- a/plugins/circleci/src/plugin.test.ts +++ b/plugins/circleci/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { circleCIPlugin } from './plugin'; describe('circleci', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(circleCIPlugin).toBeDefined(); }); }); diff --git a/plugins/circleci/src/plugin.ts b/plugins/circleci/src/plugin.ts index c5bb122ed3..4e0d485726 100644 --- a/plugins/circleci/src/plugin.ts +++ b/plugins/circleci/src/plugin.ts @@ -18,10 +18,12 @@ import { createPlugin, createApiFactory, discoveryApiRef, + createRoutableExtension, } from '@backstage/core'; import { circleCIApiRef, CircleCIApi } from './api'; +import { circleCIRouteRef } from './route-refs'; -export const plugin = createPlugin({ +export const circleCIPlugin = createPlugin({ id: 'circleci', apis: [ createApiFactory({ @@ -31,3 +33,10 @@ export const plugin = createPlugin({ }), ], }); + +export const EntityCircleCIContent = circleCIPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: circleCIRouteRef, + }), +); diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index d174677f51..ae3f8faada 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,26 @@ # @backstage/plugin-cloudbuild +## 0.2.8 + +### Patch Changes + +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/cloudbuild/dev/index.tsx b/plugins/cloudbuild/dev/index.tsx index 264d6f801f..26fb65d151 100644 --- a/plugins/cloudbuild/dev/index.tsx +++ b/plugins/cloudbuild/dev/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { cloudbuildPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(cloudbuildPlugin).render(); diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index 3e5bbef3a9..cbad4928b7 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -46,8 +47,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/cloudbuild/src/components/Cards/Cards.tsx b/plugins/cloudbuild/src/components/Cards/Cards.tsx index bbfb0b59c3..75ae33027e 100644 --- a/plugins/cloudbuild/src/components/Cards/Cards.tsx +++ b/plugins/cloudbuild/src/components/Cards/Cards.tsx @@ -17,6 +17,7 @@ import React, { useEffect } from 'react'; import { useWorkflowRuns } from '../useWorkflowRuns'; import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core'; import { @@ -72,12 +73,13 @@ const WidgetContent = ({ }; export const LatestWorkflowRunCard = ({ - entity, branch = 'master', }: { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; branch: string; }) => { + const { entity } = useEntity(); const errorApi = useApi(errorApiRef); const projectId = entity?.metadata.annotations?.[CLOUDBUILD_ANNOTATION] || ''; @@ -104,13 +106,17 @@ export const LatestWorkflowRunCard = ({ }; export const LatestWorkflowsForBranchCard = ({ - entity, branch = 'master', }: { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; branch: string; -}) => ( - - - -); +}) => { + const { entity } = useEntity(); + + return ( + + + + ); +}; diff --git a/plugins/cloudbuild/src/components/Router.tsx b/plugins/cloudbuild/src/components/Router.tsx index af4959f4be..0f71efcf86 100644 --- a/plugins/cloudbuild/src/components/Router.tsx +++ b/plugins/cloudbuild/src/components/Router.tsx @@ -15,6 +15,7 @@ */ import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Routes, Route } from 'react-router'; import { rootRouteRef, buildRouteRef } from '../plugin'; import { WorkflowRunDetails } from './WorkflowRunDetails'; @@ -22,14 +23,22 @@ import { WorkflowRunsTable } from './WorkflowRunsTable'; import { CLOUDBUILD_ANNOTATION } from './useProjectName'; import { MissingAnnotationEmptyState } from '@backstage/core'; -export const isPluginApplicableToEntity = (entity: Entity) => +export const isCloudbuildAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[CLOUDBUILD_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => - // TODO(shmidt-i): move warning to a separate standardized component - !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isCloudbuildAvailable(entity)) { + // TODO(shmidt-i): move warning to a separate standardized component + return ; + } + return ( ) ); +}; diff --git a/plugins/cloudbuild/src/index.ts b/plugins/cloudbuild/src/index.ts index 616bb6b4e7..b7cd6c29d5 100644 --- a/plugins/cloudbuild/src/index.ts +++ b/plugins/cloudbuild/src/index.ts @@ -13,8 +13,18 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { plugin } from './plugin'; +export { + cloudbuildPlugin, + cloudbuildPlugin as plugin, + EntityCloudbuildContent, + EntityLatestCloudbuildRunCard, + EntityLatestCloudbuildsForBranchCard, +} from './plugin'; export * from './api'; -export { Router, isPluginApplicableToEntity } from './components/Router'; +export { + Router, + isCloudbuildAvailable, + isCloudbuildAvailable as isPluginApplicableToEntity, +} from './components/Router'; export * from './components/Cards'; export { CLOUDBUILD_ANNOTATION } from './components/useProjectName'; diff --git a/plugins/cloudbuild/src/plugin.test.ts b/plugins/cloudbuild/src/plugin.test.ts index dae16f1d1b..ea042a901a 100644 --- a/plugins/cloudbuild/src/plugin.test.ts +++ b/plugins/cloudbuild/src/plugin.test.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { plugin } from './plugin'; +import { cloudbuildPlugin } from './plugin'; describe('cloudbuild', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(cloudbuildPlugin).toBeDefined(); }); }); diff --git a/plugins/cloudbuild/src/plugin.ts b/plugins/cloudbuild/src/plugin.ts index ee997cc7ec..f5aaf4696a 100644 --- a/plugins/cloudbuild/src/plugin.ts +++ b/plugins/cloudbuild/src/plugin.ts @@ -18,6 +18,8 @@ import { createRouteRef, createApiFactory, googleAuthApiRef, + createRoutableExtension, + createComponentExtension, } from '@backstage/core'; import { cloudbuildApiRef, CloudbuildClient } from './api'; @@ -31,7 +33,7 @@ export const buildRouteRef = createRouteRef({ title: 'Cloudbuild Run', }); -export const plugin = createPlugin({ +export const cloudbuildPlugin = createPlugin({ id: 'cloudbuild', apis: [ createApiFactory({ @@ -42,4 +44,32 @@ export const plugin = createPlugin({ }, }), ], + routes: { + entityContent: rootRouteRef, + }, }); + +export const EntityCloudbuildContent = cloudbuildPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); + +export const EntityLatestCloudbuildRunCard = cloudbuildPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.LatestWorkflowRunCard), + }, + }), +); + +export const EntityLatestCloudbuildsForBranchCard = cloudbuildPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.LatestWorkflowsForBranchCard), + }, + }), +); diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index 78d4c69da4..87bfd3af07 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-cost-insights +## 0.8.0 + +### Minor Changes + +- 19172f5a9: add alert hooks + +### Patch Changes + +- 4c6a6dddd: Fixed date calculations incorrectly converting to UTC in some cases. This should be a transparent change. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + ## 0.7.0 ### Minor Changes diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 022e778150..0184d50517 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.7.0", + "version": "0.8.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ }, "dependencies": { "@backstage/config": "^0.1.2", - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -55,8 +55,8 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx index 59b2dc9eec..7360c99cf7 100644 --- a/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx +++ b/plugins/cost-insights/src/components/AlertInsights/AlertInsights.tsx @@ -124,18 +124,18 @@ export const AlertInsights = ({ setSnackbarOpen(!!error); }, [error]); - function onSnooze(alert: Alert) { - setAlert(alert); + function onSnooze(alertToSnooze: Alert) { + setAlert(alertToSnooze); setStatus(AlertStatus.Snoozed); } - function onAccept(alert: Alert) { - setAlert(alert); + function onAccept(alertToAccept: Alert) { + setAlert(alertToAccept); setStatus(AlertStatus.Accepted); } - function onDismiss(alert: Alert) { - setAlert(alert); + function onDismiss(alertToDismiss: Alert) { + setAlert(alertToDismiss); setStatus(AlertStatus.Dismissed); } @@ -148,8 +148,8 @@ export const AlertInsights = ({ setStatus(null); } - function onDialogFormSubmit(data: any) { - setData(data); + function onDialogFormSubmit(formData: any) { + setData(formData); } function onSummaryButtonClick() { @@ -175,10 +175,10 @@ export const AlertInsights = ({
{isAlertInsightSectionDisplayed && ( - {active.map((alert, index) => ( + {active.map((activeAlert, index) => ( ) => { +}: PropsWithChildren) => { const classes = useBarChartLabelStyles(); const translateX = width * -0.5; diff --git a/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx b/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx index 11a295965e..8157e05444 100644 --- a/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx +++ b/plugins/cost-insights/src/components/BarChart/BarChartSteps.tsx @@ -18,7 +18,7 @@ import React from 'react'; import { ButtonBase } from '@material-ui/core'; import { useBarChartStepperStyles as useStyles } from '../../utils/styles'; -export type BarChartSteps = { +export type BarChartStepsProps = { steps: number; activeStep: number; onClick: (index: number) => void; @@ -28,7 +28,7 @@ export const BarChartSteps = ({ steps, activeStep, onClick, -}: BarChartSteps) => { +}: BarChartStepsProps) => { const classes = useStyles(); const handleOnClick = (index: number) => ( event: React.MouseEvent, diff --git a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx index 0ef35bf04c..5702e67172 100644 --- a/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx +++ b/plugins/cost-insights/src/components/ProductInsights/ProductInsights.tsx @@ -82,12 +82,7 @@ export const ProductInsights = ({ ); useEffect(() => { - async function getAllProductInsights( - group: string, - project: Maybe, - products: Product[], - lastCompleteBillingDate: string, - ) { + async function getAllProductInsights() { try { dispatchLoadingProducts(true); const responses = await Promise.allSettled( @@ -101,10 +96,10 @@ export const ProductInsights = ({ ), ).then(settledResponseOf); - const initialStates = initialStatesOf(products, responses).sort( + const updatedInitialStates = initialStatesOf(products, responses).sort( totalAggregationSort, ); - setStates(initialStates); + setStates(updatedInitialStates); } catch (e) { setError(e); } finally { @@ -112,7 +107,7 @@ export const ProductInsights = ({ } } - getAllProductInsights(group, project, products, lastCompleteBillingDate); + getAllProductInsights(); }, [ client, group, @@ -125,8 +120,8 @@ export const ProductInsights = ({ useEffect( function handleOnLoaded() { if (onceRef.current) { - const products = initialStates.map(state => state.product); - onLoaded(products); + const initialProducts = initialStates.map(state => state.product); + onLoaded(initialProducts); } else { onceRef.current = true; } diff --git a/plugins/cost-insights/src/types/Alert.ts b/plugins/cost-insights/src/types/Alert.ts index eb8919b71a..84c37a7c15 100644 --- a/plugins/cost-insights/src/types/Alert.ts +++ b/plugins/cost-insights/src/types/Alert.ts @@ -149,7 +149,7 @@ export const AlertSnoozeOptions: AlertSnoozeOption[] = [ label: '1 Month', }, { - duration: Duration.P3M, + duration: Duration.P90D, label: '1 Quarter', }, ]; diff --git a/plugins/cost-insights/src/utils/change.test.ts b/plugins/cost-insights/src/utils/change.test.ts index 7cc03caa0b..9516dfe270 100644 --- a/plugins/cost-insights/src/utils/change.test.ts +++ b/plugins/cost-insights/src/utils/change.test.ts @@ -76,13 +76,13 @@ describe('getPreviousPeriodTotalCost', () => { change: changeOf(MockAggregatedDailyCosts), trendline: trendlineOf(MockAggregatedDailyCosts), }; - const exclusiveEndDate = '2020-09-30'; + const inclusiveEndDate = '2020-09-30'; expect( getPreviousPeriodTotalCost( mockGroupDailyCost.aggregation, Duration.P30D, - exclusiveEndDate, + inclusiveEndDate, ), - ).toEqual(100_000); + ).toEqual(96_600); }); }); diff --git a/plugins/cost-insights/src/utils/change.ts b/plugins/cost-insights/src/utils/change.ts index fc50439aad..950fbfeb82 100644 --- a/plugins/cost-insights/src/utils/change.ts +++ b/plugins/cost-insights/src/utils/change.ts @@ -22,14 +22,13 @@ import { GrowthType, MetricData, Duration, - DEFAULT_DATE_FORMAT, DateAggregation, } from '../types'; import dayjs, { OpUnitType } from 'dayjs'; -import duration from 'dayjs/plugin/duration'; +import durationPlugin from 'dayjs/plugin/duration'; import { inclusiveStartDateOf } from './duration'; -dayjs.extend(duration); +dayjs.extend(durationPlugin); // Used for displaying status colors export function growthOf(ratio: number, amount?: number) { @@ -73,10 +72,7 @@ export function getPreviousPeriodTotalCost( inclusiveEndDate: string, ): number { const dayjsDuration = dayjs.duration(duration); - const startDate = inclusiveStartDateOf( - duration, - dayjs(inclusiveEndDate).add(1, 'day').format(DEFAULT_DATE_FORMAT), - ); + const startDate = inclusiveStartDateOf(duration, inclusiveEndDate); // dayjs doesn't allow adding an ISO 8601 period to dates. const [amount, type]: [number, OpUnitType] = dayjsDuration.days() ? [dayjsDuration.days(), 'day'] diff --git a/plugins/cost-insights/src/utils/duration.ts b/plugins/cost-insights/src/utils/duration.ts index 79ea150fc5..27447537cc 100644 --- a/plugins/cost-insights/src/utils/duration.ts +++ b/plugins/cost-insights/src/utils/duration.ts @@ -24,23 +24,21 @@ export const DEFAULT_DURATION = Duration.P30D; * Derive the start date of a given period, assuming two repeating intervals. * * @param duration see comment on Duration enum - * @param exclusiveEndDate from CostInsightsApi.getLastCompleteBillingDate + 1 day + * @param inclusiveEndDate from CostInsightsApi.getLastCompleteBillingDate */ export function inclusiveStartDateOf( duration: Duration, - exclusiveEndDate: string, + inclusiveEndDate: string, ): string { switch (duration) { case Duration.P7D: case Duration.P30D: case Duration.P90D: - return moment(exclusiveEndDate) - .utc() + return moment(inclusiveEndDate) .subtract(moment.duration(duration).add(moment.duration(duration))) .format(DEFAULT_DATE_FORMAT); case Duration.P3M: - return moment(exclusiveEndDate) - .utc() + return moment(inclusiveEndDate) .startOf('quarter') .subtract(moment.duration(duration).add(moment.duration(duration))) .format(DEFAULT_DATE_FORMAT); @@ -57,13 +55,9 @@ export function exclusiveEndDateOf( case Duration.P7D: case Duration.P30D: case Duration.P90D: - return moment(inclusiveEndDate) - .utc() - .add(1, 'day') - .format(DEFAULT_DATE_FORMAT); + return moment(inclusiveEndDate).add(1, 'day').format(DEFAULT_DATE_FORMAT); case Duration.P3M: return moment(quarterEndDate(inclusiveEndDate)) - .utc() .add(1, 'day') .format(DEFAULT_DATE_FORMAT); default: @@ -76,7 +70,6 @@ export function inclusiveEndDateOf( inclusiveEndDate: string, ): string { return moment(exclusiveEndDateOf(duration, inclusiveEndDate)) - .utc() .subtract(1, 'day') .format(DEFAULT_DATE_FORMAT); } @@ -94,7 +87,7 @@ export function intervalsOf( } export function quarterEndDate(inclusiveEndDate: string): string { - const endDate = moment(inclusiveEndDate).utc(); + const endDate = moment(inclusiveEndDate); const endOfQuarter = endDate.endOf('quarter').format(DEFAULT_DATE_FORMAT); if (endOfQuarter === inclusiveEndDate) { return endDate.format(DEFAULT_DATE_FORMAT); diff --git a/plugins/cost-insights/src/utils/formatters.ts b/plugins/cost-insights/src/utils/formatters.ts index 182c567644..8f2b94f997 100644 --- a/plugins/cost-insights/src/utils/formatters.ts +++ b/plugins/cost-insights/src/utils/formatters.ts @@ -16,7 +16,7 @@ import moment from 'moment'; import pluralize from 'pluralize'; -import { Duration, DEFAULT_DATE_FORMAT } from '../types'; +import { Duration } from '../types'; import { inclusiveEndDateOf, inclusiveStartDateOf } from '../utils/duration'; export type Period = { @@ -92,11 +92,8 @@ export function formatPercent(n: number): string { } export function formatLastTwoLookaheadQuarters(inclusiveEndDate: string) { - const exclusiveEndDate = moment(inclusiveEndDate) - .add(1, 'day') - .format(DEFAULT_DATE_FORMAT); const start = moment( - inclusiveStartDateOf(Duration.P3M, exclusiveEndDate), + inclusiveStartDateOf(Duration.P3M, inclusiveEndDate), ).format('[Q]Q YYYY'); const end = moment(inclusiveEndDateOf(Duration.P3M, inclusiveEndDate)).format( '[Q]Q YYYY', diff --git a/plugins/cost-insights/src/utils/mockData.ts b/plugins/cost-insights/src/utils/mockData.ts index 14ff9aecd1..d4ac4cd2df 100644 --- a/plugins/cost-insights/src/utils/mockData.ts +++ b/plugins/cost-insights/src/utils/mockData.ts @@ -36,7 +36,7 @@ import { getDefaultState as getDefaultLoadingState, } from '../utils/loading'; import { findAlways } from '../utils/assert'; -import { inclusiveStartDateOf } from './duration'; +import { inclusiveEndDateOf, inclusiveStartDateOf } from './duration'; type mockAlertRenderer = (alert: T) => T; type mockEntityRenderer = (entity: T) => T; @@ -228,8 +228,9 @@ export function aggregationFor( baseline: number, ): DateAggregation[] { const { duration, endDate } = parseIntervals(intervals); + const inclusiveEndDate = inclusiveEndDateOf(duration, endDate); const days = dayjs(endDate).diff( - inclusiveStartDateOf(duration, endDate), + inclusiveStartDateOf(duration, inclusiveEndDate), 'day', ); @@ -244,7 +245,7 @@ export function aggregationFor( return [...Array(days).keys()].reduce( (values: DateAggregation[], i: number): DateAggregation[] => { const last = values.length ? values[values.length - 1].amount : baseline; - const date = dayjs(inclusiveStartDateOf(duration, endDate)) + const date = dayjs(inclusiveStartDateOf(duration, inclusiveEndDate)) .add(i, 'day') .format(DEFAULT_DATE_FORMAT); const amount = Math.max(0, last + nextDelta()); diff --git a/plugins/explore-react/.eslintrc.js b/plugins/explore-react/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/explore-react/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/explore-react/CHANGELOG.md b/plugins/explore-react/CHANGELOG.md new file mode 100644 index 0000000000..1b58da0f04 --- /dev/null +++ b/plugins/explore-react/CHANGELOG.md @@ -0,0 +1,14 @@ +# @backstage/plugin-explore-react + +## 0.0.2 + +### Patch Changes + +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 diff --git a/plugins/explore-react/README.md b/plugins/explore-react/README.md new file mode 100644 index 0000000000..4a881955df --- /dev/null +++ b/plugins/explore-react/README.md @@ -0,0 +1,4 @@ +# explore-react + +This package provides helpers to the `explore` plugin that can be imported by +any other plugin or app. diff --git a/plugins/explore-react/package.json b/plugins/explore-react/package.json new file mode 100644 index 0000000000..924c723a67 --- /dev/null +++ b/plugins/explore-react/package.json @@ -0,0 +1,48 @@ +{ + "name": "@backstage/plugin-explore-react", + "version": "0.0.2", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "private": false, + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/backstage/backstage", + "directory": "plugins/explore-react" + }, + "keywords": [ + "backstage" + ], + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "clean": "backstage-cli clean", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack" + }, + "dependencies": { + "@backstage/core": "^0.6.0" + }, + "devDependencies": { + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", + "@backstage/test-utils": "^0.1.6", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^12.0.0", + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/explore/src/components/Router.tsx b/plugins/explore-react/src/index.ts similarity index 66% rename from plugins/explore/src/components/Router.tsx rename to plugins/explore-react/src/index.ts index becb2522d0..d46f5d4f27 100644 --- a/plugins/explore/src/components/Router.tsx +++ b/plugins/explore-react/src/index.ts @@ -14,13 +14,4 @@ * limitations under the License. */ -import React from 'react'; -import { Route, Routes } from 'react-router'; -import { ExplorePluginPage } from './ExplorePluginPage'; -import { rootRouteRef } from '../plugin'; - -export const Router = () => ( - - } /> - -); +export * from './tools'; diff --git a/plugins/explore-react/src/setupTests.ts b/plugins/explore-react/src/setupTests.ts new file mode 100644 index 0000000000..825bcd4115 --- /dev/null +++ b/plugins/explore-react/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; diff --git a/plugins/explore-react/src/tools/api.test.ts b/plugins/explore-react/src/tools/api.test.ts new file mode 100644 index 0000000000..386d66ead2 --- /dev/null +++ b/plugins/explore-react/src/tools/api.test.ts @@ -0,0 +1,23 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { exploreToolsConfigRef } from './api'; + +describe('api', () => { + // This is a dummy test to have at least one test in the package. + it('api ref exists', () => { + expect(exploreToolsConfigRef.id).toBe('plugin.explore.toolsconfig'); + }); +}); diff --git a/plugins/explore-react/src/tools/api.ts b/plugins/explore-react/src/tools/api.ts new file mode 100644 index 0000000000..5d96033546 --- /dev/null +++ b/plugins/explore-react/src/tools/api.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createApiRef } from '@backstage/core'; + +export const exploreToolsConfigRef = createApiRef({ + id: 'plugin.explore.toolsconfig', + description: 'Used to configure tools displayed in the explore plugin', +}); + +export type ExploreTool = { + title: string; + description?: string; + url: string; + image: string; + tags?: string[]; + lifecycle?: string; + newsTag?: string; +}; + +export interface ExploreToolsConfig { + getTools: () => Promise; +} diff --git a/plugins/explore-react/src/tools/index.ts b/plugins/explore-react/src/tools/index.ts new file mode 100644 index 0000000000..7f727881ca --- /dev/null +++ b/plugins/explore-react/src/tools/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { exploreToolsConfigRef } from './api'; +export type { ExploreTool, ExploreToolsConfig } from './api'; diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index 75a0471ce7..0d2daf37b5 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-explore +## 0.2.4 + +### Patch Changes + +- 54c7d02f7: Introduce `TabbedLayout` for creating tabs that are routed. + + ```typescript + + +
This is rendered under /example/anything-here route
+
+
+ ``` + +- 806929fe2: Rework the explore plugin to allow the user to explore things in the ecosystem, + including tools and domains. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + - @backstage/plugin-explore-react@0.0.2 + ## 0.2.3 ### Patch Changes diff --git a/plugins/explore/README.md b/plugins/explore/README.md index d5453e594c..ec00cb4dee 100644 --- a/plugins/explore/README.md +++ b/plugins/explore/README.md @@ -1,7 +1,32 @@ -# Title +# explore Welcome to the explore plugin! +This plugin helps to visualize the domains and tools in your ecosystem. -## Sub-section 1 +## Getting started -## Sub-section 2 +To install the plugin, include the following import your `plugins.ts`: + +```typescript +export { explorePlugin } from '@backstage/plugin-explore'; +``` + +Register the route in `App.tsx`: + +```typescript +import { ExplorePage } from '@backstage/plugin-explore'; + +... + +} /> +``` + +Add a link to the sidebar in `Root.tsx`: + +```typescript +import LayersIcon from '@material-ui/icons/Layers'; + +... + + +``` diff --git a/plugins/explore/dev/index.tsx b/plugins/explore/dev/index.tsx index 812a5585d4..9a7b66a7b5 100644 --- a/plugins/explore/dev/index.tsx +++ b/plugins/explore/dev/index.tsx @@ -14,7 +14,59 @@ * limitations under the License. */ +import { Entity } from '@backstage/catalog-model'; import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; +import React from 'react'; +import { ExplorePage, explorePlugin } from '../src'; +import { exampleTools } from '../src/util/examples'; -createDevApp().registerPlugin(plugin).render(); +createDevApp() + .registerPlugin(explorePlugin) + .registerApi({ + api: exploreToolsConfigRef, + deps: {}, + factory: () => ({ + async getTools() { + return exampleTools; + }, + }), + }) + .registerApi({ + api: catalogApiRef, + deps: {}, + factory: () => + ({ + async getEntities() { + const domainNames = [ + 'playback', + 'artists', + 'payments', + 'analytics', + 'songs', + 'devops', + ]; + + return { + items: domainNames.map( + (n, i) => + ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: n, + description: `Everything about ${n}`, + tags: i % 2 === 0 ? [n] : undefined, + }, + spec: { + owner: `${n}@example.com`, + }, + } as Entity), + ), + }; + }, + } as CatalogApi), + }) + .addPage({ element: , title: 'Explore' }) + .render(); diff --git a/plugins/explore/package.json b/plugins/explore/package.json index c6668c9c4d..e9d21cc221 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.2.3", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,20 +30,23 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/plugin-explore-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "classnames": "^2.2.6", "react": "^16.13.1", "react-dom": "^16.13.1", - "react-router": "6.0.0-beta.0", + "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/explore/src/components/DomainCard/DomainCard.test.tsx b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx new file mode 100644 index 0000000000..9d409051f6 --- /dev/null +++ b/plugins/explore/src/components/DomainCard/DomainCard.test.tsx @@ -0,0 +1,49 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DomainEntity } from '@backstage/catalog-model'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { DomainCard } from './DomainCard'; + +describe('', () => { + it('renders a domain card', () => { + const entity: DomainEntity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'artists', + description: 'Everything about artists', + tags: ['a-tag'], + }, + spec: { + owner: 'guest', + }, + }; + const { getByText } = render(, { + wrapper: MemoryRouter, + }); + + expect(getByText('artists')).toBeInTheDocument(); + expect(getByText('Everything about artists')).toBeInTheDocument(); + expect(getByText('a-tag')).toBeInTheDocument(); + expect(getByText('Explore').parentElement).toHaveAttribute( + 'href', + '/catalog/default/domain/artists', + ); + }); +}); diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx new file mode 100644 index 0000000000..2c0126b94a --- /dev/null +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DomainEntity } from '@backstage/catalog-model'; +import { ItemCard } from '@backstage/core'; +import { + entityRoute, + entityRouteParams, +} from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { generatePath } from 'react-router-dom'; + +type DomainCardProps = { + entity: DomainEntity; +}; + +export const DomainCard = ({ entity }: DomainCardProps) => ( + +); diff --git a/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx b/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx new file mode 100644 index 0000000000..bad3ca581b --- /dev/null +++ b/plugins/explore/src/components/DomainCard/DomainCardGrid.test.tsx @@ -0,0 +1,54 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DomainEntity } from '@backstage/catalog-model'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { DomainCardGrid } from './DomainCardGrid'; + +describe('', () => { + it('renders a grid of domain cards', () => { + const entities: DomainEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'playback', + }, + spec: { + owner: 'guest', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'artists', + }, + spec: { + owner: 'guest', + }, + }, + ]; + const { getByText } = render(, { + wrapper: MemoryRouter, + }); + + expect(getByText('artists')).toBeInTheDocument(); + expect(getByText('playback')).toBeInTheDocument(); + }); +}); diff --git a/plugins/explore/src/components/DomainCard/DomainCardGrid.tsx b/plugins/explore/src/components/DomainCard/DomainCardGrid.tsx new file mode 100644 index 0000000000..b4a13aa39f --- /dev/null +++ b/plugins/explore/src/components/DomainCard/DomainCardGrid.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DomainEntity } from '@backstage/catalog-model'; +import { Grid } from '@material-ui/core'; +import React from 'react'; +import { DomainCard } from '.'; + +type DomainCardGridProps = { + entities: DomainEntity[]; +}; + +export const DomainCardGrid = ({ entities }: DomainCardGridProps) => ( + + {entities.map((e, i) => ( + + + + ))} + +); diff --git a/plugins/explore/src/components/DomainCard/index.ts b/plugins/explore/src/components/DomainCard/index.ts new file mode 100644 index 0000000000..6acf10800a --- /dev/null +++ b/plugins/explore/src/components/DomainCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { DomainCard } from './DomainCard'; +export { DomainCardGrid } from './DomainCardGrid'; diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx new file mode 100644 index 0000000000..4846565bdd --- /dev/null +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -0,0 +1,93 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { DomainEntity } from '@backstage/catalog-model'; +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { render, waitFor } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { DomainExplorerContent } from './DomainExplorerContent'; + +describe('', () => { + const catalogApi: jest.Mocked = { + addLocation: jest.fn(_a => new Promise(() => {})), + getEntities: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + getEntityByName: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + + {children} + + + ); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders a grid of domains', async () => { + const entities: DomainEntity[] = [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'playback', + }, + spec: { + owner: 'guest', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Domain', + metadata: { + name: 'artists', + }, + spec: { + owner: 'guest', + }, + }, + ]; + catalogApi.getEntities.mockResolvedValue({ items: entities }); + + const { getByText } = render(, { + wrapper: Wrapper, + }); + + await waitFor(() => { + expect(getByText('artists')).toBeInTheDocument(); + expect(getByText('playback')).toBeInTheDocument(); + }); + }); + + it('renders empty state', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + + const { getByText } = render(, { + wrapper: Wrapper, + }); + + await waitFor(() => + expect(getByText('No domains to display')).toBeInTheDocument(), + ); + }); +}); diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx new file mode 100644 index 0000000000..ca00c525c9 --- /dev/null +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.tsx @@ -0,0 +1,67 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DomainEntity } from '@backstage/catalog-model'; +import { + Content, + ContentHeader, + EmptyState, + Progress, + SupportButton, + useApi, +} from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { Button } from '@material-ui/core'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { DomainCardGrid } from '../DomainCard'; + +export const DomainExplorerContent = () => { + const catalogApi = useApi(catalogApiRef); + const { value: entities, loading } = useAsync(async () => { + const response = await catalogApi.getEntities({ + filter: { kind: 'domain' }, + }); + + return response.items as DomainEntity[]; + }, [catalogApi]); + + return ( + + + Discover the domains in your ecosystem. + + + {loading && } + {!loading && (!entities || entities.length === 0) && ( + + Read more + + } + /> + )} + {!loading && entities && } + + ); +}; diff --git a/plugins/explore/src/components/DomainExplorerContent/index.ts b/plugins/explore/src/components/DomainExplorerContent/index.ts new file mode 100644 index 0000000000..4012332006 --- /dev/null +++ b/plugins/explore/src/components/DomainExplorerContent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { DomainExplorerContent } from './DomainExplorerContent'; diff --git a/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx b/plugins/explore/src/components/ExplorePage/ExplorePage.tsx similarity index 51% rename from plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx rename to plugins/explore/src/components/ExplorePage/ExplorePage.tsx index fe99e70575..3f9394a1ab 100644 --- a/plugins/rollbar/src/components/RollbarHome/RollbarHome.tsx +++ b/plugins/explore/src/components/ExplorePage/ExplorePage.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 Spotify AB + * Copyright 2021 Spotify AB * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,28 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - +import { configApiRef, Header, Page, useApi } from '@backstage/core'; import React from 'react'; -import { Content, Header, Page } from '@backstage/core'; -import { RollbarProjectTable } from '../RollbarProjectTable/RollbarProjectTable'; -import { useRollbarEntities } from '../../hooks/useRollbarEntities'; - -export const RollbarHome = () => { - const { entities, loading, error } = useRollbarEntities(); +import { ExploreTabs } from './ExploreTabs'; +export const ExplorePage = () => { + const configApi = useApi(configApiRef); + const organizationName = + configApi.getOptionalString('organization.name') ?? 'Backstage'; return ( - +
- - - + + ); }; diff --git a/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx b/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx new file mode 100644 index 0000000000..0cf28f9f1c --- /dev/null +++ b/plugins/explore/src/components/ExplorePage/ExploreTabs.tsx @@ -0,0 +1,30 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { TabbedLayout } from '@backstage/core'; +import React from 'react'; +import { DomainExplorerContent } from '../DomainExplorerContent'; +import { ToolExplorerContent } from '../ToolExplorerContent'; + +export const ExploreTabs = () => ( + + + + + + + + +); diff --git a/plugins/explore/src/components/ExplorePage/index.ts b/plugins/explore/src/components/ExplorePage/index.ts new file mode 100644 index 0000000000..b075c410c3 --- /dev/null +++ b/plugins/explore/src/components/ExplorePage/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ExplorePage } from './ExplorePage'; diff --git a/plugins/explore/src/components/ExploreCard.test.js b/plugins/explore/src/components/ToolCard/ToolCard.test.tsx similarity index 70% rename from plugins/explore/src/components/ExploreCard.test.js rename to plugins/explore/src/components/ToolCard/ToolCard.test.tsx index 45652015a5..b3c1737ae6 100644 --- a/plugins/explore/src/components/ExploreCard.test.js +++ b/plugins/explore/src/components/ToolCard/ToolCard.test.tsx @@ -14,11 +14,10 @@ * limitations under the License. */ -import React from 'react'; -import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; - -import ExploreCard from './ExploreCard'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { ToolCard } from './ToolCard'; const minProps = { card: { @@ -30,37 +29,31 @@ const minProps = { }, }; -describe('', () => { +describe('', () => { it('renders without exploding', () => { - const { getByText } = render(wrapInTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText('Explore')).toBeInTheDocument(); }); it('renders props correctly', () => { - const { getByText } = render(wrapInTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText(minProps.card.title)).toBeInTheDocument(); expect(getByText(minProps.card.description)).toBeInTheDocument(); }); it('should link out', () => { - const rendered = render(wrapInTestApp()); + const rendered = render(wrapInTestApp()); const anchor = rendered.container.querySelector('a'); - expect(anchor.href).toBe(minProps.card.url); + expect(anchor).toHaveAttribute('href', minProps.card.url); }); it('renders default description when missing', () => { - const propsWithoutDescription = { - card: { - card: { - title: 'Title', - url: 'http://spotify.com/', - image: 'https://developer.spotify.com/assets/WebAPI_intro.png', - }, - }, + const card = { + title: 'Title', + url: 'http://spotify.com/', + image: 'https://developer.spotify.com/assets/WebAPI_intro.png', }; - const { getByText } = render( - wrapInTestApp(), - ); + const { getByText } = render(wrapInTestApp()); expect(getByText('Description missing')).toBeInTheDocument(); }); @@ -74,13 +67,13 @@ describe('', () => { }, }; const { queryByText } = render( - wrapInTestApp(), + wrapInTestApp(), ); expect(queryByText('GA')).not.toBeInTheDocument(); }); it('renders tags correctly', () => { - const { getByText } = render(wrapInTestApp()); + const { getByText } = render(wrapInTestApp()); expect(getByText(minProps.card.tags[0])).toBeInTheDocument(); expect(getByText(minProps.card.tags[1])).toBeInTheDocument(); }); diff --git a/plugins/explore/src/components/ExploreCard.tsx b/plugins/explore/src/components/ToolCard/ToolCard.tsx similarity index 91% rename from plugins/explore/src/components/ExploreCard.tsx rename to plugins/explore/src/components/ToolCard/ToolCard.tsx index 8d1348c116..c6d812b098 100644 --- a/plugins/explore/src/components/ExploreCard.tsx +++ b/plugins/explore/src/components/ToolCard/ToolCard.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import React from 'react'; -import classNames from 'classnames'; +import { ExploreTool } from '@backstage/plugin-explore-react'; +import { BackstageTheme } from '@backstage/theme'; import { Button, Card, @@ -23,10 +23,13 @@ import { CardContent, CardMedia, Chip, - Typography, makeStyles, + Typography, } from '@material-ui/core'; -import { BackstageTheme } from '@backstage/theme'; +import classNames from 'classnames'; +import React from 'react'; + +// TODO: Align styling between Domain and ToolCard const useStyles = makeStyles(theme => ({ card: { @@ -65,22 +68,12 @@ const useStyles = makeStyles(theme => ({ }, })); -export type CardData = { - title: string; - description: string; - url: string; - image: string; - tags?: string[]; - lifecycle?: string; - newsTag?: string; -}; - type Props = { - card: CardData; + card: ExploreTool; objectFit?: 'cover' | 'contain'; }; -const ExploreCard = ({ card, objectFit }: Props) => { +export const ToolCard = ({ card, objectFit }: Props) => { const classes = useStyles(); const { title, description, url, image, lifecycle, newsTag, tags } = card; @@ -130,5 +123,3 @@ const ExploreCard = ({ card, objectFit }: Props) => { ); }; - -export default ExploreCard; diff --git a/plugins/explore/src/components/ToolCard/ToolCardGrid.tsx b/plugins/explore/src/components/ToolCard/ToolCardGrid.tsx new file mode 100644 index 0000000000..5b6fbd1834 --- /dev/null +++ b/plugins/explore/src/components/ToolCard/ToolCardGrid.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ExploreTool } from '@backstage/plugin-explore-react'; +import { BackstageTheme } from '@backstage/theme'; +import { makeStyles } from '@material-ui/core'; +import React from 'react'; +import { ToolCard } from './ToolCard'; + +const useStyles = makeStyles(theme => ({ + container: { + display: 'grid', + gridTemplateColumns: 'repeat(auto-fill, 296px)', + gridGap: theme.spacing(3), + marginBottom: theme.spacing(6), + }, +})); + +type ToolCardGridProps = { + tools: ExploreTool[]; +}; + +export const ToolCardGrid = ({ tools }: ToolCardGridProps) => { + const classes = useStyles(); + + return ( +
+ {tools.map((card: ExploreTool, ix: any) => ( + + ))} +
+ ); +}; diff --git a/plugins/explore/src/components/ToolCard/index.ts b/plugins/explore/src/components/ToolCard/index.ts new file mode 100644 index 0000000000..84599aa163 --- /dev/null +++ b/plugins/explore/src/components/ToolCard/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ToolCard } from './ToolCard'; +export { ToolCardGrid } from './ToolCardGrid'; diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx new file mode 100644 index 0000000000..b0e1330ff3 --- /dev/null +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ApiProvider, ApiRegistry } from '@backstage/core'; +import { + ExploreTool, + exploreToolsConfigRef, +} from '@backstage/plugin-explore-react'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import { render, waitFor } from '@testing-library/react'; +import React from 'react'; +import { MemoryRouter } from 'react-router-dom'; +import { ToolExplorerContent } from './ToolExplorerContent'; + +describe('', () => { + const exploreToolsConfigApi: jest.Mocked = { + getTools: jest.fn(), + }; + + const Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + + + {children} + + + + ); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('renders a grid of tools', async () => { + const tools: ExploreTool[] = [ + { + title: 'Lighthouse', + description: + "Google's Lighthouse tool is a great resource for benchmarking and improving the accessibility, performance, SEO, and best practices of your website.", + url: '/lighthouse', + image: + 'https://raw.githubusercontent.com/GoogleChrome/lighthouse/8b3d7f052b2e64dd857e741d7395647f487697e7/assets/lighthouse-logo.png', + tags: ['web', 'seo', 'accessibility', 'performance'], + }, + { + title: 'Tech Radar', + description: + 'Tech Radar is a list of technologies, complemented by an assessment result, called ring assignment.', + url: '/tech-radar', + image: + 'https://storage.googleapis.com/wf-blogs-engineering-media/2018/09/fe13bb32-wf-tech-radar-hero-1024x597.png', + tags: ['standards', 'landscape'], + }, + ]; + exploreToolsConfigApi.getTools.mockResolvedValue(tools); + + const { getByText } = render(, { + wrapper: Wrapper, + }); + + await waitFor(() => { + expect(getByText('Lighthouse')).toBeInTheDocument(); + expect(getByText('Tech Radar')).toBeInTheDocument(); + }); + }); + + it('renders empty state', async () => { + exploreToolsConfigApi.getTools.mockResolvedValue([]); + + const { getByText } = render(, { + wrapper: Wrapper, + }); + + await waitFor(() => + expect(getByText('No tools to display')).toBeInTheDocument(), + ); + }); +}); diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx new file mode 100644 index 0000000000..8b8b3f0a61 --- /dev/null +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.tsx @@ -0,0 +1,52 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + Content, + ContentHeader, + EmptyState, + Progress, + SupportButton, + useApi, +} from '@backstage/core'; +import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; +import React from 'react'; +import { useAsync } from 'react-use'; +import { ToolCardGrid } from '../ToolCard'; + +export const ToolExplorerContent = () => { + const exploreToolsConfigApi = useApi(exploreToolsConfigRef); + const { value: tools, loading } = useAsync(async () => { + return await exploreToolsConfigApi.getTools(); + }, [exploreToolsConfigApi]); + + return ( + + + Discover the tools in your ecosystem. + + + {loading && } + {!loading && (!tools || tools.length === 0) && ( + + )} + {!loading && tools && } + + ); +}; diff --git a/plugins/explore/src/components/ToolExplorerContent/index.ts b/plugins/explore/src/components/ToolExplorerContent/index.ts new file mode 100644 index 0000000000..8fb3072d46 --- /dev/null +++ b/plugins/explore/src/components/ToolExplorerContent/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { ToolExplorerContent } from './ToolExplorerContent'; diff --git a/plugins/explore/src/extensions.tsx b/plugins/explore/src/extensions.tsx new file mode 100644 index 0000000000..cdf43d3035 --- /dev/null +++ b/plugins/explore/src/extensions.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { createRoutableExtension } from '@backstage/core'; +import { explorePlugin } from './plugin'; +import { exploreRouteRef } from './routes'; + +export const ExplorePage = explorePlugin.provide( + createRoutableExtension({ + component: () => + import('./components/ExplorePage').then(m => m.ExplorePage), + mountPoint: exploreRouteRef, + }), +); diff --git a/plugins/explore/src/index.ts b/plugins/explore/src/index.ts index ff7857cacd..70a00f5bbb 100644 --- a/plugins/explore/src/index.ts +++ b/plugins/explore/src/index.ts @@ -14,5 +14,6 @@ * limitations under the License. */ -export { plugin } from './plugin'; -export { Router } from './components/Router'; +export * from './extensions'; +export { explorePlugin } from './plugin'; +export * from './routes'; diff --git a/plugins/explore/src/plugin.test.ts b/plugins/explore/src/plugin.test.ts index d6503c038b..652a0590dc 100644 --- a/plugins/explore/src/plugin.test.ts +++ b/plugins/explore/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { explorePlugin } from './plugin'; describe('explore', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(explorePlugin).toBeDefined(); }); }); diff --git a/plugins/explore/src/plugin.ts b/plugins/explore/src/plugin.ts index 75ea892242..27573eaeb9 100644 --- a/plugins/explore/src/plugin.ts +++ b/plugins/explore/src/plugin.ts @@ -14,9 +14,27 @@ * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; +import { createApiFactory, createPlugin } from '@backstage/core'; +import { exploreToolsConfigRef } from '@backstage/plugin-explore-react'; +import { exploreRouteRef } from './routes'; +import { exampleTools } from './util/examples'; -export const rootRouteRef = createRouteRef({ path: '', title: 'Explore' }); -export const plugin = createPlugin({ +export const explorePlugin = createPlugin({ id: 'explore', + apis: [ + // Register a default for exploreToolsConfigRef, you may want to override + // the API locally in your app. + createApiFactory({ + api: exploreToolsConfigRef, + deps: {}, + factory: () => ({ + async getTools() { + return exampleTools; + }, + }), + }), + ], + routes: { + explore: exploreRouteRef, + }, }); diff --git a/plugins/rollbar/src/routes.ts b/plugins/explore/src/routes.ts similarity index 69% rename from plugins/rollbar/src/routes.ts rename to plugins/explore/src/routes.ts index b514dff3c8..c41a53128f 100644 --- a/plugins/rollbar/src/routes.ts +++ b/plugins/explore/src/routes.ts @@ -18,19 +18,7 @@ import { createRouteRef } from '@backstage/core'; const NoIcon = () => null; -export const rootRouteRef = createRouteRef({ +export const exploreRouteRef = createRouteRef({ icon: NoIcon, - path: '/rollbar', - title: 'Rollbar Home', -}); - -export const entityRouteRef = createRouteRef({ - path: '/rollbar/:optionalNamespaceAndName', - title: 'Rollbar', -}); - -export const catalogRouteRef = createRouteRef({ - icon: NoIcon, - path: '', - title: 'Rollbar', + title: 'Explore', }); diff --git a/plugins/explore/src/components/ExplorePluginPage.tsx b/plugins/explore/src/util/examples.ts similarity index 75% rename from plugins/explore/src/components/ExplorePluginPage.tsx rename to plugins/explore/src/util/examples.ts index f3e75e0011..d0a4cdd1f4 100644 --- a/plugins/explore/src/components/ExplorePluginPage.tsx +++ b/plugins/explore/src/util/examples.ts @@ -14,28 +14,7 @@ * limitations under the License. */ -import React from 'react'; -import { makeStyles, Typography } from '@material-ui/core'; -import { - Content, - ContentHeader, - Header, - Page, - SupportButton, -} from '@backstage/core'; -import ExploreCard, { CardData } from './ExploreCard'; -import { BackstageTheme } from '@backstage/theme'; - -const useStyles = makeStyles(theme => ({ - container: { - display: 'grid', - gridTemplateColumns: 'repeat(auto-fill, 296px)', - gridGap: theme.spacing(3), - marginBottom: theme.spacing(6), - }, -})); - -const toolsCards = [ +export const exampleTools = [ { title: 'New Relic', description: @@ -112,28 +91,3 @@ const toolsCards = [ tags: ['rollbar', 'monitoring', 'errors'], }, ]; - -export const ExplorePluginPage = () => { - const classes = useStyles(); - - return ( - -
- - - - Explore tools available in Backstage - - -
- {toolsCards.map((card: CardData, ix: any) => ( - - ))} -
-
- - ); -}; diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 6fa8584017..de1e2fda06 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-fossa +## 0.2.0 + +### Minor Changes + +- 5ac9df899: Port FOSSA plugin to new extension model. + + If you are using the FOSSA plugin adjust the plugin import from `plugin` to + `fossaPlugin` and replace `` with ``. + +### Patch Changes + +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.1.2 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 879f12c6f6..74f04e3fd0 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-fossa", - "version": "0.1.2", + "version": "0.2.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -44,8 +44,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md index 3b4a168644..92f1da463e 100644 --- a/plugins/gcp-projects/CHANGELOG.md +++ b/plugins/gcp-projects/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-gcp-projects +## 0.2.4 + +### Patch Changes + +- bc5082a00: Migrate to new composability API, exporting the plugin as `gcpProjectsPlugin` and page as `GcpProjectsPage`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + ## 0.2.3 ### Patch Changes diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index d48295e204..fced060d99 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gcp-projects", - "version": "0.2.3", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,8 +41,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 1bd0481610..a9bbf67a9a 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,31 @@ # @backstage/plugin-github-actions +## 0.3.1 + +### Patch Changes + +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [c4abcdb60] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [064c513e1] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [3149bfe63] +- Updated dependencies [54c7d02f7] +- Updated dependencies [2e62aea6f] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/integration@0.3.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.3.0 ### Minor Changes diff --git a/plugins/github-actions/dev/index.tsx b/plugins/github-actions/dev/index.tsx index 812a5585d4..9156224202 100644 --- a/plugins/github-actions/dev/index.tsx +++ b/plugins/github-actions/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { githubActionsPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(githubActionsPlugin).render(); diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index beb35b3cb0..7d05395b42 100644 --- a/plugins/github-actions/package.json +++ b/plugins/github-actions/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-github-actions", - "version": "0.3.0", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/integration": "^0.3.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.0", + "@backstage/integration": "^0.3.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -49,8 +50,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx index f1c27cb666..202bf5aa92 100644 --- a/plugins/github-actions/src/components/Cards/Cards.tsx +++ b/plugins/github-actions/src/components/Cards/Cards.tsx @@ -17,6 +17,7 @@ import React, { useEffect } from 'react'; import { useWorkflowRuns } from '../useWorkflowRuns'; import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { WorkflowRunStatus } from '../WorkflowRunStatus'; import { @@ -81,11 +82,11 @@ const WidgetContent = ({ }; export const LatestWorkflowRunCard = ({ - entity, branch = 'master', // Display the card full height suitable for variant, }: Props) => { + const { entity } = useEntity(); const config = useApi(configApiRef); const errorApi = useApi(errorApiRef); // TODO: Get github hostname from metadata annotation @@ -121,17 +122,21 @@ export const LatestWorkflowRunCard = ({ }; type Props = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; branch: string; variant?: string; }; export const LatestWorkflowsForBranchCard = ({ - entity, branch = 'master', variant, -}: Props) => ( - - - -); +}: Props) => { + const { entity } = useEntity(); + + return ( + + + + ); +}; diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx index 0deb4ae39d..18cc59439b 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.test.tsx @@ -22,6 +22,7 @@ import { ConfigApi, ConfigReader, } from '@backstage/core'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; import { render } from '@testing-library/react'; @@ -84,7 +85,9 @@ describe('', () => { configApi, )} > - + + + , diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index aa30423c3e..4425bc8364 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -23,6 +23,7 @@ import { useApi, } from '@backstage/core'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Button, Link } from '@material-ui/core'; import React, { useEffect } from 'react'; import { generatePath, Link as RouterLink } from 'react-router-dom'; @@ -33,7 +34,8 @@ import { WorkflowRunStatus } from '../WorkflowRunStatus'; const firstLine = (message: string): string => message.split('\n')[0]; export type Props = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; branch?: string; dense?: boolean; limit?: number; @@ -41,12 +43,12 @@ export type Props = { }; export const RecentWorkflowRunsCard = ({ - entity, branch, dense = false, limit = 5, variant, }: Props) => { + const { entity } = useEntity(); const config = useApi(configApiRef); const errorApi = useApi(errorApiRef); // TODO: Get github hostname from metadata annotation diff --git a/plugins/github-actions/src/components/Router.tsx b/plugins/github-actions/src/components/Router.tsx index e7430d40ed..834fcae43c 100644 --- a/plugins/github-actions/src/components/Router.tsx +++ b/plugins/github-actions/src/components/Router.tsx @@ -15,6 +15,7 @@ */ import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Routes, Route } from 'react-router'; import { rootRouteRef, buildRouteRef } from '../plugin'; import { WorkflowRunDetails } from './WorkflowRunDetails'; @@ -22,13 +23,23 @@ import { WorkflowRunsTable } from './WorkflowRunsTable'; import { GITHUB_ACTIONS_ANNOTATION } from './useProjectName'; import { MissingAnnotationEmptyState } from '@backstage/core'; -export const isPluginApplicableToEntity = (entity: Entity) => +export const isGithubActionsAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[GITHUB_ACTIONS_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => - !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isGithubActionsAvailable(entity)) { + return ( + + ); + } + return ( ) ); +}; diff --git a/plugins/github-actions/src/index.ts b/plugins/github-actions/src/index.ts index 24fe6fc90d..b4179a9d92 100644 --- a/plugins/github-actions/src/index.ts +++ b/plugins/github-actions/src/index.ts @@ -14,8 +14,19 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + githubActionsPlugin, + githubActionsPlugin as plugin, + EntityGithubActionsContent, + EntityLatestGithubActionRunCard, + EntityLatestGithubActionsForBranchCard, + EntityRecentGithubActionsRunsCard, +} from './plugin'; export * from './api'; -export { Router, isPluginApplicableToEntity } from './components/Router'; +export { + Router, + isGithubActionsAvailable, + isGithubActionsAvailable as isPluginApplicableToEntity, +} from './components/Router'; export * from './components/Cards'; export { GITHUB_ACTIONS_ANNOTATION } from './components/useProjectName'; diff --git a/plugins/github-actions/src/plugin.test.ts b/plugins/github-actions/src/plugin.test.ts index fa1075cbc8..53bc96c77a 100644 --- a/plugins/github-actions/src/plugin.test.ts +++ b/plugins/github-actions/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { githubActionsPlugin } from './plugin'; describe('github-actions', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(githubActionsPlugin).toBeDefined(); }); }); diff --git a/plugins/github-actions/src/plugin.ts b/plugins/github-actions/src/plugin.ts index 39ffc468b1..d296b4ad30 100644 --- a/plugins/github-actions/src/plugin.ts +++ b/plugins/github-actions/src/plugin.ts @@ -20,6 +20,8 @@ import { createRouteRef, createApiFactory, githubAuthApiRef, + createRoutableExtension, + createComponentExtension, } from '@backstage/core'; import { githubActionsApiRef, GithubActionsClient } from './api'; @@ -34,7 +36,7 @@ export const buildRouteRef = createRouteRef({ title: 'GitHub Actions Workflow Run', }); -export const plugin = createPlugin({ +export const githubActionsPlugin = createPlugin({ id: 'github-actions', apis: [ createApiFactory({ @@ -44,4 +46,41 @@ export const plugin = createPlugin({ new GithubActionsClient({ configApi, githubAuthApi }), }), ], + routes: { + entityContent: rootRouteRef, + }, }); + +export const EntityGithubActionsContent = githubActionsPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); + +export const EntityLatestGithubActionRunCard = githubActionsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.LatestWorkflowRunCard), + }, + }), +); + +export const EntityLatestGithubActionsForBranchCard = githubActionsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.LatestWorkflowsForBranchCard), + }, + }), +); + +export const EntityRecentGithubActionsRunsCard = githubActionsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/Cards').then(m => m.RecentWorkflowRunsCard), + }, + }), +); diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md index 5aa38c65e5..0fa9fa24a2 100644 --- a/plugins/gitops-profiles/CHANGELOG.md +++ b/plugins/gitops-profiles/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-gitops-profiles +## 0.2.4 + +### Patch Changes + +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + ## 0.2.3 ### Patch Changes diff --git a/plugins/gitops-profiles/dev/index.tsx b/plugins/gitops-profiles/dev/index.tsx index 812a5585d4..c164e4005d 100644 --- a/plugins/gitops-profiles/dev/index.tsx +++ b/plugins/gitops-profiles/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { gitopsProfilesPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(gitopsProfilesPlugin).render(); diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 447c3cbb81..c25628fa62 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-gitops-profiles", - "version": "0.2.3", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -42,8 +42,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/gitops-profiles/src/index.ts b/plugins/gitops-profiles/src/index.ts index d67bc6a864..876c7c3263 100644 --- a/plugins/gitops-profiles/src/index.ts +++ b/plugins/gitops-profiles/src/index.ts @@ -14,5 +14,11 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + gitopsProfilesPlugin, + gitopsProfilesPlugin as plugin, + GitopsProfilesClusterListPage, + GitopsProfilesClusterPage, + GitopsProfilesCreatePage, +} from './plugin'; export * from './api'; diff --git a/plugins/gitops-profiles/src/plugin.test.ts b/plugins/gitops-profiles/src/plugin.test.ts index 19bc49eba7..fa26902f2b 100644 --- a/plugins/gitops-profiles/src/plugin.test.ts +++ b/plugins/gitops-profiles/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { gitopsProfilesPlugin } from './plugin'; describe('gitops-profiles', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(gitopsProfilesPlugin).toBeDefined(); }); }); diff --git a/plugins/gitops-profiles/src/plugin.ts b/plugins/gitops-profiles/src/plugin.ts index 45820644f7..98a0067969 100644 --- a/plugins/gitops-profiles/src/plugin.ts +++ b/plugins/gitops-profiles/src/plugin.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { createPlugin, createApiFactory } from '@backstage/core'; +import { + createPlugin, + createApiFactory, + createRoutableExtension, +} from '@backstage/core'; import ProfileCatalog from './components/ProfileCatalog'; import ClusterPage from './components/ClusterPage'; import ClusterList from './components/ClusterList'; @@ -25,7 +29,7 @@ import { } from './routes'; import { gitOpsApiRef, GitOpsRestApi } from './api'; -export const plugin = createPlugin({ +export const gitopsProfilesPlugin = createPlugin({ id: 'gitops-profiles', apis: [ createApiFactory(gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')), @@ -35,4 +39,30 @@ export const plugin = createPlugin({ router.addRoute(gitOpsClusterDetailsRoute, ClusterPage); router.addRoute(gitOpsClusterCreateRoute, ProfileCatalog); }, + routes: { + listPage: gitOpsClusterListRoute, + detailsPage: gitOpsClusterDetailsRoute, + createPage: gitOpsClusterCreateRoute, + }, }); + +export const GitopsProfilesClusterListPage = gitopsProfilesPlugin.provide( + createRoutableExtension({ + component: () => import('./components/ClusterList').then(m => m.default), + mountPoint: gitOpsClusterListRoute, + }), +); + +export const GitopsProfilesClusterPage = gitopsProfilesPlugin.provide( + createRoutableExtension({ + component: () => import('./components/ClusterPage').then(m => m.default), + mountPoint: gitOpsClusterDetailsRoute, + }), +); + +export const GitopsProfilesCreatePage = gitopsProfilesPlugin.provide( + createRoutableExtension({ + component: () => import('./components/ProfileCatalog').then(m => m.default), + mountPoint: gitOpsClusterCreateRoute, + }), +); diff --git a/plugins/gitops-profiles/src/routes.ts b/plugins/gitops-profiles/src/routes.ts index f9bdefa806..28be85bd38 100644 --- a/plugins/gitops-profiles/src/routes.ts +++ b/plugins/gitops-profiles/src/routes.ts @@ -28,6 +28,7 @@ export const gitOpsClusterDetailsRoute = createRouteRef({ icon: NoIcon, path: '/gitops-cluster/:owner/:repo', title: 'GitOps Cluster details', + params: ['owner', 'repo'], }); export const gitOpsClusterCreateRoute = createRouteRef({ diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md index 88e6cc63e5..d6f4864a2f 100644 --- a/plugins/graphiql/CHANGELOG.md +++ b/plugins/graphiql/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-graphiql +## 0.2.7 + +### Patch Changes + +- 87b189d00: Finalized composability API migration, now exporting the plugin as `graphiqlPlugin`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + ## 0.2.6 ### Patch Changes diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 850b29108e..9293a07dd7 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.6", + "version": "0.2.7", "private": false, "publishConfig": { "access": "public", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -43,8 +43,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/graphql/package.json b/plugins/graphql/package.json index 99d369b1a7..ab12ee5278 100644 --- a/plugins/graphql/package.json +++ b/plugins/graphql/package.json @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", + "@backstage/backend-common": "^0.5.2", "@backstage/config": "^0.1.2", "@backstage/plugin-catalog-graphql": "^0.2.6", "@graphql-modules/core": "^0.7.17", @@ -45,7 +45,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/supertest": "^2.0.8", "eslint-plugin-graphql": "^4.0.0", "msw": "^0.20.5", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index c49857c7a5..2199c58949 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-jenkins +## 0.3.8 + +### Patch Changes + +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.3.7 ### Patch Changes diff --git a/plugins/jenkins/dev/index.tsx b/plugins/jenkins/dev/index.tsx index 812a5585d4..8beb6eabc1 100644 --- a/plugins/jenkins/dev/index.tsx +++ b/plugins/jenkins/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { jenkinsPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(jenkinsPlugin).render(); diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index 7456d4be53..5d59d2e5ab 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.3.7", + "version": "0.3.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,8 +47,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/jenkins/src/components/Router.tsx b/plugins/jenkins/src/components/Router.tsx index 91bcde2a8b..85f06bb0e1 100644 --- a/plugins/jenkins/src/components/Router.tsx +++ b/plugins/jenkins/src/components/Router.tsx @@ -15,6 +15,7 @@ */ import React from 'react'; import { Route, Routes } from 'react-router'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { buildRouteRef, rootRouteRef } from '../plugin'; import { DetailedViewPage } from './BuildWithStepsPage/'; import { JENKINS_ANNOTATION } from '../constants'; @@ -22,13 +23,22 @@ import { Entity } from '@backstage/catalog-model'; import { MissingAnnotationEmptyState } from '@backstage/core'; import { CITable } from './BuildsPage/lib/CITable'; -export const isPluginApplicableToEntity = (entity: Entity) => +export const isJenkinsAvailable = (entity: Entity) => Boolean(entity.metadata.annotations?.[JENKINS_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => { - return !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isJenkinsAvailable(entity)) { + return ; + } + + return ( } /> } /> diff --git a/plugins/jenkins/src/components/useBuilds.ts b/plugins/jenkins/src/components/useBuilds.ts index db7c7ddd55..a1ff3a1eef 100644 --- a/plugins/jenkins/src/components/useBuilds.ts +++ b/plugins/jenkins/src/components/useBuilds.ts @@ -36,17 +36,17 @@ export function useBuilds(owner: string, repo: string, branch?: string) { const { loading, value: builds, retry } = useAsyncRetry(async () => { try { - let builds; + let build; if (branch) { - builds = await api.getLastBuild(`${owner}/${repo}/${branch}`); + build = await api.getLastBuild(`${owner}/${repo}/${branch}`); } else { - builds = await api.getFolder(`${owner}/${repo}`); + build = await api.getFolder(`${owner}/${repo}`); } - const size = Array.isArray(builds) ? builds?.[0].build_num! : 1; + const size = Array.isArray(build) ? build?.[0].build_num! : 1; setTotal(size); - return builds || []; + return build || []; } catch (e) { errorApi.post(e); throw e; diff --git a/plugins/jenkins/src/index.ts b/plugins/jenkins/src/index.ts index 30fa0c70a5..e434e959e8 100644 --- a/plugins/jenkins/src/index.ts +++ b/plugins/jenkins/src/index.ts @@ -14,8 +14,17 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + jenkinsPlugin, + jenkinsPlugin as plugin, + EntityJenkinsContent, + EntityLatestJenkinsRunCard, +} from './plugin'; export { LatestRunCard } from './components/Cards'; -export { Router, isPluginApplicableToEntity } from './components/Router'; +export { + Router, + isJenkinsAvailable, + isJenkinsAvailable as isPluginApplicableToEntity, +} from './components/Router'; export { JENKINS_ANNOTATION } from './constants'; export * from './api'; diff --git a/plugins/jenkins/src/plugin.test.ts b/plugins/jenkins/src/plugin.test.ts index 8c3f057ecb..0498b80b90 100644 --- a/plugins/jenkins/src/plugin.test.ts +++ b/plugins/jenkins/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { jenkinsPlugin } from './plugin'; describe('jenkins', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(jenkinsPlugin).toBeDefined(); }); }); diff --git a/plugins/jenkins/src/plugin.ts b/plugins/jenkins/src/plugin.ts index 23c54743a3..85001d9bbc 100644 --- a/plugins/jenkins/src/plugin.ts +++ b/plugins/jenkins/src/plugin.ts @@ -19,6 +19,8 @@ import { createRouteRef, createApiFactory, discoveryApiRef, + createRoutableExtension, + createComponentExtension, } from '@backstage/core'; import { jenkinsApiRef, JenkinsApi } from './api'; @@ -32,7 +34,7 @@ export const buildRouteRef = createRouteRef({ title: 'Jenkins run', }); -export const plugin = createPlugin({ +export const jenkinsPlugin = createPlugin({ id: 'jenkins', apis: [ createApiFactory({ @@ -41,4 +43,21 @@ export const plugin = createPlugin({ factory: ({ discoveryApi }) => new JenkinsApi({ discoveryApi }), }), ], + routes: { + entityContent: rootRouteRef, + }, }); + +export const EntityJenkinsContent = jenkinsPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); +export const EntityLatestJenkinsRunCard = jenkinsPlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./components/Cards').then(m => m.LatestRunCard), + }, + }), +); diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 311464b47d..1eaa1d8316 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -42,7 +42,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/jest-when": "^2.7.2", "@types/lodash": "^4.14.151", "jest-when": "^3.1.0", diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 92f49a1d35..2895288b56 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-kafka +## 0.2.1 + +### Patch Changes + +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.0 ### Minor Changes diff --git a/plugins/kafka/dev/index.tsx b/plugins/kafka/dev/index.tsx index 264d6f801f..5506d47026 100644 --- a/plugins/kafka/dev/index.tsx +++ b/plugins/kafka/dev/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { kafkaPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(kafkaPlugin).render(); diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 546d20bc67..5d6454ed60 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka", - "version": "0.2.0", + "version": "0.2.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,8 +33,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/kafka/src/Router.tsx b/plugins/kafka/src/Router.tsx index b445f8f408..13503fefe8 100644 --- a/plugins/kafka/src/Router.tsx +++ b/plugins/kafka/src/Router.tsx @@ -17,7 +17,7 @@ import { Entity } from '@backstage/catalog-model'; import React from 'react'; import { Route, Routes } from 'react-router'; - +import { useEntity } from '@backstage/plugin-catalog-react'; import { rootCatalogKafkaRouteRef } from './plugin'; import { KAFKA_CONSUMER_GROUP_ANNOTATION } from './constants'; import { KafkaTopicsForConsumer } from './components/ConsumerGroupOffsets/ConsumerGroupOffsets'; @@ -26,10 +26,23 @@ import { MissingAnnotationEmptyState } from '@backstage/core'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[KAFKA_CONSUMER_GROUP_ANNOTATION]); -export const Router = ({ entity }: { entity: Entity }) => { - return !isPluginApplicableToEntity(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isPluginApplicableToEntity(entity)) { + return ( + + ); + } + + return ( { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(kafkaPlugin).toBeDefined(); }); }); diff --git a/plugins/kafka/src/plugin.ts b/plugins/kafka/src/plugin.ts index 878a7f56e9..832099a30c 100644 --- a/plugins/kafka/src/plugin.ts +++ b/plugins/kafka/src/plugin.ts @@ -16,6 +16,7 @@ import { createApiFactory, createPlugin, + createRoutableExtension, createRouteRef, discoveryApiRef, } from '@backstage/core'; @@ -27,7 +28,7 @@ export const rootCatalogKafkaRouteRef = createRouteRef({ title: 'Kafka', }); -export const plugin = createPlugin({ +export const kafkaPlugin = createPlugin({ id: 'kafka', apis: [ createApiFactory({ @@ -36,4 +37,14 @@ export const plugin = createPlugin({ factory: ({ discoveryApi }) => new KafkaBackendClient({ discoveryApi }), }), ], + routes: { + entityContent: rootCatalogKafkaRouteRef, + }, }); + +export const EntityKafkaContent = kafkaPlugin.provide( + createRoutableExtension({ + component: () => import('./Router').then(m => m.Router), + mountPoint: rootCatalogKafkaRouteRef, + }), +); diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index e69a82b738..3823ee1899 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -32,8 +32,8 @@ }, "dependencies": { "@aws-sdk/credential-provider-node": "^3.3.0", - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", "@kubernetes/client-node": "^0.13.2", "@types/express": "^4.17.6", @@ -51,7 +51,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/aws4": "^1.5.1", "supertest": "^4.0.2" }, diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts index 2dd1e32b96..811a3c8acb 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts @@ -85,18 +85,18 @@ export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServic ] || `backstage.io/kubernetes-id=${requestBody.entity.metadata.name}`; return Promise.all( - clusterDetailsDecoratedForAuth.map(clusterDetails => { + clusterDetailsDecoratedForAuth.map(clusterDetailsItem => { return fetcher .fetchObjectsForService({ serviceId, - clusterDetails, + clusterDetails: clusterDetailsItem, objectTypesToFetch, labelSelector, } as ObjectFetchParams) .then(result => { return { cluster: { - name: clusterDetails.name, + name: clusterDetailsItem.name, }, resources: result.responses, errors: result.errors, diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index f16728e1fe..311bd5edaa 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-kubernetes +## 0.3.8 + +### Patch Changes + +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.3.7 ### Patch Changes diff --git a/plugins/kubernetes/dev/index.tsx b/plugins/kubernetes/dev/index.tsx index f352fb9c34..de93d21348 100644 --- a/plugins/kubernetes/dev/index.tsx +++ b/plugins/kubernetes/dev/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src'; +import { kubernetesPlugin } from '../src'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(kubernetesPlugin).render(); diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 1d985c0d10..9013fa9395 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.3.7", + "version": "0.3.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,12 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", + "@backstage/catalog-model": "^0.7.1", + "@backstage/plugin-catalog-react": "^0.0.2", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.5.0", + "@backstage/core": "^0.6.0", "@backstage/plugin-kubernetes-backend": "^0.2.6", - "@backstage/theme": "^0.2.2", + "@backstage/theme": "^0.2.3", "@kubernetes/client-node": "^0.13.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,8 +48,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/kubernetes/src/Router.tsx b/plugins/kubernetes/src/Router.tsx index 20a4c04285..36851b3c46 100644 --- a/plugins/kubernetes/src/Router.tsx +++ b/plugins/kubernetes/src/Router.tsx @@ -16,15 +16,22 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Route, Routes } from 'react-router-dom'; - import { rootCatalogKubernetesRouteRef } from './plugin'; import { KubernetesContent } from './components/KubernetesContent'; import { MissingAnnotationEmptyState } from '@backstage/core'; const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id'; -export const Router = ({ entity }: { entity: Entity }) => { +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const Router = (_props: Props) => { + const { entity } = useEntity(); + const kubernetesAnnotationValue = entity.metadata.annotations?.[KUBERNETES_ANNOTATION]; diff --git a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx index 6bacad9b16..8c1365769f 100644 --- a/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx +++ b/plugins/kubernetes/src/components/DeploymentsAccordions/DeploymentDrawer.tsx @@ -32,8 +32,8 @@ export const DeploymentDrawer = ({ object={deployment} expanded={expanded} kind="Deployment" - renderObject={(deployment: V1Deployment) => { - const conditions = (deployment.status?.conditions ?? []) + renderObject={(deploymentObj: V1Deployment) => { + const conditions = (deploymentObj.status?.conditions ?? []) .map(renderCondition) .reduce((accum, next) => { accum[next[0]] = next[1]; @@ -41,10 +41,10 @@ export const DeploymentDrawer = ({ }, {} as { [key: string]: React.ReactNode }); return { - strategy: deployment.spec?.strategy ?? '???', - minReadySeconds: deployment.spec?.minReadySeconds ?? '???', + strategy: deploymentObj.spec?.strategy ?? '???', + minReadySeconds: deploymentObj.spec?.minReadySeconds ?? '???', progressDeadlineSeconds: - deployment.spec?.progressDeadlineSeconds ?? '???', + deploymentObj.spec?.progressDeadlineSeconds ?? '???', ...conditions, }; }} diff --git a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx index 00ab2cad84..26dcdfd249 100644 --- a/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx +++ b/plugins/kubernetes/src/components/HorizontalPodAutoscalers/HorizontalPodAutoscalerDrawer.tsx @@ -32,16 +32,16 @@ export const HorizontalPodAutoscalerDrawer = ({ kind="HorizontalPodAutoscaler" object={hpa} expanded={expanded} - renderObject={(hpa: V1HorizontalPodAutoscaler) => { + renderObject={(hpaObject: V1HorizontalPodAutoscaler) => { return { targetCPUUtilizationPercentage: - hpa.spec?.targetCPUUtilizationPercentage, + hpaObject.spec?.targetCPUUtilizationPercentage, currentCPUUtilizationPercentage: - hpa.status?.currentCPUUtilizationPercentage, - minReplicas: hpa.spec?.minReplicas, - maxReplicas: hpa.spec?.maxReplicas, - currentReplicas: hpa.status?.currentReplicas, - desiredReplicas: hpa.status?.desiredReplicas, + hpaObject.status?.currentCPUUtilizationPercentage, + minReplicas: hpaObject.spec?.minReplicas, + maxReplicas: hpaObject.spec?.maxReplicas, + currentReplicas: hpaObject.status?.currentReplicas, + desiredReplicas: hpaObject.status?.desiredReplicas, }; }} > diff --git a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx index 9e63f22fda..d619700ac2 100644 --- a/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx +++ b/plugins/kubernetes/src/components/IngressesAccordions/IngressDrawer.tsx @@ -31,8 +31,8 @@ export const IngressDrawer = ({ object={ingress} expanded={expanded} kind="Ingress" - renderObject={(ingress: ExtensionsV1beta1Ingress) => { - return ingress.spec || {}; + renderObject={(ingressObject: ExtensionsV1beta1Ingress) => { + return ingressObject.spec || {}; }} > { - const phase = pod.status?.phase ?? 'unknown'; + renderObject={(podObject: V1Pod) => { + const phase = podObject.status?.phase ?? 'unknown'; const ports = - pod.spec?.containers?.map(c => { + podObject.spec?.containers?.map(c => { return { [c.name]: c.ports, }; }) ?? 'N/A'; - const conditions = (pod.status?.conditions ?? []) + const conditions = (podObject.status?.conditions ?? []) .map(renderCondition) .reduce((accum, next) => { accum[next[0]] = next[1]; @@ -55,11 +55,11 @@ export const PodDrawer = ({ }, {} as { [key: string]: React.ReactNode }); return { - images: imageChips(pod), + images: imageChips(podObject), phase: phase, - 'Containers Ready': containersReady(pod), - 'Total Restarts': totalRestarts(pod), - 'Container Statuses': containerStatuses(pod), + 'Containers Ready': containersReady(podObject), + 'Total Restarts': totalRestarts(podObject), + 'Container Statuses': containerStatuses(podObject), ...conditions, 'Exposed ports': ports, }; diff --git a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx index d48f17813a..d0962c6314 100644 --- a/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx +++ b/plugins/kubernetes/src/components/ServicesAccordions/ServiceDrawer.tsx @@ -31,8 +31,8 @@ export const ServiceDrawer = ({ object={service} expanded={expanded} kind="Service" - renderObject={(service: V1Service) => { - return service.spec || {}; + renderObject={(serviceObject: V1Service) => { + return serviceObject.spec || {}; }} > { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(kubernetesPlugin).toBeDefined(); }); }); diff --git a/plugins/kubernetes/src/plugin.ts b/plugins/kubernetes/src/plugin.ts index cff9e1468b..af29df3dcc 100644 --- a/plugins/kubernetes/src/plugin.ts +++ b/plugins/kubernetes/src/plugin.ts @@ -19,6 +19,7 @@ import { createRouteRef, discoveryApiRef, googleAuthApiRef, + createRoutableExtension, } from '@backstage/core'; import { KubernetesBackendClient } from './api/KubernetesBackendClient'; import { kubernetesApiRef } from './api/types'; @@ -30,7 +31,7 @@ export const rootCatalogKubernetesRouteRef = createRouteRef({ title: 'Kubernetes', }); -export const plugin = createPlugin({ +export const kubernetesPlugin = createPlugin({ id: 'kubernetes', apis: [ createApiFactory({ @@ -47,4 +48,14 @@ export const plugin = createPlugin({ }, }), ], + routes: { + entityContent: rootCatalogKubernetesRouteRef, + }, }); + +export const EntityKubernetesContent = kubernetesPlugin.provide( + createRoutableExtension({ + component: () => import('./Router').then(m => m.Router), + mountPoint: rootCatalogKubernetesRouteRef, + }), +); diff --git a/plugins/kubernetes/src/utils/pod.tsx b/plugins/kubernetes/src/utils/pod.tsx index 4e317cd606..59e18e6ecb 100644 --- a/plugins/kubernetes/src/utils/pod.tsx +++ b/plugins/kubernetes/src/utils/pod.tsx @@ -36,9 +36,9 @@ export const imageChips = (pod: V1Pod): ReactNode => { export const containersReady = (pod: V1Pod): string => { const containerStatuses = pod.status?.containerStatuses ?? []; - const containersReady = containerStatuses.filter(cs => cs.ready).length; + const containersReadyItem = containerStatuses.filter(cs => cs.ready).length; - return `${containersReady}/${containerStatuses.length}`; + return `${containersReadyItem}/${containerStatuses.length}`; }; export const totalRestarts = (pod: V1Pod): number => { @@ -47,8 +47,8 @@ export const totalRestarts = (pod: V1Pod): number => { }; export const containerStatuses = (pod: V1Pod): ReactNode => { - const containerStatuses = pod.status?.containerStatuses ?? []; - const errors = containerStatuses.reduce((accum, next) => { + const containerStatusesItem = pod.status?.containerStatuses ?? []; + const errors = containerStatusesItem.reduce((accum, next) => { if (next.state === undefined) { return accum; } diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index 301a9b4fed..8fec8887c3 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-lighthouse +## 0.2.9 + +### Patch Changes + +- a5628df40: Migrate to new composability API, exporting the plugin instance as `lighthousePlugin`, the top-level page as `LighthousePage`, the entity card as `EntityLastLighthouseAuditCard`, and the entity content as `EntityLighthouseContent`. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.8 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 5b45bfa48b..07e1ad8475 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.8", + "version": "0.2.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -46,8 +46,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/lighthouse/src/Router.tsx b/plugins/lighthouse/src/Router.tsx index 0634a0aaa0..643d30cdc2 100644 --- a/plugins/lighthouse/src/Router.tsx +++ b/plugins/lighthouse/src/Router.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; +import { useEntity } from '@backstage/plugin-catalog-react'; import AuditList from './components/AuditList'; import AuditView, { AuditViewContent } from './components/AuditView'; import CreateAudit, { CreateAuditContent } from './components/CreateAudit'; @@ -35,15 +36,27 @@ export const Router = () => ( ); -export const EmbeddedRouter = ({ entity }: { entity: Entity }) => - !isLighthouseAvailable(entity) ? ( - - ) : ( +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const EmbeddedRouter = (_props: Props) => { + const { entity } = useEntity(); + + if (!isLighthouseAvailable(entity)) { + return ( + + ); + } + + return ( } /> } /> } /> ); +}; diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md index d7806c4081..6eae73bc86 100644 --- a/plugins/newrelic/CHANGELOG.md +++ b/plugins/newrelic/CHANGELOG.md @@ -1,5 +1,20 @@ # @backstage/plugin-newrelic +## 0.2.4 + +### Patch Changes + +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + ## 0.2.3 ### Patch Changes diff --git a/plugins/newrelic/dev/index.tsx b/plugins/newrelic/dev/index.tsx index 812a5585d4..9ca421f94a 100644 --- a/plugins/newrelic/dev/index.tsx +++ b/plugins/newrelic/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { newRelicPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(newRelicPlugin).render(); diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 2f43c9cbeb..ccd43c42c8 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-newrelic", - "version": "0.2.3", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,8 +31,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,8 +41,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/newrelic/src/index.ts b/plugins/newrelic/src/index.ts index 3a0a0fe2d3..aa4e990d6c 100644 --- a/plugins/newrelic/src/index.ts +++ b/plugins/newrelic/src/index.ts @@ -14,4 +14,8 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + newRelicPlugin, + newRelicPlugin as plugin, + NewRelicPage, +} from './plugin'; diff --git a/plugins/newrelic/src/plugin.test.ts b/plugins/newrelic/src/plugin.test.ts index 17429a95e0..f2e8fde924 100644 --- a/plugins/newrelic/src/plugin.test.ts +++ b/plugins/newrelic/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { newRelicPlugin } from './plugin'; describe('newrelic', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(newRelicPlugin).toBeDefined(); }); }); diff --git a/plugins/newrelic/src/plugin.ts b/plugins/newrelic/src/plugin.ts index 5f5ba88617..d89aa813e9 100644 --- a/plugins/newrelic/src/plugin.ts +++ b/plugins/newrelic/src/plugin.ts @@ -19,6 +19,7 @@ import { createPlugin, createRouteRef, discoveryApiRef, + createRoutableExtension, } from '@backstage/core'; import { NewRelicClient, newRelicApiRef } from './api'; import NewRelicComponent from './components/NewRelicComponent'; @@ -28,7 +29,7 @@ export const rootRouteRef = createRouteRef({ title: 'newrelic', }); -export const plugin = createPlugin({ +export const newRelicPlugin = createPlugin({ id: 'newrelic', apis: [ createApiFactory({ @@ -40,4 +41,15 @@ export const plugin = createPlugin({ register({ router }) { router.addRoute(rootRouteRef, NewRelicComponent); }, + routes: { + root: rootRouteRef, + }, }); + +export const NewRelicPage = newRelicPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/NewRelicComponent').then(m => m.default), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 17dd37266f..6101685e1d 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,36 @@ # @backstage/plugin-org +## 0.3.5 + +### Patch Changes + +- 7fc89bae2: Display owner and system as entity page links in the tables of the `api-docs` + plugin. + + Move `isOwnerOf` and `getEntityRelations` from `@backstage/plugin-catalog` to + `@backstage/plugin-catalog-react` and export it from there to use it by other + plugins. + +- 0269f4fd9: Migrate to new composability API, exporting the plugin instance as `orgPlugin`, and the entity cards as `EntityGroupProfileCard`, `EntityMembersListCard`, `EntityOwnershipCard`, and `EntityUserProfileCard`. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.3.4 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index e376dea5bc..0491554f77 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.3.4", + "version": "0.3.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,10 +20,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -33,8 +33,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index f5a1acd5e6..2f0f1e2e16 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -21,7 +21,7 @@ import { RELATION_PARENT_OF, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog-react'; +import { useEntity, entityRouteParams } from '@backstage/plugin-catalog-react'; import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; import EmailIcon from '@material-ui/icons/Email'; @@ -33,26 +33,29 @@ import { generatePath, Link as RouterLink } from 'react-router-dom'; const GroupLink = ({ groupName, index = 0, - entity, }: { groupName: string; index?: number; - entity: Entity; -}) => ( - <> - {index >= 1 ? ', ' : ''} - - [{groupName}] - - -); + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}) => { + const { entity } = useEntity(); + return ( + <> + {index >= 1 ? ', ' : ''} + + [{groupName}] + + + ); +}; const CardTitle = ({ title }: { title: string }) => ( @@ -61,24 +64,25 @@ const CardTitle = ({ title }: { title: string }) => ( ); export const GroupProfileCard = ({ - entity: group, variant, }: { - entity: GroupEntity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: GroupEntity; variant: string; }) => { + const group = useEntity().entity as GroupEntity; const { metadata: { name, description }, spec: { profile }, } = group; const parent = group?.relations ?.filter(r => r.type === RELATION_CHILD_OF) - ?.map(group => group.target.name) + ?.map(groupItem => groupItem.target.name) .toString(); const childrens = group?.relations ?.filter(r => r.type === RELATION_PARENT_OF) - ?.map(group => group.target.name); + ?.map(groupItem => groupItem.target.name); const displayName = profile?.displayName ?? name; diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index 6815fd0e55..28c55b376b 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -16,7 +16,11 @@ import { Entity, GroupEntity } from '@backstage/catalog-model'; import { ApiProvider, ApiRegistry } from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { + CatalogApi, + catalogApiRef, + EntityProvider, +} from '@backstage/plugin-catalog-react'; import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import React from 'react'; import { MembersListCard } from './MembersListCard'; @@ -78,7 +82,10 @@ describe('MemberTab Test', () => { const rendered = await renderWithEffects( wrapInTestApp( - + + + + , , ), ); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index f644e7db7e..7f8ede2eb7 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -21,6 +21,7 @@ import { } from '@backstage/catalog-model'; import { Avatar, InfoCard, Progress, useApi } from '@backstage/core'; import { + useEntity, catalogApiRef, entityRouteParams, } from '@backstage/plugin-catalog-react'; @@ -105,11 +106,11 @@ const MemberComponent = ({ ); }; -export const MembersListCard = ({ - entity: groupEntity, -}: { - entity: GroupEntity; +export const MembersListCard = (_props: { + /** @deprecated The entity is now grabbed from context instead */ + entity?: GroupEntity; }) => { + const groupEntity = useEntity().entity as GroupEntity; const { metadata: { name: groupName }, spec: { profile }, diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 60f06bd53a..80e80bddcc 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -16,7 +16,11 @@ import { Entity } from '@backstage/catalog-model'; import { InfoCard, Progress, useApi } from '@backstage/core'; -import { catalogApiRef, isOwnerOf } from '@backstage/plugin-catalog-react'; +import { + catalogApiRef, + isOwnerOf, + useEntity, +} from '@backstage/plugin-catalog-react'; import { pageTheme } from '@backstage/theme'; import { Box, @@ -113,12 +117,13 @@ const EntityCountTile = ({ }; export const OwnershipCard = ({ - entity, variant, }: { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; variant: string; }) => { + const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); const { loading, diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx index 77708e6f27..7c7d26c228 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -15,6 +15,7 @@ */ import { UserEntity } from '@backstage/catalog-model'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import React from 'react'; import { UserProfileCard } from './UserProfileCard'; @@ -48,7 +49,11 @@ describe('UserSummary Test', () => { it('Display Profile Card', async () => { const rendered = await renderWithEffects( - wrapInTestApp(), + wrapInTestApp( + + + , + ), ); expect(rendered.getByText('calum-leavy@example.com')).toBeInTheDocument(); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index bdd0cd15f6..1cf7c968b3 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -19,7 +19,7 @@ import { UserEntity, } from '@backstage/catalog-model'; import { Avatar, InfoCard } from '@backstage/core'; -import { entityRouteParams } from '@backstage/plugin-catalog-react'; +import { useEntity, entityRouteParams } from '@backstage/plugin-catalog-react'; import { Box, Grid, Link, Tooltip, Typography } from '@material-ui/core'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; @@ -60,12 +60,13 @@ const CardTitle = ({ title }: { title?: string }) => ) : null; export const UserProfileCard = ({ - entity: user, variant, }: { - entity: UserEntity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: UserEntity; variant: string; }) => { + const user = useEntity().entity as UserEntity; const { metadata: { name: metaName }, spec: { profile }, diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index d27becfa47..eb00ce36be 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,25 @@ # @backstage/plugin-pagerduty +## 0.2.7 + +### Patch Changes + +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.6 ### Patch Changes diff --git a/plugins/pagerduty/dev/index.tsx b/plugins/pagerduty/dev/index.tsx index 264d6f801f..ad46c0ca9e 100644 --- a/plugins/pagerduty/dev/index.tsx +++ b/plugins/pagerduty/dev/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { pagerDutyPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(pagerDutyPlugin).render(); diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index 0e87bdc2d7..afd41dcc46 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-pagerduty", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -44,8 +45,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx index faa480f6e2..6fd036330a 100644 --- a/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx +++ b/plugins/pagerduty/src/components/Escalation/EscalationPolicy.tsx @@ -32,11 +32,11 @@ export const EscalationPolicy = ({ policyId }: Props) => { const { value: users, loading, error } = useAsync(async () => { const oncalls = await api.getOnCallByPolicyId(policyId); - const users = oncalls + const usersItem = oncalls .sort((a, b) => a.escalation_level - b.escalation_level) .map(oncall => oncall.user); - return users; + return usersItem; }); if (error) { diff --git a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx index a6615aee82..a719a27f86 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard.test.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard.test.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { render, waitFor, fireEvent, act } from '@testing-library/react'; import { PagerDutyCard } from './PagerDutyCard'; import { Entity } from '@backstage/catalog-model'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; import { wrapInTestApp } from '@backstage/test-utils'; import { alertApiRef, @@ -80,7 +81,9 @@ describe('PageDutyCard', () => { const { getByText, queryByTestId } = render( wrapInTestApp( - + + + , ), ); @@ -99,7 +102,9 @@ describe('PageDutyCard', () => { const { getByText, queryByTestId } = render( wrapInTestApp( - + + + , ), ); @@ -114,7 +119,9 @@ describe('PageDutyCard', () => { const { getByText, queryByTestId } = render( wrapInTestApp( - + + + , ), ); @@ -134,7 +141,9 @@ describe('PageDutyCard', () => { const { getByText, queryByTestId, getByTestId, getByRole } = render( wrapInTestApp( - + + + , ), ); diff --git a/plugins/pagerduty/src/components/PagerDutyCard.tsx b/plugins/pagerduty/src/components/PagerDutyCard.tsx index 5fb2cd7f4a..e03fb0fdbe 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard.tsx @@ -16,6 +16,7 @@ import React, { useState, useCallback } from 'react'; import { useApi, Progress, HeaderIconLinkRow } from '@backstage/core'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Button, makeStyles, @@ -56,11 +57,13 @@ export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]); type Props = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; }; -export const PagerDutyCard = ({ entity }: Props) => { +export const PagerDutyCard = (_props: Props) => { const classes = useStyles(); + const { entity } = useEntity(); const api = useApi(pagerDutyApiRef); const [showDialog, setShowDialog] = useState(false); const [refreshIncidents, setRefreshIncidents] = useState(false); diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index 4fa88a1b9a..c390ed1f82 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -52,11 +52,11 @@ export const TriggerDialog = ({ const [description, setDescription] = useState(''); const [{ value, loading, error }, handleTriggerAlarm] = useAsyncFn( - async (description: string) => + async (descriptions: string) => await api.triggerAlarm({ integrationKey, source: window.location.toString(), - description, + description: descriptions, userName, }), ); diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts index 4ecd4edcc6..2dfedab164 100644 --- a/plugins/pagerduty/src/index.ts +++ b/plugins/pagerduty/src/index.ts @@ -13,9 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { plugin } from './plugin'; +export { + pagerDutyPlugin, + pagerDutyPlugin as plugin, + EntityPagerDutyCard, +} from './plugin'; export { isPluginApplicableToEntity, + isPluginApplicableToEntity as isPagerDutyAvailable, PagerDutyCard, } from './components/PagerDutyCard'; export { diff --git a/plugins/pagerduty/src/plugin.test.ts b/plugins/pagerduty/src/plugin.test.ts index 8d4545ac12..c1175ab384 100644 --- a/plugins/pagerduty/src/plugin.test.ts +++ b/plugins/pagerduty/src/plugin.test.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { plugin } from './plugin'; +import { pagerDutyPlugin } from './plugin'; describe('pagerduty', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(pagerDutyPlugin).toBeDefined(); }); }); diff --git a/plugins/pagerduty/src/plugin.ts b/plugins/pagerduty/src/plugin.ts index 34796f491e..fbe827b90d 100644 --- a/plugins/pagerduty/src/plugin.ts +++ b/plugins/pagerduty/src/plugin.ts @@ -19,6 +19,7 @@ import { createRouteRef, discoveryApiRef, configApiRef, + createComponentExtension, } from '@backstage/core'; import { pagerDutyApiRef, PagerDutyClient } from './api'; @@ -27,7 +28,7 @@ export const rootRouteRef = createRouteRef({ title: 'pagerduty', }); -export const plugin = createPlugin({ +export const pagerDutyPlugin = createPlugin({ id: 'pagerduty', apis: [ createApiFactory({ @@ -38,3 +39,12 @@ export const plugin = createPlugin({ }), ], }); + +export const EntityPagerDutyCard = pagerDutyPlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./components/PagerDutyCard').then(m => m.PagerDutyCard), + }, + }), +); diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json index ae828cb2ec..51ccf98f8b 100644 --- a/plugins/proxy-backend/package.json +++ b/plugins/proxy-backend/package.json @@ -28,7 +28,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", + "@backstage/backend-common": "^0.5.2", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "express": "^4.17.1", @@ -42,7 +42,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/http-proxy-middleware": "^0.19.3", "@types/supertest": "^2.0.8", "@types/uuid": "^8.0.0", diff --git a/plugins/register-component/CHANGELOG.md b/plugins/register-component/CHANGELOG.md index abc2fdbd58..901c556052 100644 --- a/plugins/register-component/CHANGELOG.md +++ b/plugins/register-component/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-register-component +## 0.2.8 + +### Patch Changes + +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.7 ### Patch Changes diff --git a/plugins/register-component/dev/index.tsx b/plugins/register-component/dev/index.tsx index 812a5585d4..3304d0874f 100644 --- a/plugins/register-component/dev/index.tsx +++ b/plugins/register-component/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { registerComponentPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(registerComponentPlugin).render(); diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 39c431b3cd..9e0f045cd4 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-register-component", - "version": "0.2.7", + "version": "0.2.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -45,8 +45,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/register-component/src/index.ts b/plugins/register-component/src/index.ts index ff7857cacd..56bcd06fde 100644 --- a/plugins/register-component/src/index.ts +++ b/plugins/register-component/src/index.ts @@ -14,5 +14,9 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + registerComponentPlugin, + registerComponentPlugin as plugin, + RegisterComponentPage, +} from './plugin'; export { Router } from './components/Router'; diff --git a/plugins/register-component/src/plugin.test.ts b/plugins/register-component/src/plugin.test.ts index 1e61060202..2b6ad48bcf 100644 --- a/plugins/register-component/src/plugin.test.ts +++ b/plugins/register-component/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { registerComponentPlugin } from './plugin'; describe('register-component', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(registerComponentPlugin).toBeDefined(); }); }); diff --git a/plugins/register-component/src/plugin.ts b/plugins/register-component/src/plugin.ts index 17f99917c8..d3727158e5 100644 --- a/plugins/register-component/src/plugin.ts +++ b/plugins/register-component/src/plugin.ts @@ -14,8 +14,29 @@ * limitations under the License. */ -import { createPlugin } from '@backstage/core'; +import { + createPlugin, + createRoutableExtension, + createRouteRef, +} from '@backstage/core'; -export const plugin = createPlugin({ - id: 'register-component', +const rootRouteRef = createRouteRef({ + title: 'Register Component', }); + +export const registerComponentPlugin = createPlugin({ + id: 'register-component', + routes: { + root: rootRouteRef, + }, +}); + +export const RegisterComponentPage = registerComponentPlugin.provide( + createRoutableExtension({ + component: () => + import('./components/RegisterComponentPage').then( + m => m.RegisterComponentPage, + ), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json index 522f5a3b4c..ff7a4f06f5 100644 --- a/plugins/rollbar-backend/package.json +++ b/plugins/rollbar-backend/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", + "@backstage/backend-common": "^0.5.2", "@backstage/config": "^0.1.2", "@types/express": "^4.17.6", "axios": "^0.21.1", @@ -47,7 +47,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@types/supertest": "^2.0.8", "supertest": "^4.0.2" }, diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index d317640948..e49c843116 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-rollbar +## 0.2.9 + +### Patch Changes + +- 9d6ef14bc: Migrated to new composability API, exporting the plugin instance as `rollbarPlugin`, the entity page content as `EntityRollbarContent`, and entity conditional as `isRollbarAvailable`. Updated the `EntityPage` for the `example-app` to include a composite `ErrorsSwitcher` component that works with both `Sentry` & `Rollbar`. Also removed the unused and undocumented `RollbarHome` related components. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.8 ### Patch Changes diff --git a/plugins/rollbar/README.md b/plugins/rollbar/README.md index cc862051f7..6337d45ffe 100644 --- a/plugins/rollbar/README.md +++ b/plugins/rollbar/README.md @@ -19,17 +19,7 @@ yarn add @backstage/plugin-rollbar export { plugin as Rollbar } from '@backstage/plugin-rollbar'; ``` -4. Add plugin API to your Backstage instance: - -```ts -// packages/app/src/api.ts -import { RollbarClient, rollbarApiRef } from '@backstage/plugin-rollbar'; - -// ... -builder.add(rollbarApiRef, new RollbarClient({ discoveryApi })); -``` - -5. Add to the app `EntityPage` component: +4. Add to the app `EntityPage` component: ```ts // packages/app/src/components/catalog/EntityPage.tsx @@ -48,7 +38,7 @@ const ServiceEntityPage = ({ entity }: { entity: Entity }) => ( ); ``` -6. Setup the `app.config.yaml` and account token environment variable +5. Setup the `app.config.yaml` and account token environment variable ```yaml # app.config.yaml @@ -59,7 +49,7 @@ rollbar: $env: ROLLBAR_ACCOUNT_TOKEN ``` -7. Annotate entities with the rollbar project slug +6. Annotate entities with the rollbar project slug ```yaml # pump-station-catalog-component.yaml @@ -71,7 +61,7 @@ metadata: rollbar.com/project-slug: project-name ``` -8. Run app with `yarn start` and navigate to `/rollbar` or a catalog entity +7. Run app with `yarn start` and navigate to `/rollbar` or a catalog entity ## Features diff --git a/plugins/rollbar/dev/index.tsx b/plugins/rollbar/dev/index.tsx index 812a5585d4..6cac0d9a0c 100644 --- a/plugins/rollbar/dev/index.tsx +++ b/plugins/rollbar/dev/index.tsx @@ -15,6 +15,6 @@ */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { rollbarPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(rollbarPlugin).render(); diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 7ba72dad0d..44d8583bfa 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.2.8", + "version": "0.2.9", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,8 +47,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/rollbar/src/components/EntityPageRollbar/EntityPageRollbar.tsx b/plugins/rollbar/src/components/EntityPageRollbar/EntityPageRollbar.tsx index 888b9aabe0..07a39bd147 100644 --- a/plugins/rollbar/src/components/EntityPageRollbar/EntityPageRollbar.tsx +++ b/plugins/rollbar/src/components/EntityPageRollbar/EntityPageRollbar.tsx @@ -14,14 +14,18 @@ * limitations under the License. */ -import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import React from 'react'; import { RollbarProject } from '../RollbarProject/RollbarProject'; type Props = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; }; -export const EntityPageRollbar = ({ entity }: Props) => { +export const EntityPageRollbar = (_props: Props) => { + const { entity } = useEntity(); + return ; }; diff --git a/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx b/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx deleted file mode 100644 index 5bd885c488..0000000000 --- a/plugins/rollbar/src/components/RollbarHome/RollbarHome.test.tsx +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import * as React from 'react'; -import { - ApiProvider, - ApiRegistry, - ConfigApi, - configApiRef, -} from '@backstage/core'; -import { catalogApiRef, CatalogApi } from '@backstage/plugin-catalog-react'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; -import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; -import { RollbarProject } from '../../api/types'; -import { RollbarHome } from './RollbarHome'; - -describe('RollbarHome component', () => { - const projects: RollbarProject[] = [ - { id: 123, name: 'abc', accountId: 1, status: 'enabled' }, - { id: 456, name: 'xyz', accountId: 1, status: 'enabled' }, - ]; - const rollbarApi: Partial = { - getAllProjects: () => Promise.resolve(projects), - }; - const config: Partial = { - getString: () => 'foo', - getOptionalString: () => 'foo', - }; - - const renderWrapped = (children: React.ReactNode) => - render( - wrapInTestApp( - ) as CatalogApi, - ], - ])} - > - {children} - , - ), - ); - - it('should render rollbar landing page', async () => { - const rendered = renderWrapped(); - expect(rendered.getByText(/Rollbar/)).toBeInTheDocument(); - }); -}); diff --git a/plugins/rollbar/src/components/RollbarProject/RollbarProject.tsx b/plugins/rollbar/src/components/RollbarProject/RollbarProject.tsx index 34881ba560..b69c69a56b 100644 --- a/plugins/rollbar/src/components/RollbarProject/RollbarProject.tsx +++ b/plugins/rollbar/src/components/RollbarProject/RollbarProject.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ -import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import React from 'react'; import { useTopActiveItems } from '../../hooks/useTopActiveItems'; import { RollbarTopItemsTable } from '../RollbarTopItemsTable/RollbarTopItemsTable'; diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx deleted file mode 100644 index 6aeee782c4..0000000000 --- a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.test.tsx +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import { - ApiProvider, - ApiRegistry, - ConfigApi, - configApiRef, -} from '@backstage/core'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; -import { wrapInTestApp } from '@backstage/test-utils'; -import { render } from '@testing-library/react'; -import * as React from 'react'; -import { RollbarApi, rollbarApiRef } from '../../api/RollbarApi'; -import { RollbarTopActiveItem } from '../../api/types'; -import { RollbarProjectPage } from './RollbarProjectPage'; - -describe('RollbarProjectPage component', () => { - const items: RollbarTopActiveItem[] = [ - { - item: { - id: 9898989, - counter: 1234, - environment: 'production', - framework: 2, - lastOccurrenceTimestamp: new Date().getTime() / 1000, - level: 50, - occurrences: 100, - projectId: 12345, - title: 'error occurred', - uniqueOccurrences: 10, - }, - counts: [10, 10, 10, 10, 10, 50], - }, - ]; - const rollbarApi: Partial = { - getTopActiveItems: () => Promise.resolve(items), - }; - const config: Partial = { - getString: () => 'foo', - getOptionalString: () => 'foo', - }; - - const renderWrapped = (children: React.ReactNode) => - render( - wrapInTestApp( - ) as CatalogApi, - ], - ])} - > - {children} - , - ), - ); - - it('should render rollbar project page', async () => { - const rendered = renderWrapped(); - expect(rendered.getByText(/Rollbar/)).toBeInTheDocument(); - }); -}); diff --git a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx b/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx deleted file mode 100644 index 5563a6d7a0..0000000000 --- a/plugins/rollbar/src/components/RollbarProjectPage/RollbarProjectPage.tsx +++ /dev/null @@ -1,36 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { Content, Header, HeaderLabel, Page } from '@backstage/core'; -import { useCatalogEntity } from '../../hooks/useCatalogEntity'; -import { RollbarProject } from '../RollbarProject/RollbarProject'; - -export const RollbarProjectPage = () => { - const { entity } = useCatalogEntity(); - - return ( - -
- - -
- - {entity ? : 'Loading'} - -
- ); -}; diff --git a/plugins/rollbar/src/components/RollbarProjectTable/RollbarProjectTable.tsx b/plugins/rollbar/src/components/RollbarProjectTable/RollbarProjectTable.tsx deleted file mode 100644 index 5b3631ade9..0000000000 --- a/plugins/rollbar/src/components/RollbarProjectTable/RollbarProjectTable.tsx +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -import React from 'react'; -import { Link as RouterLink, generatePath } from 'react-router-dom'; -import { Table, TableColumn } from '@backstage/core'; -import { Entity } from '@backstage/catalog-model'; -import { Link } from '@material-ui/core'; -import { Alert } from '@material-ui/lab'; -import { entityRouteRef } from '../../routes'; - -const columns: TableColumn[] = [ - { - title: 'Name', - field: 'metadata.name', - type: 'string', - highlight: true, - render: (entity: any) => ( - - {entity.metadata.name} - - ), - }, - { - title: 'Description', - field: 'metadata.description', - }, -]; - -type Props = { - entities: Entity[]; - loading: boolean; - error?: any; -}; - -export const RollbarProjectTable = ({ entities, loading, error }: Props) => { - if (error) { - return ( -
- - Error encountered while fetching rollbar projects. {error.toString()} - -
- ); - } - - return ( - - ); -}; diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx index dc177265cd..2916fefbcf 100644 --- a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.test.tsx @@ -17,8 +17,8 @@ import { wrapInTestApp } from '@backstage/test-utils'; import { render } from '@testing-library/react'; import * as React from 'react'; -import { RollbarTopItemsTable } from './RollbarTopItemsTable'; import { RollbarTopActiveItem } from '../../api/types'; +import { RollbarTopItemsTable } from './RollbarTopItemsTable'; const items: RollbarTopActiveItem[] = [ { diff --git a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx index a04155a0c3..c22f813dd9 100644 --- a/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx +++ b/plugins/rollbar/src/components/RollbarTopItemsTable/RollbarTopItemsTable.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ -import React from 'react'; import { Table, TableColumn } from '@backstage/core'; import { Box, Link, Typography } from '@material-ui/core'; import { Alert } from '@material-ui/lab'; +import React from 'react'; import { RollbarFrameworkId, RollbarLevel, diff --git a/plugins/rollbar/src/components/Router.tsx b/plugins/rollbar/src/components/Router.tsx index f0756cb02a..7b95a4ec7c 100644 --- a/plugins/rollbar/src/components/Router.tsx +++ b/plugins/rollbar/src/components/Router.tsx @@ -14,29 +14,36 @@ * limitations under the License. */ -import React from 'react'; -import { Routes, Route } from 'react-router'; import { Entity } from '@backstage/catalog-model'; import { MissingAnnotationEmptyState } from '@backstage/core'; -import { catalogRouteRef } from '../routes'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import React from 'react'; +import { Route, Routes } from 'react-router'; import { ROLLBAR_ANNOTATION } from '../constants'; +import { rootRouteRef } from '../plugin'; import { EntityPageRollbar } from './EntityPageRollbar/EntityPageRollbar'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[ROLLBAR_ANNOTATION]); type Props = { - entity: Entity; + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; }; -export const Router = ({ entity }: Props) => - !isPluginApplicableToEntity(entity) ? ( - - ) : ( +export const Router = (_props: Props) => { + const { entity } = useEntity(); + + if (!isPluginApplicableToEntity(entity)) { + ; + } + + return ( } /> ); +}; diff --git a/plugins/rollbar/src/components/TrendGraph/TrendGraph.test.tsx b/plugins/rollbar/src/components/TrendGraph/TrendGraph.test.tsx index ac1a328521..e944a53551 100644 --- a/plugins/rollbar/src/components/TrendGraph/TrendGraph.test.tsx +++ b/plugins/rollbar/src/components/TrendGraph/TrendGraph.test.tsx @@ -14,9 +14,9 @@ * limitations under the License. */ -import React from 'react'; -import { render } from '@testing-library/react'; import { wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import React from 'react'; import { TrendGraph } from './TrendGraph'; describe('TrendGraph component', () => { diff --git a/plugins/rollbar/src/index.ts b/plugins/rollbar/src/index.ts index 2a31c91a13..def2d99658 100644 --- a/plugins/rollbar/src/index.ts +++ b/plugins/rollbar/src/index.ts @@ -14,10 +14,16 @@ * limitations under the License. */ -export { plugin } from './plugin'; export * from './api'; -export * from './routes'; -export { Router } from './components/Router'; -export { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage'; export { EntityPageRollbar } from './components/EntityPageRollbar/EntityPageRollbar'; +export { + isPluginApplicableToEntity, + isPluginApplicableToEntity as isRollbarAvailable, + Router, +} from './components/Router'; export { ROLLBAR_ANNOTATION } from './constants'; +export { + EntityRollbarContent, + rollbarPlugin as plugin, + rollbarPlugin, +} from './plugin'; diff --git a/plugins/rollbar/src/plugin.test.ts b/plugins/rollbar/src/plugin.test.ts index 44c2168c78..7ef23b019a 100644 --- a/plugins/rollbar/src/plugin.test.ts +++ b/plugins/rollbar/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { rollbarPlugin } from './plugin'; describe('rollbar', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(rollbarPlugin).toBeDefined(); }); }); diff --git a/plugins/rollbar/src/plugin.ts b/plugins/rollbar/src/plugin.ts index c3ca6173ff..216713b3fc 100644 --- a/plugins/rollbar/src/plugin.ts +++ b/plugins/rollbar/src/plugin.ts @@ -15,17 +15,21 @@ */ import { - createPlugin, createApiFactory, + createPlugin, + createRoutableExtension, + createRouteRef, discoveryApiRef, } from '@backstage/core'; -import { rootRouteRef, entityRouteRef } from './routes'; -import { RollbarHome } from './components/RollbarHome/RollbarHome'; -import { RollbarProjectPage } from './components/RollbarProjectPage/RollbarProjectPage'; import { rollbarApiRef } from './api/RollbarApi'; import { RollbarClient } from './api/RollbarClient'; -export const plugin = createPlugin({ +export const rootRouteRef = createRouteRef({ + path: '', + title: 'Rollbar', +}); + +export const rollbarPlugin = createPlugin({ id: 'rollbar', apis: [ createApiFactory({ @@ -34,8 +38,14 @@ export const plugin = createPlugin({ factory: ({ discoveryApi }) => new RollbarClient({ discoveryApi }), }), ], - register({ router }) { - router.addRoute(rootRouteRef, RollbarHome); - router.addRoute(entityRouteRef, RollbarProjectPage); + routes: { + entityContent: rootRouteRef, }, }); + +export const EntityRollbarContent = rollbarPlugin.provide( + createRoutableExtension({ + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 353e2321cf..e8526e0b56 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,103 @@ # @backstage/plugin-scaffolder-backend +## 0.6.0 + +### Minor Changes + +- cdea0baf1: The scaffolder is updated to generate a unique workspace directory inside the temp folder. This directory is cleaned up by the job processor after each run. + + The prepare/template/publish steps have been refactored to operate on known directories, `template/` and `result/`, inside the temporary workspace path. + + Updated preparers to accept the template url instead of the entire template. This is done primarily to allow for backwards compatibility between v1 and v2 scaffolder templates. + + Fixes broken GitHub actions templating in the Create React App template. + + #### For those with **custom** preparers, templates, or publishers + + The preparer interface has changed, the prepare method now only takes a single argument, and doesn't return anything. As part of this change the preparers were refactored to accept a URL pointing to the target directory, rather than computing that from the template entity. + + The `workingDirectory` option was also removed, and replaced with a `workspacePath` option. The difference between the two is that `workingDirectory` was a place for the preparer to create temporary directories, while the `workspacePath` is the specific folder were the entire templating process for a single template job takes place. Instead of returning a path to the folder were the prepared contents were placed, the contents are put at the `/template` path. + + ```diff + type PreparerOptions = { + - workingDirectory?: string; + + /** + + * Full URL to the directory containg template data + + */ + + url: string; + + /** + + * The workspace path that will eventually be the the root of the new repo + + */ + + workspacePath: string; + logger: Logger; + }; + + -prepare(template: TemplateEntityV1alpha1, opts?: PreparerOptions): Promise + +prepare(opts: PreparerOptions): Promise; + ``` + + Instead of returning a path to the folder were the templaters contents were placed, the contents are put at the `/result` path. All templaters now also expect the source template to be present in the `template` directory within the `workspacePath`. + + ```diff + export type TemplaterRunOptions = { + - directory: string; + + workspacePath: string; + values: TemplaterValues; + logStream?: Writable; + dockerClient: Docker; + }; + + -public async run(options: TemplaterRunOptions): Promise + +public async run(options: TemplaterRunOptions): Promise + ``` + + Just like the preparer and templaters, the publishers have also switched to using `workspacePath`. The root of the new repo is expected to be located at `/result`. + + ```diff + export type PublisherOptions = { + values: TemplaterValues; + - directory: string; + + workspacePath: string; + logger: Logger; + }; + ``` + +### Patch Changes + +- a26668913: Attempt to fix windows test errors in master +- 529d16d27: # Repo visibility for GitLab and BitBucket repos + + **NOTE: This changes default repo visibility from `private` to `public` for GitLab and BitBucket** which + is consistent with the GitHub default. If you were counting on `private` visibility, you'll need to update + your scaffolder config to use `private`. + + This adds repo visibility feature parity with GitHub for GitLab and BitBucket. + + To configure the repo visibility, set scaffolder._type_.visibility as in this example: + + ```yaml + scaffolder: + github: + visibility: private # 'public' or 'internal' or 'private' (default is 'public') + gitlab: + visibility: public # 'public' or 'internal' or 'private' (default is 'public') + bitbucket: + visibility: public # 'public' or 'private' (default is 'public') + ``` + +- Updated dependencies [c4abcdb60] +- Updated dependencies [2430ee7c2] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [064c513e1] +- Updated dependencies [7881f2117] +- Updated dependencies [3149bfe63] +- Updated dependencies [2e62aea6f] +- Updated dependencies [11cb5ef94] + - @backstage/integration@0.3.2 + - @backstage/backend-common@0.5.2 + - @backstage/catalog-model@0.7.1 + ## 0.5.2 ### Patch Changes diff --git a/plugins/scaffolder-backend/migrations/20210120143715_init.js b/plugins/scaffolder-backend/migrations/20210120143715_init.js new file mode 100644 index 0000000000..256ec7d423 --- /dev/null +++ b/plugins/scaffolder-backend/migrations/20210120143715_init.js @@ -0,0 +1,85 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// @ts-check + +/** + * @param {import('knex')} knex + */ +exports.up = async function up(knex) { + await knex.schema.createTable('tasks', table => { + table.comment('The table of scaffolder tasks'); + table.uuid('id').primary().notNullable().comment('The ID of the task'); + table + .text('spec') + .notNullable() + .comment('A JSON encoded task specification'); + table + .text('status') + .notNullable() + .comment('The current status of the task'); + table + .dateTime('created_at') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this task was created'); + table + .dateTime('last_heartbeat_at') + .nullable() + .comment('The last timestamp when a heartbeat was received'); + }); + + await knex.schema.createTable('task_events', table => { + table.comment('The event stream a given task'); + table + .bigIncrements('id') + .primary() + .notNullable() + .comment('The ID of the event'); + table + .uuid('task_id') + .references('id') + .inTable('tasks') + .notNullable() + .onDelete('CASCADE') + .comment('The task that generated the event'); + table + .text('body') + .notNullable() + .comment('The JSON encoded body of the event'); + table.text('event_type').notNullable().comment('The type of event'); + table + .timestamp('created_at') + .defaultTo(knex.fn.now()) + .notNullable() + .comment('The timestamp when this event was generated'); + + table.index(['task_id'], 'task_events_task_id_idx'); + }); +}; + +/** + * @param {import('knex')} knex + */ +exports.down = async function down(knex) { + if (knex.client.config.client !== 'sqlite3') { + await knex.schema.alterTable('task_events', table => { + table.dropIndex([], 'ctask_events_task_id_idx'); + }); + } + await knex.schema.dropTable('task_events'); + await knex.schema.dropTable('tasks'); +}; diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 7af9f567e3..ecce161c8e 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.5.2", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.3.1", + "@backstage/integration": "^0.3.2", "@gitbeaker/core": "^28.0.2", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", @@ -53,13 +53,14 @@ "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", + "knex": "^0.21.6", "morgan": "^1.10.0", "uuid": "^8.2.0", "winston": "^3.2.1", "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "@backstage/test-utils": "^0.1.5", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", @@ -71,6 +72,7 @@ }, "files": [ "dist", + "migrations", "config.d.ts" ], "configSchema": "config.d.ts" diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts new file mode 100644 index 0000000000..f151780ba9 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateActionRegistry } from '../tasks/TemplateConverter'; +import { FilePreparer, PreparerBuilder } from './prepare'; +import Docker from 'dockerode'; +import { TemplaterBuilder, TemplaterValues } from './templater'; +import { PublisherBuilder } from './publish'; + +type Options = { + dockerClient: Docker; + preparers: PreparerBuilder; + templaters: TemplaterBuilder; + publishers: PublisherBuilder; +}; + +export function registerLegacyActions( + registry: TemplateActionRegistry, + options: Options, +) { + const { dockerClient, preparers, templaters, publishers } = options; + + registry.register({ + id: 'legacy:prepare', + async handler(ctx) { + const { protocol, url } = ctx.parameters; + const preparer = + protocol === 'file' ? new FilePreparer() : preparers.get(url as string); + + ctx.logger.info('Prepare the skeleton'); + await preparer.prepare({ + url: url as string, + logger: ctx.logger, + workspacePath: ctx.workspacePath, + }); + }, + }); + + registry.register({ + id: 'legacy:template', + async handler(ctx) { + const { logger } = ctx; + + const templater = templaters.get(ctx.parameters.templater as string); + + logger.info('Run the templater'); + await templater.run({ + workspacePath: ctx.workspacePath, + dockerClient, + logStream: ctx.logStream, + values: ctx.parameters.values as TemplaterValues, + }); + }, + }); + + registry.register({ + id: 'legacy:publish', + async handler(ctx) { + const { values } = ctx.parameters; + if ( + typeof values !== 'object' || + values === null || + Array.isArray(values) + ) { + throw new Error( + `Invalid values passed to publish, got ${typeof values}`, + ); + } + const storePath = values.storePath as unknown; + if (typeof storePath !== 'string') { + throw new Error( + `Invalid store path passed to publish, got ${typeof storePath}`, + ); + } + const owner = values.owner as unknown; + if (typeof owner !== 'string') { + throw new Error(`Invalid owner passed to publish, got ${typeof owner}`); + } + + const publisher = publishers.get(storePath); + ctx.logger.info('Will now store the template'); + const { remoteUrl, catalogInfoUrl } = await publisher.publish({ + values: { + ...values, + owner, + storePath, + }, + workspacePath: ctx.workspacePath, + logger: ctx.logger, + }); + ctx.output('remoteUrl', remoteUrl); + if (catalogInfoUrl) { + ctx.output('catalogInfoUrl', catalogInfoUrl); + } + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts index beba62eb2a..faf7f0f6ff 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/bitbucket.test.ts @@ -62,12 +62,12 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with the correct arguments if an app password is provided for a repository', async () => { - const preparer = BitbucketPreparer.fromConfig({ + const preparerCheck = BitbucketPreparer.fromConfig({ host: 'bitbucket.org', username: 'fake-user', appPassword: 'fake-password', }); - await preparer.prepare(prepareOptions); + await preparerCheck.prepare(prepareOptions); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, @@ -98,12 +98,11 @@ describe('BitbucketPreparer', () => { }); it('calls the clone command with with token for auth method', async () => { - const preparer = BitbucketPreparer.fromConfig({ + const preparerCheck = BitbucketPreparer.fromConfig({ host: 'bitbucket.org', token: 'fake-token', }); - - await preparer.prepare(prepareOptions); + await preparerCheck.prepare(prepareOptions); expect(Git.fromAuth).toHaveBeenCalledWith({ logger, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts index 1a3ad76499..77368fc1ab 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.test.ts @@ -56,6 +56,7 @@ describe('CookieCutter Templater', () => { dockerClient: mockDocker, }); + expect(fs.ensureDir).toBeCalledWith(path.join('tempdir', 'intermediate')); expect(fs.writeJson).toBeCalledWith( path.join('tempdir', 'template', 'cookiecutter.json'), expect.objectContaining(values), diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts index 09d4faa37c..9568af2a15 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/templater/cookiecutter.ts @@ -44,6 +44,7 @@ export class CookieCutter implements TemplaterBase { }: TemplaterRunOptions): Promise { const templateDir = path.join(workspacePath, 'template'); const intermediateDir = path.join(workspacePath, 'intermediate'); + await fs.ensureDir(intermediateDir); const resultDir = path.join(workspacePath, 'result'); // First lets grab the default cookiecutter.json file diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts new file mode 100644 index 0000000000..5717f3c8c1 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -0,0 +1,272 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JsonObject } from '@backstage/config'; +import { + ConflictError, + NotFoundError, + resolvePackagePath, +} from '@backstage/backend-common'; +import Knex from 'knex'; +import { v4 as uuid } from 'uuid'; +import { + DbTaskEventRow, + DbTaskRow, + Status, + TaskEventType, + TaskSpec, + TaskStore, + TaskStoreEmitOptions, + TaskStoreGetEventsOptions, +} from './types'; + +const migrationsDir = resolvePackagePath( + '@backstage/plugin-scaffolder-backend', + 'migrations', +); + +export type RawDbTaskRow = { + id: string; + spec: string; + status: Status; + last_heartbeat_at?: string; + created_at: string; +}; + +export type RawDbTaskEventRow = { + id: number; + task_id: string; + body: string; + event_type: TaskEventType; + created_at: string; +}; + +export class DatabaseTaskStore implements TaskStore { + static async create(knex: Knex): Promise { + await knex.migrate.latest({ + directory: migrationsDir, + }); + return new DatabaseTaskStore(knex); + } + + constructor(private readonly db: Knex) {} + + async get(taskId: string): Promise { + const [result] = await this.db('tasks') + .where({ id: taskId }) + .select(); + if (!result) { + throw new NotFoundError(`No task with id '${taskId}' found`); + } + try { + const spec = JSON.parse(result.spec); + return { + id: result.id, + spec, + status: result.status, + lastHeartbeatAt: result.last_heartbeat_at, + createdAt: result.created_at, + }; + } catch (error) { + throw new Error(`Failed to parse spec of task '${taskId}', ${error}`); + } + } + + async createTask(spec: TaskSpec): Promise<{ taskId: string }> { + const taskId = uuid(); + await this.db('tasks').insert({ + id: taskId, + spec: JSON.stringify(spec), + status: 'open', + }); + return { taskId }; + } + + async claimTask(): Promise { + return this.db.transaction(async tx => { + const [task] = await tx('tasks') + .where({ + status: 'open', + }) + .limit(1) + .select(); + + if (!task) { + return undefined; + } + + const updateCount = await tx('tasks') + .where({ id: task.id, status: 'open' }) + .update({ + status: 'processing', + last_heartbeat_at: this.db.fn.now(), + }); + + if (updateCount < 1) { + return undefined; + } + + try { + const spec = JSON.parse(task.spec); + return { + id: task.id, + spec, + status: 'processing', + lastHeartbeatAt: task.last_heartbeat_at, + createdAt: task.created_at, + }; + } catch (error) { + throw new Error(`Failed to parse spec of task '${task.id}', ${error}`); + } + }); + } + + async heartbeatTask(taskId: string): Promise { + const updateCount = await this.db('tasks') + .where({ id: taskId, status: 'processing' }) + .update({ + last_heartbeat_at: this.db.fn.now(), + }); + if (updateCount === 0) { + throw new ConflictError(`No running task with taskId ${taskId} found`); + } + } + + async listStaleTasks({ + timeoutS, + }: { + timeoutS: number; + }): Promise<{ + tasks: { taskId: string }[]; + }> { + const rawRows = await this.db('tasks') + .where('status', 'processing') + .andWhere( + 'last_heartbeat_at', + '<=', + this.db.client.config.client === 'sqlite3' + ? this.db.raw(`datetime('now', ?)`, [`-${timeoutS} seconds`]) + : this.db.raw(`dateadd('second', ?, ?)`, [ + `-${timeoutS}`, + this.db.fn.now(), + ]), + ); + const tasks = rawRows.map(row => ({ + taskId: row.id, + })); + return { tasks }; + } + + async completeTask({ + taskId, + status, + eventBody, + }: { + taskId: string; + status: Status; + eventBody: JsonObject; + }): Promise { + let oldStatus: string; + if (status === 'failed' || status === 'completed') { + oldStatus = 'processing'; + } else { + throw new Error( + `Invalid status update of run '${taskId}' to status '${status}'`, + ); + } + await this.db.transaction(async tx => { + const [task] = await tx('tasks') + .where({ + id: taskId, + }) + .limit(1) + .select(); + + if (!task) { + throw new Error(`No task with taskId ${taskId} found`); + } + if (task.status !== oldStatus) { + throw new ConflictError( + `Refusing to update status of run '${taskId}' to status '${status}' ` + + `as it is currently '${task.status}', expected '${oldStatus}'`, + ); + } + const updateCount = await tx('tasks') + .where({ + id: taskId, + status: oldStatus, + }) + .update({ + status, + }); + if (updateCount !== 1) { + throw new ConflictError( + `Failed to update status to '${status}' for taskId ${taskId}`, + ); + } + + await tx('task_events').insert({ + task_id: taskId, + event_type: 'completion', + body: JSON.stringify(eventBody), + }); + }); + } + + async emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise { + const serliazedBody = JSON.stringify(body); + await this.db('task_events').insert({ + task_id: taskId, + event_type: 'log', + body: serliazedBody, + }); + } + + async listEvents({ + taskId, + after, + }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }> { + const rawEvents = await this.db('task_events') + .where({ + task_id: taskId, + }) + .andWhere(builder => { + if (typeof after === 'number') { + builder.where('id', '>', after).orWhere('event_type', 'completion'); + } + }) + .orderBy('id') + .select(); + + const events = rawEvents.map(event => { + try { + const body = JSON.parse(event.body) as JsonObject; + return { + id: event.id, + taskId, + body, + type: event.event_type, + createdAt: event.created_at, + }; + } catch (error) { + throw new Error( + `Failed to parse event body from event taskId=${taskId} id=${event.id}, ${error}`, + ); + } + }); + return { events }; + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts new file mode 100644 index 0000000000..c45fa46335 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -0,0 +1,183 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + getVoidLogger, + SingleConnectionDatabaseManager, +} from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { DatabaseTaskStore } from './DatabaseTaskStore'; +import { StorageTaskBroker, TaskAgent } from './StorageTaskBroker'; +import { TaskSpec, DbTaskEventRow } from './types'; + +async function createStore(): Promise { + const manager = SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('scaffolder'); + return await DatabaseTaskStore.create(await manager.getClient()); +} + +describe('StorageTaskBroker', () => { + let storage: DatabaseTaskStore; + + beforeAll(async () => { + storage = await createStore(); + }); + + const logger = getVoidLogger(); + it('should claim a dispatched work item', async () => { + const broker = new StorageTaskBroker(storage, logger); + await broker.dispatch({ steps: [] }); + await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); + }); + + it('should wait for a dispatched work item', async () => { + const broker = new StorageTaskBroker(storage, logger); + const promise = broker.claim(); + + await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); + + await broker.dispatch({ steps: [] }); + await expect(promise).resolves.toEqual(expect.any(TaskAgent)); + }); + + it('should dispatch multiple items and claim them in order', async () => { + const broker = new StorageTaskBroker(storage, logger); + await broker.dispatch({ steps: [{ id: 'a' }] } as TaskSpec); + await broker.dispatch({ steps: [{ id: 'b' }] } as TaskSpec); + await broker.dispatch({ steps: [{ id: 'c' }] } as TaskSpec); + + const taskA = await broker.claim(); + const taskB = await broker.claim(); + const taskC = await broker.claim(); + await expect(taskA).toEqual(expect.any(TaskAgent)); + await expect(taskB).toEqual(expect.any(TaskAgent)); + await expect(taskC).toEqual(expect.any(TaskAgent)); + await expect(taskA.spec.steps[0].id).toBe('a'); + await expect(taskB.spec.steps[0].id).toBe('b'); + await expect(taskC.spec.steps[0].id).toBe('c'); + }); + + it('should complete a task', async () => { + const broker = new StorageTaskBroker(storage, logger); + const dispatchResult = await broker.dispatch({ steps: [] }); + const task = await broker.claim(); + await task.complete('completed'); + const taskRow = await storage.get(dispatchResult.taskId); + expect(taskRow.status).toBe('completed'); + }, 10000); + + it('should fail a task', async () => { + const broker = new StorageTaskBroker(storage, logger); + const dispatchResult = await broker.dispatch({ steps: [] }); + const task = await broker.claim(); + await task.complete('failed'); + const taskRow = await storage.get(dispatchResult.taskId); + expect(taskRow.status).toBe('failed'); + }); + + it('multiple brokers should be able to observe a single task', async () => { + const broker1 = new StorageTaskBroker(storage, logger); + const broker2 = new StorageTaskBroker(storage, logger); + + const { taskId } = await broker1.dispatch({ steps: [] }); + + const logPromise = new Promise(resolve => { + const observedEvents = new Array(); + + broker2.observe({ taskId, after: undefined }, (_err, { events }) => { + observedEvents.push(...events); + if (events.some(e => e.type === 'completion')) { + resolve(observedEvents); + } + }); + }); + const task = await broker1.claim(); + await task.emitLog('log 1'); + await task.emitLog('log 2'); + await task.emitLog('log 3'); + await task.complete('completed'); + + const logs = await logPromise; + expect(logs.map(l => l.body.message, logger)).toEqual([ + 'log 1', + 'log 2', + 'log 3', + 'Run completed with status: completed', + ]); + + const afterLogs = await new Promise(resolve => { + broker2.observe({ taskId, after: logs[1].id }, (_err, { events }) => + resolve(events.map(e => e.body.message as string)), + ); + }); + expect(afterLogs).toEqual([ + 'log 3', + 'Run completed with status: completed', + ]); + }); + + it('should heartbeat', async () => { + const broker = new StorageTaskBroker(storage, logger); + const { taskId } = await broker.dispatch({ steps: [] }); + const task = await broker.claim(); + + const initialTask = await storage.get(taskId); + + for (;;) { + const maybeTask = await storage.get(taskId); + if (maybeTask.lastHeartbeatAt !== initialTask.lastHeartbeatAt) { + break; + } + await new Promise(resolve => setTimeout(resolve, 50)); + } + await task.complete('completed'); + expect.assertions(0); + }); + + it('should be update the status to failed if heartbeat fails', async () => { + const broker = new StorageTaskBroker(storage, logger); + const { taskId } = await broker.dispatch({ steps: [] }); + const task = await broker.claim(); + + jest + .spyOn((task as any).storage, 'heartbeatTask') + .mockRejectedValue(new Error('nah m8')); + + const intervalId = setInterval(() => { + broker.vacuumTasks({ timeoutS: 2 }).catch(fail); + }, 500); + + for (;;) { + const maybeTask = await storage.get(taskId); + if (maybeTask.status === 'failed') { + break; + } + await new Promise(resolve => setTimeout(resolve, 50)); + } + + clearInterval(intervalId); + + expect(task.done).toBe(true); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts new file mode 100644 index 0000000000..48c0d5e5ba --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -0,0 +1,205 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { Logger } from 'winston'; +import { + CompletedTaskState, + Task, + TaskSpec, + TaskStore, + TaskBroker, + DispatchResult, + DbTaskEventRow, +} from './types'; + +export class TaskAgent implements Task { + private isDone = false; + + private heartbeatTimeoutId?: ReturnType; + + static create(state: TaskState, storage: TaskStore, logger: Logger) { + const agent = new TaskAgent(state, storage, logger); + agent.startTimeout(); + return agent; + } + + // Runs heartbeat internally + private constructor( + private readonly state: TaskState, + private readonly storage: TaskStore, + private readonly logger: Logger, + ) {} + + get spec() { + return this.state.spec; + } + + async getWorkspaceName() { + return this.state.taskId; + } + + get done() { + return this.isDone; + } + + async emitLog(message: string): Promise { + await this.storage.emitLogEvent({ + taskId: this.state.taskId, + body: { message }, + }); + } + + async complete(result: CompletedTaskState): Promise { + await this.storage.completeTask({ + taskId: this.state.taskId, + status: result === 'failed' ? 'failed' : 'completed', + eventBody: { message: `Run completed with status: ${result}` }, + }); + this.isDone = true; + if (this.heartbeatTimeoutId) { + clearTimeout(this.heartbeatTimeoutId); + } + } + + private startTimeout() { + this.heartbeatTimeoutId = setTimeout(async () => { + try { + await this.storage.heartbeatTask(this.state.taskId); + this.startTimeout(); + } catch (error) { + this.isDone = true; + + this.logger.error( + `Heartbeat for task ${this.state.taskId} failed`, + error, + ); + } + }, 1000); + } +} + +interface TaskState { + spec: TaskSpec; + taskId: string; +} + +function defer() { + let resolve = () => {}; + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + return { promise, resolve }; +} + +export class StorageTaskBroker implements TaskBroker { + constructor( + private readonly storage: TaskStore, + private readonly logger: Logger, + ) {} + private deferredDispatch = defer(); + + async claim(): Promise { + for (;;) { + const pendingTask = await this.storage.claimTask(); + if (pendingTask) { + return TaskAgent.create( + { + taskId: pendingTask.id, + spec: pendingTask.spec, + }, + this.storage, + this.logger, + ); + } + + await this.waitForDispatch(); + } + } + + async dispatch(spec: TaskSpec): Promise { + const taskRow = await this.storage.createTask(spec); + this.signalDispatch(); + return { + taskId: taskRow.taskId, + }; + } + + observe( + options: { + taskId: string; + after: number | undefined; + }, + callback: ( + error: Error | undefined, + result: { events: DbTaskEventRow[] }, + ) => void, + ): () => void { + const { taskId } = options; + + let cancelled = false; + const unsubscribe = () => { + cancelled = true; + }; + + (async () => { + let after = options.after; + while (!cancelled) { + const result = await this.storage.listEvents({ taskId, after: after }); + const { events } = result; + if (events.length) { + after = events[events.length - 1].id; + try { + callback(undefined, result); + } catch (error) { + callback(error, { events: [] }); + } + } + + await new Promise(resolve => setTimeout(resolve, 1000)); + } + })(); + + return unsubscribe; + } + + async vacuumTasks(timeoutS: { timeoutS: number }): Promise { + const { tasks } = await this.storage.listStaleTasks(timeoutS); + await Promise.all( + tasks.map(async task => { + try { + await this.storage.completeTask({ + taskId: task.taskId, + status: 'failed', + eventBody: { + message: + 'The task was cancelled because the task worker lost connection to the task broker', + }, + }); + } catch (error) { + this.logger.warn(`Failed to cancel task '${task.taskId}', ${error}`); + } + }), + ); + } + + private waitForDispatch() { + return this.deferredDispatch.promise; + } + + private signalDispatch() { + this.deferredDispatch.resolve(); + this.deferredDispatch = defer(); + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts new file mode 100644 index 0000000000..c62495ca4c --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -0,0 +1,110 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { Logger } from 'winston'; +import * as winston from 'winston'; +import { JsonValue } from '@backstage/config'; +import { TaskBroker, Task } from './types'; +import fs from 'fs-extra'; +import path from 'path'; +import { TemplateActionRegistry } from './TemplateConverter'; + +type Options = { + logger: Logger; + taskBroker: TaskBroker; + workingDirectory: string; + actionRegistry: TemplateActionRegistry; +}; + +export class TaskWorker { + constructor(private readonly options: Options) {} + + start() { + (async () => { + for (;;) { + const task = await this.options.taskBroker.claim(); + await this.runOneTask(task); + } + })(); + } + + async runOneTask(task: Task) { + try { + const { actionRegistry, logger } = this.options; + + const workspacePath = path.join( + this.options.workingDirectory, + await task.getWorkspaceName(), + ); + await fs.ensureDir(workspacePath); + + const taskLogger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: winston.format.combine( + winston.format.colorize(), + winston.format.timestamp(), + winston.format.simple(), + ), + defaultMeta: {}, + }); + + const stream = new PassThrough(); + stream.on('data', data => { + const message = data.toString().trim(); + if (message?.length > 1) task.emitLog(message); + }); + + taskLogger.add(new winston.transports.Stream({ stream })); + + // Give us some time to curl observe + task.emitLog('Task claimed, waiting ...'); + await new Promise(resolve => setTimeout(resolve, 5000)); + task.emitLog(`Starting up work with ${task.spec.steps.length} steps`); + + const outputs: { [name: string]: JsonValue } = {}; + + for (const step of task.spec.steps) { + task.emitLog(`Beginning step ${step.name}`); + + const action = actionRegistry.get(step.action); + if (!action) { + throw new Error(`Action '${step.action}' does not exist`); + } + + // TODO: substitute any placeholders with output from previous steps + const parameters = step.parameters!; + + await action.handler({ + logger, + logStream: stream, + parameters, + workspacePath, + output(name: string, value: JsonValue) { + outputs[name] = value; + }, + }); + + task.emitLog(`Finished step ${step.name}`); + } + + await task.complete('completed'); + } catch (error) { + task.emitLog(String(error.stack)); + await task.complete('failed'); + } + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts new file mode 100644 index 0000000000..69788238cc --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts @@ -0,0 +1,117 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { resolve as resolvePath } from 'path'; +import { JsonValue } from '@backstage/config'; +import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { Logger } from 'winston'; +import type { Writable } from 'stream'; + +import { TaskSpec } from './types'; +import { ConflictError, NotFoundError } from '@backstage/backend-common'; +import { + getTemplaterKey, + joinGitUrlPath, + parseLocationAnnotation, + TemplaterValues, +} from '../stages'; + +export function templateEntityToSpec( + template: TemplateEntityV1alpha1, + values: TemplaterValues, +): TaskSpec { + const steps: TaskSpec['steps'] = []; + + const { protocol, location } = parseLocationAnnotation(template); + + let url: string; + if (protocol === 'file') { + const path = resolvePath(location, template.spec.path || '.'); + + url = `file://${path}`; + } else { + url = joinGitUrlPath(location, template.spec.path); + } + const templater = getTemplaterKey(template); + + steps.push({ + id: 'prepare', + name: 'Prepare', + action: 'legacy:prepare', + parameters: { + protocol, + url, + }, + }); + + steps.push({ + id: 'template', + name: 'Template', + action: 'legacy:template', + parameters: { + templater, + values, + }, + }); + + steps.push({ + id: 'publish', + name: 'Publishing', + action: 'legacy:publish', + parameters: { + values, + }, + }); + + return { steps }; +} + +type ActionContext = { + logger: Logger; + logStream: Writable; + + workspacePath: string; + parameters: { [name: string]: JsonValue }; + output(name: string, value: JsonValue): void; +}; + +type TemplateAction = { + id: string; + handler: (ctx: ActionContext) => Promise; +}; + +export class TemplateActionRegistry { + private readonly actions = new Map(); + + register(action: TemplateAction) { + if (this.actions.has(action.id)) { + throw new ConflictError( + `Template action with ID '${action.id}' has already been registered`, + ); + } + this.actions.set(action.id, action); + } + + get(actionId: string): TemplateAction { + const action = this.actions.get(actionId); + if (!action) { + throw new NotFoundError( + `Template action with ID '${actionId}' is not registered.`, + ); + } + return action; + } +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts new file mode 100644 index 0000000000..c85135426e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { DatabaseTaskStore } from './DatabaseTaskStore'; +export { StorageTaskBroker } from './StorageTaskBroker'; +export { TaskWorker } from './TaskWorker'; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts new file mode 100644 index 0000000000..0c2592109b --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -0,0 +1,111 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { JsonValue, JsonObject } from '@backstage/config'; + +export type Status = + | 'open' + | 'processing' + | 'failed' + | 'cancelled' + | 'completed'; + +export type CompletedTaskState = 'failed' | 'completed'; + +export type DbTaskRow = { + id: string; + spec: TaskSpec; + status: Status; + createdAt: string; + lastHeartbeatAt?: string; +}; + +export type TaskEventType = 'completion' | 'log'; +export type DbTaskEventRow = { + id: number; + taskId: string; + body: JsonObject; + type: TaskEventType; + createdAt: string; +}; + +export type TaskSpec = { + steps: Array<{ + id: string; + name: string; + action: string; + parameters?: { [name: string]: JsonValue }; + }>; +}; + +export type DispatchResult = { + taskId: string; +}; + +export interface Task { + spec: TaskSpec; + done: boolean; + emitLog(message: string): Promise; + complete(result: CompletedTaskState): Promise; + getWorkspaceName(): Promise; +} + +export interface TaskBroker { + claim(): Promise; + dispatch(spec: TaskSpec): Promise; + vacuumTasks(timeoutS: { timeoutS: number }): Promise; + observe( + options: { + taskId: string; + after: number | undefined; + }, + callback: ( + error: Error | undefined, + result: { events: DbTaskEventRow[] }, + ) => void, + ): () => void; +} + +export type TaskStoreEmitOptions = { + taskId: string; + body: JsonObject; +}; + +export type TaskStoreGetEventsOptions = { + taskId: string; + after?: number | undefined; +}; +export interface TaskStore { + createTask(task: TaskSpec): Promise<{ taskId: string }>; + claimTask(): Promise; + completeTask(options: { + taskId: string; + status: Status; + eventBody: JsonObject; + }): Promise; + heartbeatTask(taskId: string): Promise; + listStaleTasks(options: { + timeoutS: number; + }): Promise<{ + tasks: { taskId: string }[]; + }>; + + emitLogEvent({ taskId, body }: TaskStoreEmitOptions): Promise; + listEvents({ + taskId, + after, + }: TaskStoreGetEventsOptions): Promise<{ events: DbTaskEventRow[] }>; +} diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts new file mode 100644 index 0000000000..dd3d43c6d1 --- /dev/null +++ b/plugins/scaffolder-backend/src/service/helpers.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'os'; +import fs from 'fs-extra'; +import { Logger } from 'winston'; +import { Config } from '@backstage/config'; + +export async function getWorkingDirectory( + config: Config, + logger: Logger, +): Promise { + if (!config.has('backend.workingDirectory')) { + return os.tmpdir(); + } + + const workingDirectory = config.getString('backend.workingDirectory'); + try { + // Check if working directory exists and is writable + await fs.access(workingDirectory, fs.constants.F_OK | fs.constants.W_OK); + logger.info(`using working directory: ${workingDirectory}`); + } catch (err) { + logger.error( + `working directory ${workingDirectory} ${ + err.code === 'ENOENT' ? 'does not exist' : 'is not writable' + }`, + ); + throw err; + } + return workingDirectory; +} diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 31f62febd2..8a0fa6cba6 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -16,6 +16,7 @@ const mockAccess = jest.fn(); jest.doMock('fs-extra', () => ({ + access: mockAccess, promises: { access: mockAccess, }, @@ -27,7 +28,11 @@ jest.doMock('fs-extra', () => ({ remove: jest.fn(), })); -import { getVoidLogger } from '@backstage/backend-common'; +import { + SingleConnectionDatabaseManager, + PluginDatabaseManager, + getVoidLogger, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import express from 'express'; import request from 'supertest'; @@ -41,6 +46,19 @@ const generateEntityClient: any = (template: any) => ({ findTemplate: () => Promise.resolve(template), }); +function createDatabase(): PluginDatabaseManager { + return SingleConnectionDatabaseManager.fromConfig( + new ConfigReader({ + backend: { + database: { + client: 'sqlite3', + connection: ':memory:', + }, + }, + }), + ).forPlugin('scaffolder'); +} + describe('createRouter - working directory', () => { const mockPrepare = jest.fn(); const mockPreparers = new Preparers(); @@ -78,7 +96,6 @@ describe('createRouter - working directory', () => { }; const mockedEntityClient = generateEntityClient(template); - it('should throw an error when working directory does not exist or is not writable', async () => { mockAccess.mockImplementation(() => { throw new Error('access error'); @@ -93,6 +110,7 @@ describe('createRouter - working directory', () => { config: new ConfigReader(workDirConfig('/path')), dockerClient: new Docker(), entityClient: mockedEntityClient, + database: createDatabase(), }), ).rejects.toThrow('access error'); }); @@ -106,6 +124,7 @@ describe('createRouter - working directory', () => { config: new ConfigReader(workDirConfig('/path')), dockerClient: new Docker(), entityClient: mockedEntityClient, + database: createDatabase(), }); const app = express().use(router); @@ -134,6 +153,7 @@ describe('createRouter - working directory', () => { config: new ConfigReader({}), dockerClient: new Docker(), entityClient: mockedEntityClient, + database: createDatabase(), }); const app = express().use(router); @@ -203,6 +223,7 @@ describe('createRouter', () => { config: new ConfigReader({}), dockerClient: new Docker(), entityClient: generateEntityClient(template), + database: createDatabase(), }); app = express().use(router); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index 9098e5d05e..5821edfe8c 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -33,6 +33,18 @@ import { import { CatalogEntityClient } from '../lib/catalog'; import { validate, ValidatorResult } from 'jsonschema'; import parseGitUrl from 'git-url-parse'; +import { + DatabaseTaskStore, + StorageTaskBroker, + TaskWorker, +} from '../scaffolder/tasks'; +import { + TemplateActionRegistry, + templateEntityToSpec, +} from '../scaffolder/tasks/TemplateConverter'; +import { registerLegacyActions } from '../scaffolder/stages/legacy'; +import { getWorkingDirectory } from './helpers'; +import { PluginDatabaseManager } from '@backstage/backend-common'; export interface RouterOptions { preparers: PreparerBuilder; @@ -43,6 +55,7 @@ export interface RouterOptions { config: Config; dockerClient: Docker; entityClient: CatalogEntityClient; + database: PluginDatabaseManager; } export async function createRouter( @@ -59,11 +72,34 @@ export async function createRouter( config, dockerClient, entityClient, + database, } = options; const logger = parentLogger.child({ plugin: 'scaffolder' }); + const workingDirectory = await getWorkingDirectory(config, logger); const jobProcessor = await JobProcessor.fromConfig({ config, logger }); + const databaseTaskStore = await DatabaseTaskStore.create( + await database.getClient(), + ); + const taskBroker = new StorageTaskBroker(databaseTaskStore, logger); + const actionRegistry = new TemplateActionRegistry(); + const worker = new TaskWorker({ + logger, + taskBroker, + actionRegistry, + workingDirectory, + }); + + registerLegacyActions(actionRegistry, { + dockerClient, + preparers, + publishers, + templaters, + }); + + worker.start(); + router .get('/v1/job/:jobId', ({ params }, res) => { const job = jobProcessor.get(params.jobId); @@ -184,6 +220,75 @@ export async function createRouter( res.status(201).json({ id: job.id }); }); + // NOTE: The v2 API is unstable + router + .post('/v2/tasks', async (req, res) => { + const templateName: string = req.body.templateName; + const values: TemplaterValues = { + ...req.body.values, + destination: { + git: parseGitUrl(req.body.values.storePath), + }, + }; + const template = await entityClient.findTemplate(templateName); + + const validationResult: ValidatorResult = validate( + values, + template.spec.schema, + ); + + if (!validationResult.valid) { + res.status(400).json({ errors: validationResult.errors }); + return; + } + const taskSpec = templateEntityToSpec(template, values); + const result = await taskBroker.dispatch(taskSpec); + + res.status(201).json({ id: result.taskId }); + }) + .get('/v2/tasks/:taskId/eventstream', async (req, res) => { + const { taskId } = req.params; + const after = Number(req.query.after) || undefined; + logger.debug(`Event stream observing taskId '${taskId}' opened`); + + // Mandatory headers and http status to keep connection open + res.writeHead(200, { + Connection: 'keep-alive', + 'Cache-Control': 'no-cache', + 'Content-Type': 'text/event-stream', + }); + + // After client opens connection send all events as string + const unsubscribe = taskBroker.observe( + { taskId, after }, + (error, { events }) => { + if (error) { + logger.error( + `Received error from event stream when observing taskId '${taskId}', ${error}`, + ); + } + + for (const event of events) { + res.write( + `event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`, + ); + if (event.type === 'completion') { + unsubscribe(); + // Closing the event stream here would cause the frontend + // to automatically reconnect because it lost connection. + } + } + res.flush(); + }, + ); + // When client closes connection we update the clients list + // avoiding the disconnected one + req.on('close', () => { + unsubscribe(); + logger.debug(`Event stream observing taskId '${taskId}' closed`); + }); + }); + const app = express(); app.set('logger', logger); app.use('/', router); diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 11a9cfb75c..cd7c8fe685 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,29 @@ # @backstage/plugin-scaffolder +## 0.4.2 + +### Patch Changes + +- 720149854: Migrated to new composability API, exporting the plugin as `scaffolderPlugin`. The template list page (`/create`) is exported as the `TemplateIndexPage` extension, and the templating page itself is exported as `TemplatePage`. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.4.1 ### Patch Changes diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 69021953a2..56beb7b10b 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.4.1", + "version": "0.4.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -51,8 +51,8 @@ "swr": "^0.3.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@backstage/catalog-client": "^0.3.5", "@testing-library/jest-dom": "^5.10.1", diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx index 528d42b94f..27c5b4f18a 100644 --- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx +++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx @@ -19,7 +19,7 @@ import { Box, Button, Paper, - Step, + Step as StepUI, StepContent, StepLabel, Stepper, @@ -67,7 +67,7 @@ export const MultistepJsonForm = ({ <> {steps.map(({ label, schema, ...formProps }) => ( - + {label}
-
+ ))}
{activeStep === steps.length && ( diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 64351c7ec8..e1422d4ec1 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -92,8 +92,8 @@ export const TemplatePage = () => { ); const [jobId, setJobId] = useState(null); - const job = useJobPolling(jobId, async job => { - if (!job.metadata.catalogInfoUrl) { + const job = useJobPolling(jobId, async jobItem => { + if (!jobItem.metadata.catalogInfoUrl) { errorApi.post( new Error(`No catalogInfoUrl returned from the scaffolder`), ); @@ -103,7 +103,9 @@ export const TemplatePage = () => { try { const { entities: [createdEntity], - } = await catalogApi.addLocation({ target: job.metadata.catalogInfoUrl }); + } = await catalogApi.addLocation({ + target: jobItem.metadata.catalogInfoUrl, + }); const resolvedPath = generatePath( `/catalog/${entityRoute.path}`, @@ -122,8 +124,8 @@ export const TemplatePage = () => { const handleCreate = async () => { try { - const jobId = await scaffolderApi.scaffold(templateName, formState); - setJobId(jobId); + const id = await scaffolderApi.scaffold(templateName, formState); + setJobId(id); setModalOpen(true); } catch (e) { errorApi.post(e); diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 35590bc2d7..383cb3c484 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-search +## 0.2.7 + +### Patch Changes + +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.2.6 ### Patch Changes diff --git a/plugins/search/dev/index.tsx b/plugins/search/dev/index.tsx index 264d6f801f..e6e97ead6d 100644 --- a/plugins/search/dev/index.tsx +++ b/plugins/search/dev/index.tsx @@ -14,6 +14,6 @@ * limitations under the License. */ import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { searchPlugin } from '../src/plugin'; -createDevApp().registerPlugin(plugin).render(); +createDevApp().registerPlugin(searchPlugin).render(); diff --git a/plugins/search/package.json b/plugins/search/package.json index 28627b88ea..6142e65dd3 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,10 +29,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/catalog-model": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/catalog-model": "^0.7.1", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -43,8 +43,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index 3c43c1d24c..c4d8809732 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -57,11 +57,6 @@ type TableHeaderProps = { handleToggleFilters: () => void; }; -type Filters = { - selected: string; - checked: Array; -}; - // TODO: move out column to make the search result component more generic const columns: TableColumn[] = [ { diff --git a/plugins/search/src/index.ts b/plugins/search/src/index.ts index 77ad7f9266..572d75bea5 100644 --- a/plugins/search/src/index.ts +++ b/plugins/search/src/index.ts @@ -13,5 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { plugin } from './plugin'; -export * from './components'; +export { searchPlugin, searchPlugin as plugin, SearchPage } from './plugin'; +export { + Filters, + FiltersButton, + SearchBar, + SearchPage as Router, + SearchResult, + SidebarSearch, +} from './components'; +export type { FiltersState } from './components'; diff --git a/plugins/search/src/plugin.test.ts b/plugins/search/src/plugin.test.ts index 92b8d5bcbf..902faeaf9e 100644 --- a/plugins/search/src/plugin.test.ts +++ b/plugins/search/src/plugin.test.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { plugin } from './plugin'; +import { searchPlugin } from './plugin'; describe('search', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(searchPlugin).toBeDefined(); }); }); diff --git a/plugins/search/src/plugin.ts b/plugins/search/src/plugin.ts index 44c7bcb042..37503b7751 100644 --- a/plugins/search/src/plugin.ts +++ b/plugins/search/src/plugin.ts @@ -13,17 +13,31 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { createPlugin, createRouteRef } from '@backstage/core'; -import { SearchPage } from './components/SearchPage'; +import { + createPlugin, + createRouteRef, + createRoutableExtension, +} from '@backstage/core'; +import { SearchPage as SearchPageComponent } from './components/SearchPage'; export const rootRouteRef = createRouteRef({ path: '/search', title: 'search', }); -export const plugin = createPlugin({ +export const searchPlugin = createPlugin({ id: 'search', register({ router }) { - router.addRoute(rootRouteRef, SearchPage); + router.addRoute(rootRouteRef, SearchPageComponent); + }, + routes: { + root: rootRouteRef, }, }); + +export const SearchPage = searchPlugin.provide( + createRoutableExtension({ + component: () => import('./components/SearchPage').then(m => m.SearchPage), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 92d88ece8a..698f72020c 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-sentry +## 0.3.4 + +### Patch Changes + +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.3.3 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 38e1c55ed0..06b538289b 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.3.3", + "version": "0.3.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -46,8 +46,8 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index c3be5e7415..c422f9d72e 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,28 @@ # @backstage/plugin-sonarqube +## 0.1.10 + +### Patch Changes + +- 8dfdec613: Migrate to new composability API, exporting the plugin as `sonarQubePlugin` and card as `EntitySonarQubeCard`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.1.9 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index c286dd9dac..a31ee320f2 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.9", + "version": "0.1.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,10 +32,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/plugin-catalog-react": "^0.0.1", - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -47,8 +47,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 9325827eb9..49a5f8bfcd 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-tech-radar +## 0.3.4 + +### Patch Changes + +- 90c8f20b9: Fix mapping RadarEntry and Entry for moved and url attributes + Fix clicking of links in the radar legend +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + ## 0.3.3 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 9014d52931..a5c9f7bb53 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-tech-radar", - "version": "0.3.3", + "version": "0.3.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -43,8 +43,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index fab57f2a00..28ba4e032c 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,23 @@ # @backstage/plugin-techdocs-backend +## 0.5.5 + +### Patch Changes + +- c777df180: 1. Added option to use Azure Blob Storage as a choice to store the static generated files for TechDocs. +- e44925723: `techdocs.requestUrl` and `techdocs.storageUrl` are now optional configs and the discovery API will be used to get the URL where techdocs plugin is hosted. +- Updated dependencies [c777df180] +- Updated dependencies [2430ee7c2] +- Updated dependencies [6e612ce25] +- Updated dependencies [e44925723] +- Updated dependencies [025e122c3] +- Updated dependencies [7881f2117] +- Updated dependencies [f0320190d] +- Updated dependencies [11cb5ef94] + - @backstage/techdocs-common@0.3.7 + - @backstage/backend-common@0.5.2 + - @backstage/catalog-model@0.7.1 + ## 0.5.4 ### Patch Changes diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index b51d86138a..7ec1162d03 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -14,17 +14,12 @@ * limitations under the License. */ /** - * techdocs schema below is an abstract of what's used within techdocs-backend and for its visibility - * to view the complete techdoc schema please refer: plugins/techdocs/config.d.ts + * TechDocs schema below is an abstract of what's used within techdocs-backend and for its visibility + * to view the complete TechDocs schema please refer: plugins/techdocs/config.d.ts * */ export interface Config { /** Configuration options for the techdocs-backend plugin */ techdocs: { - /** - * attr: 'storageUrl' - accepts a string value - * e.g. storageUrl: http://localhost:7000/api/techdocs/static/docs - */ - storageUrl: string; /** * documentation building process depends on the builder attr * attr: 'builder' - accepts a string value @@ -45,5 +40,11 @@ export interface Config { */ type: 'local' | 'googleGcs' | 'awsS3'; }; + /** + * attr: 'storageUrl' - accepts a string value + * e.g. storageUrl: http://localhost:7000/api/techdocs/static/docs + * @deprecated + */ + storageUrl?: string; }; } diff --git a/plugins/techdocs-backend/package.json b/plugins/techdocs-backend/package.json index e6af442d20..36e5bd3bcd 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.5.4", + "version": "0.5.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,10 +30,10 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.1", - "@backstage/catalog-model": "^0.7.0", + "@backstage/backend-common": "^0.5.2", + "@backstage/catalog-model": "^0.7.1", "@backstage/config": "^0.1.2", - "@backstage/techdocs-common": "^0.3.6", + "@backstage/techdocs-common": "^0.3.7", "@types/dockerode": "^3.2.1", "@types/express": "^4.17.6", "cross-fetch": "^3.0.6", @@ -45,7 +45,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.5.0", + "@backstage/cli": "^0.6.0", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index dc5e6a4911..fe7b52ab52 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -102,7 +102,9 @@ export async function createRouter({ router.get('/docs/:namespace/:kind/:name/*', async (req, res) => { const { kind, namespace, name } = req.params; - const storageUrl = config.getString('techdocs.storageUrl'); + const storageUrl = + config.getOptionalString('techdocs.storageUrl') ?? + `${await discovery.getBaseUrl('techdocs')}/static/docs`; const catalogUrl = await discovery.getBaseUrl('catalog'); const triple = [kind, namespace, name].map(encodeURIComponent).join('/'); diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index 5d2d04aaee..69fe41dd32 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,35 @@ # @backstage/plugin-techdocs +## 0.5.5 + +### Patch Changes + +- 5fa3bdb55: Add `href` in addition to `onClick` to `ItemCard`. Ensure that the height of a + `ItemCard` with and without tags is equal. +- e44925723: `techdocs.requestUrl` and `techdocs.storageUrl` are now optional configs and the discovery API will be used to get the URL where techdocs plugin is hosted. +- 019fe39a0: Switch dependency from `@backstage/plugin-catalog` to `@backstage/plugin-catalog-react`. +- Updated dependencies [c777df180] +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [7fc89bae2] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [6e612ce25] +- Updated dependencies [e44925723] +- Updated dependencies [025e122c3] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [7881f2117] +- Updated dependencies [f0320190d] +- Updated dependencies [54c7d02f7] +- Updated dependencies [11cb5ef94] + - @backstage/techdocs-common@0.3.7 + - @backstage/core@0.6.0 + - @backstage/plugin-catalog-react@0.0.2 + - @backstage/theme@0.2.3 + - @backstage/catalog-model@0.7.1 + ## 0.5.4 ### Patch Changes diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index 0af3ab4290..1104d75237 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -17,17 +17,6 @@ export interface Config { /** Configuration options for the techdocs plugin */ techdocs: { - /** - * attr: 'requestUrl' - accepts a string value - * e.g. requestUrl: http://localhost:7000/api/techdocs - * @visibility frontend - */ - requestUrl: string; - /** - * attr: 'storageUrl' - accepts a string value - * e.g. storageUrl: http://localhost:7000/api/techdocs/static/docs - */ - storageUrl: string; /** * documentation building process depends on the builder attr * attr: 'builder' - accepts a string value @@ -184,5 +173,18 @@ export interface Config { credentials?: string; }; }; + /** + * attr: 'requestUrl' - accepts a string value + * e.g. requestUrl: http://localhost:7000/api/techdocs + * @visibility frontend + * @deprecated + */ + requestUrl?: string; + /** + * attr: 'storageUrl' - accepts a string value + * e.g. storageUrl: http://localhost:7000/api/techdocs/static/docs + * @deprecated + */ + storageUrl?: string; }; } diff --git a/plugins/techdocs/dev/api.ts b/plugins/techdocs/dev/api.ts index ae2b8c58ae..7d0679f6f6 100644 --- a/plugins/techdocs/dev/api.ts +++ b/plugins/techdocs/dev/api.ts @@ -13,20 +13,38 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { DiscoveryApi } from '@backstage/core'; +import { Config } from '@backstage/config'; import { EntityName } from '@backstage/catalog-model'; import { TechDocsStorage } from '../src/api'; export class TechDocsDevStorageApi implements TechDocsStorage { - public apiOrigin: string; + public configApi: Config; + public discoveryApi: DiscoveryApi; - constructor({ apiOrigin }: { apiOrigin: string }) { - this.apiOrigin = apiOrigin; + constructor({ + configApi, + discoveryApi, + }: { + configApi: Config; + discoveryApi: DiscoveryApi; + }) { + this.configApi = configApi; + this.discoveryApi = discoveryApi; + } + + async getApiOrigin() { + return ( + this.configApi.getOptionalString('techdocs.requestUrl') ?? + (await this.discoveryApi.getBaseUrl('techdocs')) + ); } async getEntityDocs(entityId: EntityName, path: string) { const { name } = entityId; - const url = `${this.apiOrigin}/${name}/${path}`; + const apiOrigin = await this.getApiOrigin(); + const url = `${apiOrigin}/${name}/${path}`; const request = await fetch( `${url.endsWith('/') ? url : `${url}/`}index.html`, @@ -39,8 +57,13 @@ export class TechDocsDevStorageApi implements TechDocsStorage { return request.text(); } - getBaseUrl(oldBaseUrl: string, entityId: EntityName, path: string): string { + async getBaseUrl( + oldBaseUrl: string, + entityId: EntityName, + path: string, + ): Promise { const { name } = entityId; - return new URL(oldBaseUrl, `${this.apiOrigin}/${name}/${path}`).toString(); + const apiOrigin = await this.getApiOrigin(); + return new URL(oldBaseUrl, `${apiOrigin}/${name}/${path}`).toString(); } } diff --git a/plugins/techdocs/dev/index.tsx b/plugins/techdocs/dev/index.tsx index 12974ef8b7..4cf74c9622 100644 --- a/plugins/techdocs/dev/index.tsx +++ b/plugins/techdocs/dev/index.tsx @@ -14,19 +14,21 @@ * limitations under the License. */ +import { configApiRef, discoveryApiRef } from '@backstage/core'; import { createDevApp } from '@backstage/dev-utils'; -import { plugin } from '../src/plugin'; +import { techdocsPlugin } from '../src/plugin'; import { TechDocsDevStorageApi } from './api'; import { techdocsStorageApiRef } from '../src'; createDevApp() .registerApi({ api: techdocsStorageApiRef, - deps: {}, - factory: () => + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => new TechDocsDevStorageApi({ - apiOrigin: 'http://localhost:3000/api', + configApi, + discoveryApi, }), }) - .registerPlugin(plugin) + .registerPlugin(techdocsPlugin) .render(); diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 90c8fd3840..ad9e1f7bef 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.5.4", + "version": "0.5.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,12 +31,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.0", - "@backstage/core": "^0.5.0", - "@backstage/plugin-catalog-react": "^0.0.1", + "@backstage/config": "^0.1.2", + "@backstage/catalog-model": "^0.7.1", + "@backstage/core": "^0.6.0", + "@backstage/plugin-catalog-react": "^0.0.2", "@backstage/test-utils": "^0.1.6", - "@backstage/theme": "^0.2.2", - "@backstage/techdocs-common": "^0.3.6", + "@backstage/theme": "^0.2.3", + "@backstage/techdocs-common": "^0.3.7", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -49,8 +50,8 @@ "sanitize-html": "^1.27.0" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/techdocs/src/Router.tsx b/plugins/techdocs/src/Router.tsx index 262f542ee6..b220912174 100644 --- a/plugins/techdocs/src/Router.tsx +++ b/plugins/techdocs/src/Router.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { Entity } from '@backstage/catalog-model'; +import { useEntity } from '@backstage/plugin-catalog-react'; import { Route, Routes } from 'react-router-dom'; import { MissingAnnotationEmptyState } from '@backstage/core'; import { @@ -38,7 +39,14 @@ export const Router = () => { ); }; -export const EmbeddedDocsRouter = ({ entity }: { entity: Entity }) => { +type Props = { + /** @deprecated The entity is now grabbed from context instead */ + entity?: Entity; +}; + +export const EmbeddedDocsRouter = (_props: Props) => { + const { entity } = useEntity(); + const projectId = entity.metadata.annotations?.[TECHDOCS_ANNOTATION]; if (!projectId) { diff --git a/plugins/techdocs/src/api.test.ts b/plugins/techdocs/src/api.test.ts index 132976e280..734650d7c8 100644 --- a/plugins/techdocs/src/api.test.ts +++ b/plugins/techdocs/src/api.test.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { Config } from '@backstage/config'; +import { UrlPatternDiscovery } from '@backstage/core'; import { TechDocsStorageApi } from './api'; -const DOC_STORAGE_URL = 'https://example-storage.com'; - const mockEntity = { kind: 'Component', namespace: 'default', @@ -24,19 +24,31 @@ const mockEntity = { }; describe('TechDocsStorageApi', () => { - it('should return correct base url based on defined storage', () => { - const storageApi = new TechDocsStorageApi({ apiOrigin: DOC_STORAGE_URL }); + const mockBaseUrl = 'http://backstage:9191/api/techdocs'; + const configApi = { + getOptionalString: () => 'http://backstage:9191/api/techdocs', + } as Partial; + const discoveryApi = UrlPatternDiscovery.compile(mockBaseUrl); - expect(storageApi.getBaseUrl('test.js', mockEntity, '')).toEqual( - `${DOC_STORAGE_URL}/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test.js`, + it('should return correct base url based on defined storage', async () => { + // @ts-ignore Partial not assignable to Config. + const storageApi = new TechDocsStorageApi({ configApi, discoveryApi }); + + await expect( + storageApi.getBaseUrl('test.js', mockEntity, ''), + ).resolves.toEqual( + `${mockBaseUrl}/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test.js`, ); }); - it('should return base url with correct entity structure', () => { - const storageApi = new TechDocsStorageApi({ apiOrigin: DOC_STORAGE_URL }); + it('should return base url with correct entity structure', async () => { + // @ts-ignore Partial not assignable to Config. + const storageApi = new TechDocsStorageApi({ configApi, discoveryApi }); - expect(storageApi.getBaseUrl('test/', mockEntity, '')).toEqual( - `${DOC_STORAGE_URL}/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test/`, + await expect( + storageApi.getBaseUrl('test/', mockEntity, ''), + ).resolves.toEqual( + `${mockBaseUrl}/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test/`, ); }); }); diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 3a950aeeb3..7f77be44e2 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -14,7 +14,8 @@ * limitations under the License. */ -import { createApiRef } from '@backstage/core'; +import { createApiRef, DiscoveryApi } from '@backstage/core'; +import { Config } from '@backstage/config'; import { EntityName } from '@backstage/catalog-model'; import { TechDocsMetadata } from './types'; @@ -30,7 +31,11 @@ export const techdocsApiRef = createApiRef({ export interface TechDocsStorage { getEntityDocs(entityId: EntityName, path: string): Promise; - getBaseUrl(oldBaseUrl: string, entityId: EntityName, path: string): string; + getBaseUrl( + oldBaseUrl: string, + entityId: EntityName, + path: string, + ): Promise; } export interface TechDocs { @@ -44,10 +49,25 @@ export interface TechDocs { * @property {string} apiOrigin Set to techdocs.requestUrl as the URL for techdocs-backend API */ export class TechDocsApi implements TechDocs { - public apiOrigin: string; + public configApi: Config; + public discoveryApi: DiscoveryApi; - constructor({ apiOrigin }: { apiOrigin: string }) { - this.apiOrigin = apiOrigin; + constructor({ + configApi, + discoveryApi, + }: { + configApi: Config; + discoveryApi: DiscoveryApi; + }) { + this.configApi = configApi; + this.discoveryApi = discoveryApi; + } + + async getApiOrigin() { + return ( + this.configApi.getOptionalString('techdocs.requestUrl') ?? + (await this.discoveryApi.getBaseUrl('techdocs')) + ); } /** @@ -62,7 +82,8 @@ export class TechDocsApi implements TechDocs { async getTechDocsMetadata(entityId: EntityName) { const { kind, namespace, name } = entityId; - const requestUrl = `${this.apiOrigin}/metadata/techdocs/${namespace}/${kind}/${name}`; + const apiOrigin = await this.getApiOrigin(); + const requestUrl = `${apiOrigin}/metadata/techdocs/${namespace}/${kind}/${name}`; const request = await fetch(`${requestUrl}`); const res = await request.json(); @@ -81,7 +102,8 @@ export class TechDocsApi implements TechDocs { async getEntityMetadata(entityId: EntityName) { const { kind, namespace, name } = entityId; - const requestUrl = `${this.apiOrigin}/metadata/entity/${namespace}/${kind}/${name}`; + const apiOrigin = await this.getApiOrigin(); + const requestUrl = `${apiOrigin}/metadata/entity/${namespace}/${kind}/${name}`; const request = await fetch(`${requestUrl}`); const res = await request.json(); @@ -96,10 +118,25 @@ export class TechDocsApi implements TechDocs { * @property {string} apiOrigin Set to techdocs.requestUrl as the URL for techdocs-backend API */ export class TechDocsStorageApi implements TechDocsStorage { - public apiOrigin: string; + public configApi: Config; + public discoveryApi: DiscoveryApi; - constructor({ apiOrigin }: { apiOrigin: string }) { - this.apiOrigin = apiOrigin; + constructor({ + configApi, + discoveryApi, + }: { + configApi: Config; + discoveryApi: DiscoveryApi; + }) { + this.configApi = configApi; + this.discoveryApi = discoveryApi; + } + + async getApiOrigin() { + return ( + this.configApi.getOptionalString('techdocs.requestUrl') ?? + (await this.discoveryApi.getBaseUrl('techdocs')) + ); } /** @@ -113,31 +150,47 @@ export class TechDocsStorageApi implements TechDocsStorage { async getEntityDocs(entityId: EntityName, path: string) { const { kind, namespace, name } = entityId; - const url = `${this.apiOrigin}/docs/${namespace}/${kind}/${name}/${path}`; + const apiOrigin = await this.getApiOrigin(); + const url = `${apiOrigin}/docs/${namespace}/${kind}/${name}/${path}`; const request = await fetch( `${url.endsWith('/') ? url : `${url}/`}index.html`, ); - if (request.status === 404) { - let errorMessage = 'Page not found. '; - // path is empty for the home page of an entity's docs site - if (!path) { - errorMessage += - 'This could be because there is no index.md file in the root of the docs directory of this repository.'; - } - throw new Error(errorMessage); + let errorMessage = ''; + switch (request.status) { + case 404: + errorMessage = 'Page not found. '; + // path is empty for the home page of an entity's docs site + if (!path) { + errorMessage += + 'This could be because there is no index.md file in the root of the docs directory of this repository.'; + } + throw new Error(errorMessage); + case 500: + errorMessage = + 'Could not generate documentation or an error in the TechDocs backend. '; + throw new Error(errorMessage); + default: + // Do nothing + break; } return request.text(); } - getBaseUrl(oldBaseUrl: string, entityId: EntityName, path: string): string { + async getBaseUrl( + oldBaseUrl: string, + entityId: EntityName, + path: string, + ): Promise { const { kind, namespace, name } = entityId; + const apiOrigin = await this.getApiOrigin(); + return new URL( oldBaseUrl, - `${this.apiOrigin}/docs/${namespace}/${kind}/${name}/${path}`, + `${apiOrigin}/docs/${namespace}/${kind}/${name}/${path}`, ).toString(); } } diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index 1726e0daf3..30c143e31f 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -14,7 +14,12 @@ * limitations under the License. */ -export { plugin } from './plugin'; +export { + techdocsPlugin, + techdocsPlugin as plugin, + TechdocsPage, + EntityTechdocsContent, +} from './plugin'; export { Router, EmbeddedDocsRouter } from './Router'; export * from './reader'; export * from './api'; diff --git a/plugins/techdocs/src/plugin.test.ts b/plugins/techdocs/src/plugin.test.ts index e750be9ac3..52f072627c 100644 --- a/plugins/techdocs/src/plugin.test.ts +++ b/plugins/techdocs/src/plugin.test.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -import { plugin } from './plugin'; +import { techdocsPlugin } from './plugin'; describe('techdocs', () => { it('should export plugin', () => { - expect(plugin).toBeDefined(); + expect(techdocsPlugin).toBeDefined(); }); }); diff --git a/plugins/techdocs/src/plugin.ts b/plugins/techdocs/src/plugin.ts index 576ba0a292..0b4063c237 100644 --- a/plugins/techdocs/src/plugin.ts +++ b/plugins/techdocs/src/plugin.ts @@ -34,6 +34,8 @@ import { createRouteRef, createApiFactory, configApiRef, + discoveryApiRef, + createRoutableExtension, } from '@backstage/core'; import { techdocsStorageApiRef, @@ -57,25 +59,44 @@ export const rootCatalogDocsRouteRef = createRouteRef({ title: 'Docs', }); -// TODO: Use discovery API for frontend to get URL for techdocs-backend instead of requestUrl -export const plugin = createPlugin({ +export const techdocsPlugin = createPlugin({ id: 'techdocs', apis: [ createApiFactory({ api: techdocsStorageApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => new TechDocsStorageApi({ - apiOrigin: configApi.getString('techdocs.requestUrl'), + configApi, + discoveryApi, }), }), createApiFactory({ api: techdocsApiRef, - deps: { configApi: configApiRef }, - factory: ({ configApi }) => + deps: { configApi: configApiRef, discoveryApi: discoveryApiRef }, + factory: ({ configApi, discoveryApi }) => new TechDocsApi({ - apiOrigin: configApi.getString('techdocs.requestUrl'), + configApi, + discoveryApi, }), }), ], + routes: { + root: rootRouteRef, + entityContent: rootCatalogDocsRouteRef, + }, }); + +export const TechdocsPage = techdocsPlugin.provide( + createRoutableExtension({ + component: () => import('./Router').then(m => m.Router), + mountPoint: rootRouteRef, + }), +); + +export const EntityTechdocsContent = techdocsPlugin.provide( + createRoutableExtension({ + component: () => import('./Router').then(m => m.EmbeddedDocsRouter), + mountPoint: rootCatalogDocsRouteRef, + }), +); diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 1264e2b6eb..c651f1ec13 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -129,7 +129,7 @@ export const Reader = ({ entityId, onReady }: Props) => { }, }), onCssReady({ - docStorageUrl: techdocsStorageApi.apiOrigin, + docStorageUrl: techdocsStorageApi.getApiOrigin(), onLoading: (dom: Element) => { (dom as HTMLElement).style.setProperty('opacity', '0'); }, @@ -155,7 +155,9 @@ export const Reader = ({ entityId, onReady }: Props) => { ]); if (error) { - return ; + // TODO Enhance API call to return customize error objects so we can identify which we ran into + // For now this defaults to display error code 404 + return ; } return ( diff --git a/plugins/techdocs/src/reader/components/TechDocsHome.tsx b/plugins/techdocs/src/reader/components/TechDocsHome.tsx index ed0eb68ebc..d1841c3bba 100644 --- a/plugins/techdocs/src/reader/components/TechDocsHome.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsHome.tsx @@ -27,13 +27,12 @@ import { import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { Grid } from '@material-ui/core'; import React from 'react'; -import { generatePath, useNavigate } from 'react-router-dom'; +import { generatePath } from 'react-router-dom'; import { useAsync } from 'react-use'; import { rootDocsRouteRef } from '../../plugin'; export const TechDocsHome = () => { const catalogApi = useApi(catalogApiRef); - const navigate = useNavigate(); const { value, loading, error } = useAsync(async () => { const response = await catalogApi.getEntities(); @@ -87,15 +86,11 @@ export const TechDocsHome = () => { ? value.map((entity, index: number) => ( - navigate( - generatePath(rootDocsRouteRef.path, { - namespace: entity.metadata.namespace ?? 'default', - kind: entity.kind, - name: entity.metadata.name, - }), - ) - } + href={generatePath(rootDocsRouteRef.path, { + namespace: entity.metadata.namespace ?? 'default', + kind: entity.kind, + name: entity.metadata.name, + })} title={entity.metadata.name} label="Read Docs" description={entity.metadata.description} diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index d3c98fec78..c6562ec8ad 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -41,3 +41,20 @@ describe('', ( expect(rendered.getByTestId('go-back-link')).toBeDefined(); }); }); + +describe('', () => { + it('should render with a custom status code, custom error message and go back link', () => { + const rendered = render( + wrapInTestApp( + , + ), + ); + rendered.getByText(/This is a custom error message/i); + rendered.getByText(/500/i); + rendered.getByText(/Looks like someone dropped the mic!/i); + expect(rendered.getByTestId('go-back-link')).toBeDefined(); + }); +}); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx index cdacc1cb7e..04f2106161 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx @@ -19,9 +19,10 @@ import { ErrorPage, useApi, configApiRef } from '@backstage/core'; type Props = { errorMessage?: string; + statusCode?: number; }; -export const TechDocsNotFound = ({ errorMessage }: Props) => { +export const TechDocsNotFound = ({ errorMessage, statusCode }: Props) => { const techdocsBuilder = useApi(configApiRef).getOptionalString( 'techdocs.builder', ); @@ -37,7 +38,7 @@ export const TechDocsNotFound = ({ errorMessage }: Props) => { return ( diff --git a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx index bc84f792a7..4b578bef38 100644 --- a/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsPage.test.tsx @@ -56,7 +56,8 @@ describe('', () => { }; const techdocsStorageApi: Partial = { getEntityDocs: (): Promise => Promise.resolve('String'), - getBaseUrl: (): string => '', + getBaseUrl: (): Promise => Promise.resolve('String'), + getApiOrigin: (): Promise => Promise.resolve('String'), }; const apiRegistry = ApiRegistry.from([ diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index 728c39826d..10c7191b63 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -21,7 +21,7 @@ import { TechDocsStorage } from '../../api'; const DOC_STORAGE_URL = 'https://example-host.storage.googleapis.com'; const techdocsStorageApi: TechDocsStorage = { - getBaseUrl: jest.fn(() => DOC_STORAGE_URL), + getBaseUrl: jest.fn(() => Promise.resolve(DOC_STORAGE_URL)), getEntityDocs: () => new Promise(resolve => resolve('yes!')), }; diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts index 8d9d5a7294..2872ad36cc 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.ts @@ -35,12 +35,12 @@ export const addBaseUrl = ({ ): void => { Array.from(list) .filter(elem => !!elem.getAttribute(attributeName)) - .forEach((elem: T) => { + .forEach(async (elem: T) => { const elemAttribute = elem.getAttribute(attributeName); if (!elemAttribute) return; elem.setAttribute( attributeName, - techdocsStorageApi.getBaseUrl(elemAttribute, entityId, path), + await techdocsStorageApi.getBaseUrl(elemAttribute, entityId, path), ); }); }; diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts index 67ae38c56b..1fbc59e213 100644 --- a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts +++ b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts @@ -22,8 +22,9 @@ import { } from '../../test-utils'; import { onCssReady } from '../transformers'; -const docStorageUrl: string = - 'https://techdocs-mock-sites.storage.googleapis.com'; +const docStorageUrl: Promise = Promise.resolve( + 'https://techdocs-mock-sites.storage.googleapis.com', +); const fixture = ` diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.ts b/plugins/techdocs/src/reader/transformers/onCssReady.ts index dd50879459..50dbe52cbd 100644 --- a/plugins/techdocs/src/reader/transformers/onCssReady.ts +++ b/plugins/techdocs/src/reader/transformers/onCssReady.ts @@ -17,7 +17,7 @@ import type { Transformer } from './index'; type OnCssReadyOptions = { - docStorageUrl: string; + docStorageUrl: Promise; onLoading: (dom: Element) => void; onLoaded: (dom: Element) => void; }; @@ -30,7 +30,9 @@ export const onCssReady = ({ return dom => { const cssPages = Array.from( dom.querySelectorAll('head > link[rel="stylesheet"]'), - ).filter(elem => elem.getAttribute('href')?.startsWith(docStorageUrl)); + ).filter(async elem => + elem.getAttribute('href')?.startsWith(await docStorageUrl), + ); let count = cssPages.length; diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index c9abbf2747..467f207754 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/plugin-user-settings +## 0.2.5 + +### Patch Changes + +- bc5082a00: Migrate to new composability API, exporting the plugin as `userSettingsPlugin` and the page as `UserSettingsPage`. +- de98c32ed: Keep the Pin Sidebar setting visible on small screens. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + ## 0.2.4 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 78790a891e..88453e8efb 100644 --- a/plugins/user-settings/package.json +++ b/plugins/user-settings/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-user-settings", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,8 +41,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/plugins/welcome/CHANGELOG.md b/plugins/welcome/CHANGELOG.md index a1eaf43dc4..594d30226a 100644 --- a/plugins/welcome/CHANGELOG.md +++ b/plugins/welcome/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-welcome +## 0.2.5 + +### Patch Changes + +- 8dfdec613: Migrated to new composability API, exporting the plugin as `welcomePlugin` and the page as `WelcomePage`. +- Updated dependencies [12ece98cd] +- Updated dependencies [d82246867] +- Updated dependencies [c810082ae] +- Updated dependencies [5fa3bdb55] +- Updated dependencies [21e624ba9] +- Updated dependencies [da9f53c60] +- Updated dependencies [32c95605f] +- Updated dependencies [54c7d02f7] + - @backstage/core@0.6.0 + - @backstage/theme@0.2.3 + ## 0.2.4 ### Patch Changes diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 9482e34743..f3d50dc675 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-welcome", - "version": "0.2.4", + "version": "0.2.5", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -30,8 +30,8 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.5.0", - "@backstage/theme": "^0.2.2", + "@backstage/core": "^0.6.0", + "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -41,8 +41,8 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.5.0", - "@backstage/dev-utils": "^0.1.8", + "@backstage/cli": "^0.6.0", + "@backstage/dev-utils": "^0.1.9", "@backstage/test-utils": "^0.1.6", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/yarn.lock b/yarn.lock index aac0d3735b..05282e5285 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2583,7 +2583,7 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.7.0" + version "0.7.1" dependencies: "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" @@ -2595,7 +2595,7 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.7.0" + version "0.7.1" dependencies: "@backstage/config" "^0.1.2" "@types/json-schema" "^7.0.5" @@ -2607,11 +2607,11 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.5.0" + version "0.6.0" dependencies: "@backstage/config" "^0.1.2" "@backstage/core-api" "^0.2.8" - "@backstage/theme" "^0.2.2" + "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@material-ui/lab" "4.0.0-alpha.45" @@ -2645,6 +2645,54 @@ remark-gfm "^1.0.0" zen-observable "^0.8.15" +"@backstage/plugin-catalog@^0.2.0": + version "0.3.0" + dependencies: + "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.0" + "@backstage/plugin-catalog-react" "^0.0.2" + "@backstage/plugin-scaffolder" "^0.4.2" + "@backstage/theme" "^0.2.3" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@types/react" "^16.9" + classnames "^2.2.6" + git-url-parse "^11.4.4" + moment "^2.26.0" + react "^16.13.1" + react-dom "^16.13.1" + react-helmet "6.1.0" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + swr "^0.3.0" + +"@backstage/plugin-catalog@^0.2.1": + version "0.3.0" + dependencies: + "@backstage/catalog-client" "^0.3.5" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.0" + "@backstage/plugin-catalog-react" "^0.0.2" + "@backstage/plugin-scaffolder" "^0.4.2" + "@backstage/theme" "^0.2.3" + "@material-ui/core" "^4.11.0" + "@material-ui/icons" "^4.9.1" + "@material-ui/lab" "4.0.0-alpha.45" + "@types/react" "^16.9" + classnames "^2.2.6" + git-url-parse "^11.4.4" + moment "^2.26.0" + react "^16.13.1" + react-dom "^16.13.1" + react-helmet "6.1.0" + react-router "6.0.0-beta.0" + react-router-dom "6.0.0-beta.0" + react-use "^15.3.3" + swr "^0.3.0" + "@bcoe/v8-coverage@^0.2.3": version "0.2.3" resolved "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" @@ -2655,53 +2703,54 @@ resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-5.0.0.tgz#3ba791f37b90e7f6170d252b63aacfcae943c039" integrity sha512-WmKrB/575EJCzbeSJR3YQ5sET5FaizeljLRw1382qVUeGqzuWBgIS+AF5a0FO51uQTrDpoRgvuHC2IWVsgwkkA== -"@changesets/apply-release-plan@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-4.0.0.tgz#e78efb56a4e459a8dab814ba43045f2ace0f27c9" - integrity sha512-MrcUd8wIlQ4S/PznzqJVsmnEpUGfPEkCGF54iqt8G05GEqi/zuxpoTfebcScpj5zeiDyxFIcA9RbeZ3pvJJxoA== +"@changesets/apply-release-plan@^4.2.0": + version "4.2.0" + resolved "https://registry.npmjs.org/@changesets/apply-release-plan/-/apply-release-plan-4.2.0.tgz#f1005815f27c3238f66507e90c6ae14d8fc62b41" + integrity sha512-/vt6UwgQldhOw93Gb8llI5OuYGlJt2+U45AfcXsoxzl8gZzCmChGm3vUaQJYbmtL8TbL8OOVXHRIKJJidMNPKw== dependencies: - "@babel/runtime" "^7.4.4" - "@changesets/config" "^1.2.0" + "@babel/runtime" "^7.10.4" + "@changesets/config" "^1.5.0" "@changesets/get-version-range-type" "^0.3.2" "@changesets/git" "^1.0.5" - "@changesets/types" "^3.1.0" + "@changesets/types" "^3.3.0" "@manypkg/get-packages" "^1.0.1" + detect-indent "^6.0.0" fs-extra "^7.0.1" lodash.startcase "^4.4.0" outdent "^0.5.0" - prettier "^1.18.2" + prettier "^1.19.1" resolve-from "^5.0.0" semver "^5.4.1" -"@changesets/assemble-release-plan@^4.0.0": - version "4.0.0" - resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-4.0.0.tgz#60c2392c0e2c99f24778ab3a5c8e8c80ddaaaa59" - integrity sha512-3Kv21FNvysTQvZs3fHr6aZeDibhZHtgI1++fMZplzVtwNVmpjow3zv9lcZmJP26LthbpVH3I8+nqlU7M43lfWA== +"@changesets/assemble-release-plan@^4.0.0", "@changesets/assemble-release-plan@^4.1.0": + version "4.1.0" + resolved "https://registry.npmjs.org/@changesets/assemble-release-plan/-/assemble-release-plan-4.1.0.tgz#091e5e768dfe51835937e71d1ebaca1c9d6de55b" + integrity sha512-dMqe2L5Pn4UGTW89kOuuCuZD3pQFZj1Sxv92ZW4S98sXGsxcb2PdW+PeHbQ7tawkCYCOvzhXxAlN4OdF2DlDKQ== dependencies: "@babel/runtime" "^7.10.4" "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.1.3" - "@changesets/types" "^3.1.0" + "@changesets/get-dependents-graph" "^1.2.0" + "@changesets/types" "^3.3.0" "@manypkg/get-packages" "^1.0.1" semver "^5.4.1" -"@changesets/cli@^2.11.0": - version "2.12.0" - resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.12.0.tgz#26124b051e6ce6dcc5aa4595588c8cb2ce3e4363" - integrity sha512-dGdFkg75zsaEObsG8gwMLglS6sJVjXWwgVTAzEIjqIoWVnKwqZqccTb4gn0noq47uCwy7SqxiAJqGibIy9UOKw== +"@changesets/cli@^2.14.0": + version "2.14.0" + resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.14.0.tgz#b8d1d33d832c640ce0b95333bbd8d5ac5b9c9824" + integrity sha512-rbQMRDXl1cXOglnjUvYyrFLlYBbS0YZdfxZfW3ZbGLzLoS4n50+B9fLSE9oW20hQuL3zAnyLyacb9cwNhF2lig== dependencies: "@babel/runtime" "^7.10.4" - "@changesets/apply-release-plan" "^4.0.0" - "@changesets/assemble-release-plan" "^4.0.0" - "@changesets/config" "^1.4.0" + "@changesets/apply-release-plan" "^4.2.0" + "@changesets/assemble-release-plan" "^4.1.0" + "@changesets/config" "^1.5.0" "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.1.3" + "@changesets/get-dependents-graph" "^1.2.0" "@changesets/get-release-plan" "^2.0.1" - "@changesets/git" "^1.0.6" + "@changesets/git" "^1.1.0" "@changesets/logger" "^0.0.5" "@changesets/pre" "^1.0.4" "@changesets/read" "^0.4.6" - "@changesets/types" "^3.2.0" + "@changesets/types" "^3.3.0" "@changesets/write" "^0.1.3" "@manypkg/get-packages" "^1.0.1" "@types/semver" "^6.0.0" @@ -2721,15 +2770,15 @@ term-size "^2.1.0" tty-table "^2.8.10" -"@changesets/config@^1.2.0", "@changesets/config@^1.4.0": - version "1.4.0" - resolved "https://registry.npmjs.org/@changesets/config/-/config-1.4.0.tgz#c157a4121f198b749f2bbc2e9015b6e976ece7d6" - integrity sha512-eoTOcJ6py7jBDY8rUXwEGxR5UtvUX+p//0NhkVpPGcnvIeITHq+DOIsuWyGzWcb+1FaYkof3CCr32/komZTu4Q== +"@changesets/config@^1.2.0", "@changesets/config@^1.5.0": + version "1.5.0" + resolved "https://registry.npmjs.org/@changesets/config/-/config-1.5.0.tgz#6d58b01e45916318d3f39e9cde86a5d69dc58ed8" + integrity sha512-Bl9nLVYcwFCpd9jpzcOsExZk1NuTYX20D2YWHCdS1xll3W0yOdSUlWLUCCfugN1l3+yTR6iDW6q9o6vpCevWvA== dependencies: "@changesets/errors" "^0.1.4" - "@changesets/get-dependents-graph" "^1.1.3" + "@changesets/get-dependents-graph" "^1.2.0" "@changesets/logger" "^0.0.5" - "@changesets/types" "^3.2.0" + "@changesets/types" "^3.3.0" "@manypkg/get-packages" "^1.0.1" fs-extra "^7.0.1" micromatch "^4.0.2" @@ -2741,12 +2790,12 @@ dependencies: extendable-error "^0.1.5" -"@changesets/get-dependents-graph@^1.1.3": - version "1.1.3" - resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.1.3.tgz#da959c43ce98f3a990a6b8d9c1f894bcc1b629c7" - integrity sha512-cTbySXwSv9yWp4Pp5R/b5Qv23wJgFaFCqUbsI3IJ2pyPl0vMaODAZS8NI1nNK2XSxGIg1tw+dWNSR4PlrKBSVQ== +"@changesets/get-dependents-graph@^1.2.0": + version "1.2.0" + resolved "https://registry.npmjs.org/@changesets/get-dependents-graph/-/get-dependents-graph-1.2.0.tgz#6986927a5fec60bc6fcc76f966efae2ac30c05ed" + integrity sha512-96NInEKpEZH8KvmXyh42PynXVAdq3kQ9VjAeswHtJ3umUCeTF42b/KVXaov+5P1RNnaUVtRuEwzs4syGuowDTw== dependencies: - "@changesets/types" "^3.0.0" + "@changesets/types" "^3.3.0" "@manypkg/get-packages" "^1.0.1" chalk "^2.1.0" fs-extra "^7.0.1" @@ -2770,10 +2819,10 @@ resolved "https://registry.npmjs.org/@changesets/get-version-range-type/-/get-version-range-type-0.3.2.tgz#8131a99035edd11aa7a44c341cbb05e668618c67" integrity sha512-SVqwYs5pULYjYT4op21F2pVbcrca4qA/bAA3FmFXKMN7Y+HcO8sbZUTx3TAy2VXulP2FACd1aC7f2nTuqSPbqg== -"@changesets/git@^1.0.5", "@changesets/git@^1.0.6": - version "1.0.6" - resolved "https://registry.npmjs.org/@changesets/git/-/git-1.0.6.tgz#057e627e5d3fcb74bf6c18d7284e130ba5a7632e" - integrity sha512-e0M06XuME3W5lGhz+CO0vLc60u+hLk/pYjOx/6GXEWuQrwtGgeycFIfRgRt8qTs664o1oKtVHBbd7ItpoWgFfA== +"@changesets/git@^1.0.5", "@changesets/git@^1.1.0": + version "1.1.0" + resolved "https://registry.npmjs.org/@changesets/git/-/git-1.1.0.tgz#ecaa8058ac87b450c09da0e74b68e6854cd0742c" + integrity sha512-f/2rQynT+JiAL/V0V/GQdXhLkcb86ELg3UwH3fQO4wVdfUbE9NHIHq9ohJdH1Ymh0Lv48F/b38aWZ5v2sKiF3w== dependencies: "@babel/runtime" "^7.10.4" "@changesets/errors" "^0.1.4" @@ -2790,9 +2839,9 @@ chalk "^2.1.0" "@changesets/parse@^0.3.6": - version "0.3.6" - resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.6.tgz#8c2c8480fc07d2db2c37469d4a8df10906a989c6" - integrity sha512-0XPd/es9CfogI7XIqDr7I2mWzm++xX2s9GZsij3GajPYd7ouEsgJyNatPooxNtqj6ZepkiD6uqlqbeBUyj/A0Q== + version "0.3.7" + resolved "https://registry.npmjs.org/@changesets/parse/-/parse-0.3.7.tgz#1368136e2b83d5cff11b4d383a3032723530db99" + integrity sha512-8yqKulslq/7V2VRBsJqPgjnZMoehYqhJm5lEOXJPZ2rcuSdyj8+p/2vq2vRDBJT2m0rP+C9G8DujsGYQIFZezw== dependencies: "@changesets/types" "^3.0.0" js-yaml "^3.13.1" @@ -2822,10 +2871,10 @@ fs-extra "^7.0.1" p-filter "^2.1.0" -"@changesets/types@^3.0.0", "@changesets/types@^3.1.0", "@changesets/types@^3.1.1", "@changesets/types@^3.2.0": - version "3.2.0" - resolved "https://registry.npmjs.org/@changesets/types/-/types-3.2.0.tgz#d8306d7219c3b19b6d860ddeb9d7374e2dd6b035" - integrity sha512-rAmPtOyXpisEEE25CchKNUAf2ApyAeuZ/h78YDoqKZaCk5tUD0lgYZGPIRV9WTPoqNjJULIym37ogc6pkax5jg== +"@changesets/types@^3.0.0", "@changesets/types@^3.1.0", "@changesets/types@^3.1.1", "@changesets/types@^3.3.0": + version "3.3.0" + resolved "https://registry.npmjs.org/@changesets/types/-/types-3.3.0.tgz#04cd8184b2d2da760667bd89bf9b930938dbd96e" + integrity sha512-rJamRo+OD/MQekImfIk07JZwYSB18iU6fYL8xOg0gfAiTh1a1+OlR1fPIxm55I7RsWw812is2YcPPwXdIewrhA== "@changesets/write@^0.1.3": version "0.1.3" @@ -7686,9 +7735,9 @@ integrity sha512-MBSp62AjB1KrSOI3gX9GekddXU5YYQAVA93+aSl78biBqoSzxg876aQY2KJK5Gnfbpqq7O2cadVX5kPAtBqIXw== "@types/zen-observable@^0.8.0": - version "0.8.0" - resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz#8b63ab7f1aa5321248aad5ac890a485656dcea4d" - integrity sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg== + version "0.8.2" + resolved "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.2.tgz#808c9fa7e4517274ed555fa158f2de4b4f468e71" + integrity sha512-HrCIVMLjE1MOozVoD86622S7aunluLb2PJdPfb3nYiEtohm8mIB/vyv0Fd37AdeMFrTUQXEunw78YloMA3Qilg== "@typescript-eslint/eslint-plugin@^v4.14.0": version "4.14.0" @@ -13770,12 +13819,12 @@ find-up@^5.0.0: locate-path "^6.0.0" path-exists "^4.0.0" -find-versions@^3.2.0: - version "3.2.0" - resolved "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz#10297f98030a786829681690545ef659ed1d254e" - integrity sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww== +find-versions@^4.0.0: + version "4.0.0" + resolved "https://registry.npmjs.org/find-versions/-/find-versions-4.0.0.tgz#3c57e573bf97769b8cb8df16934b627915da4965" + integrity sha512-wgpWy002tA+wgmO27buH/9KzyEOQnKsG/R0yrcjPT9BOFm0zRBVQbZ95nRGXWMywS8YR5knRbpohio0bcJABxQ== dependencies: - semver-regex "^2.0.0" + semver-regex "^3.1.2" find-yarn-workspace-root2@1.2.16: version "1.2.16" @@ -15381,17 +15430,17 @@ humanize-ms@^1.2.1: ms "^2.0.0" husky@^4.2.3: - version "4.3.6" - resolved "https://registry.npmjs.org/husky/-/husky-4.3.6.tgz#ebd9dd8b9324aa851f1587318db4cccb7665a13c" - integrity sha512-o6UjVI8xtlWRL5395iWq9LKDyp/9TE7XMOTvIpEVzW638UcGxTmV5cfel6fsk/jbZSTlvfGVJf2svFtybcIZag== + version "4.3.8" + resolved "https://registry.npmjs.org/husky/-/husky-4.3.8.tgz#31144060be963fd6850e5cc8f019a1dfe194296d" + integrity sha512-LCqqsB0PzJQ/AlCgfrfzRe3e3+NvmefAdKQhRYpxS4u6clblBoDdzzvHi8fmxKRzvMxPY/1WZWzomPZww0Anow== dependencies: chalk "^4.0.0" ci-info "^2.0.0" compare-versions "^3.6.0" cosmiconfig "^7.0.0" - find-versions "^3.2.0" + find-versions "^4.0.0" opencollective-postinstall "^2.0.2" - pkg-dir "^4.2.0" + pkg-dir "^5.0.0" please-upgrade-node "^3.2.0" slash "^3.0.0" which-pm-runs "^1.0.0" @@ -20713,6 +20762,13 @@ pkg-dir@^4.1.0, pkg-dir@^4.2.0: dependencies: find-up "^4.0.0" +pkg-dir@^5.0.0: + version "5.0.0" + resolved "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz#a02d6aebe6ba133a928f74aec20bafdfe6b8e760" + integrity sha512-NPE8TDbzl/3YQYY7CSS228s3g2ollTFnc+Qi3tqmqJp9Vg2ovUpixcJEo2HJScN2Ez+kEaal6y70c0ehqJBJeA== + dependencies: + find-up "^5.0.0" + pkg-up@3.1.0, pkg-up@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz#100ec235cc150e4fd42519412596a28512a0def5" @@ -21260,7 +21316,7 @@ prepend-http@^2.0.0: resolved "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz#e92434bfa5ea8c19f41cdfd401d741a3c819d897" integrity sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc= -prettier@^1.18.2: +prettier@^1.18.2, prettier@^1.19.1: version "1.19.1" resolved "https://registry.npmjs.org/prettier/-/prettier-1.19.1.tgz#f7d7f5ff8a9cd872a7be4ca142095956a60797cb" integrity sha512-s7PoyDv/II1ObgQunCbB9PdLmUcBZcnWOcxDh7O0N/UwDEsHyqkW+Qh28jW+mVuCdx7gLB0BotYI1Y6uI9iyew== @@ -23122,10 +23178,10 @@ rollup-plugin-postcss@^3.1.1: safe-identifier "^0.4.1" style-inject "^0.3.0" -rollup-plugin-typescript2@^0.27.3: - version "0.27.3" - resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.27.3.tgz#cd9455ac026d325b20c5728d2cc54a08a771b68b" - integrity sha512-gmYPIFmALj9D3Ga1ZbTZAKTXq1JKlTQBtj299DXhqYz9cL3g/AQfUvbb2UhH+Nf++cCq941W2Mv7UcrcgLzJJg== +rollup-plugin-typescript2@^0.29.0: + version "0.29.0" + resolved "https://registry.npmjs.org/rollup-plugin-typescript2/-/rollup-plugin-typescript2-0.29.0.tgz#b7ad83f5241dbc5bdf1e98d9c3fca005ffe39e1a" + integrity sha512-YytahBSZCIjn/elFugEGQR5qTsVhxhUwGZIsA9TmrSsC88qroGo65O5HZP/TTArH2dm0vUmYWhKchhwi2wL9bw== dependencies: "@rollup/pluginutils" "^3.1.0" find-cache-dir "^3.3.1" @@ -23334,10 +23390,10 @@ semver-diff@^3.1.1: dependencies: semver "^6.3.0" -semver-regex@^2.0.0: - version "2.0.0" - resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz#a93c2c5844539a770233379107b38c7b4ac9d338" - integrity sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw== +semver-regex@^3.1.2: + version "3.1.2" + resolved "https://registry.npmjs.org/semver-regex/-/semver-regex-3.1.2.tgz#34b4c0d361eef262e07199dbef316d0f2ab11807" + integrity sha512-bXWyL6EAKOJa81XG1OZ/Yyuq+oT0b2YLlxx7c+mrdYPaPbnj6WgVULXhinMIeZGufuUBu/eVRqXEhiv4imfwxA== "semver@2 || 3 || 4 || 5", "semver@2.x || 3.x || 4 || 5", semver@^5.3.0, semver@^5.4.1, semver@^5.5.0, semver@^5.5.1, semver@^5.6.0, semver@^5.7.0, semver@^5.7.1: version "5.7.1"