diff --git a/.changeset/afraid-dingos-own.md b/.changeset/afraid-dingos-own.md deleted file mode 100644 index 9f7010d330..0000000000 --- a/.changeset/afraid-dingos-own.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -pass registered logger to requestLoggingHandler diff --git a/.changeset/afraid-weeks-sort.md b/.changeset/afraid-weeks-sort.md new file mode 100644 index 0000000000..c5ec608daf --- /dev/null +++ b/.changeset/afraid-weeks-sort.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +k8s plugin now surfaces k8s components with only label selector query annotation. +Previously backstage.io/kubernetes-label-selector catalog entity annotation would only work if you also included backstage.io/kubernetes-id. +But backstage.io/kubernetes-id value was ignored diff --git a/.changeset/angry-walls-mate.md b/.changeset/angry-walls-mate.md deleted file mode 100644 index 5902168b2c..0000000000 --- a/.changeset/angry-walls-mate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/integration': minor ---- - -Make `ScmIntegration.resolveUrl` mandatory. diff --git a/.changeset/blue-lions-worry.md b/.changeset/blue-lions-worry.md deleted file mode 100644 index 911e1507b2..0000000000 --- a/.changeset/blue-lions-worry.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-sonarqube': patch ---- - -Fix bug retrieving current theme diff --git a/.changeset/brown-pumpkins-impress.md b/.changeset/brown-pumpkins-impress.md deleted file mode 100644 index 5748a9860b..0000000000 --- a/.changeset/brown-pumpkins-impress.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Refactored auth provider factories to accept options along with other internal refactoring of the auth providers. diff --git a/.changeset/chilly-cars-shout.md b/.changeset/chilly-cars-shout.md deleted file mode 100644 index 60871b07a3..0000000000 --- a/.changeset/chilly-cars-shout.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder': patch ---- - -Make the `TemplateCard` conform to what material-ui recommends in their examples. This fixes the extra padding around the buttons. diff --git a/.changeset/cuddly-bags-share.md b/.changeset/cuddly-bags-share.md deleted file mode 100644 index 6aff6a9404..0000000000 --- a/.changeset/cuddly-bags-share.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Hide the kind of the owner if it's the default kind for the `ownedBy` -relationship (group). diff --git a/.changeset/dry-llamas-wave.md b/.changeset/dry-llamas-wave.md deleted file mode 100644 index fea9b122a6..0000000000 --- a/.changeset/dry-llamas-wave.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Implement `UrlReader.search` for the other providers (Azure, Bitbucket, GitLab) as well. - -The `UrlReader` subclasses now are implemented in terms of the respective `Integration` class. diff --git a/.changeset/eight-doors-matter.md b/.changeset/eight-doors-matter.md deleted file mode 100644 index 7dbb56b6c3..0000000000 --- a/.changeset/eight-doors-matter.md +++ /dev/null @@ -1,19 +0,0 @@ ---- -'@backstage/core-api': patch -'@backstage/plugin-catalog': patch ---- - -Minor refactoring of BackstageApp.getSystemIcons to support custom registered -icons. Custom Icons can be added using: - -```tsx -import AlarmIcon from '@material-ui/icons/Alarm'; -import MyPersonIcon from './MyPerson'; - -const app = createApp({ - icons: { - user: MyPersonIcon // override system icon - alert: AlarmIcon, // Custom icon - }, -}); -``` diff --git a/.changeset/five-guests-promise.md b/.changeset/five-guests-promise.md deleted file mode 100644 index 1d4d0ae303..0000000000 --- a/.changeset/five-guests-promise.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Remove the "Move repository" menu entry from the catalog page, as it's just a placeholder. diff --git a/.changeset/fresh-seals-retire.md b/.changeset/fresh-seals-retire.md deleted file mode 100644 index f58eebef4e..0000000000 --- a/.changeset/fresh-seals-retire.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/core-api': patch -'@backstage/core': patch ---- - -Export `createExternalRouteRef`, as well as give it an `id` for easier debugging, and fix parameter requirements when used with `useRouteRef`. diff --git a/.changeset/grumpy-cups-hope.md b/.changeset/grumpy-cups-hope.md deleted file mode 100644 index 6445c07efa..0000000000 --- a/.changeset/grumpy-cups-hope.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-user-settings': patch ---- - -Use routed tabs to link to every settings page. diff --git a/.changeset/hungry-frogs-flow.md b/.changeset/hungry-frogs-flow.md new file mode 100644 index 0000000000..91a6ab0892 --- /dev/null +++ b/.changeset/hungry-frogs-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Consider both authentication methods for both `onprem` and `cloud` BitBucket diff --git a/.changeset/itchy-camels-grin.md b/.changeset/itchy-camels-grin.md deleted file mode 100644 index eac3c6a102..0000000000 --- a/.changeset/itchy-camels-grin.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-splunk-on-call': patch ---- - -Added splunk-on-call plugin. diff --git a/.changeset/many-bags-sort.md b/.changeset/many-bags-sort.md deleted file mode 100644 index dbc8df8b0c..0000000000 --- a/.changeset/many-bags-sort.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/backend-common': patch ---- - -Support globs in `FileReaderProcessor`. diff --git a/.changeset/mean-grapes-march.md b/.changeset/mean-grapes-march.md deleted file mode 100644 index 6065fe0892..0000000000 --- a/.changeset/mean-grapes-march.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/backend-common': patch -'@backstage/techdocs-common': patch -'@backstage/plugin-scaffolder-backend': patch ---- - -Switched to using `'x-access-token'` for authenticating Git over HTTPS towards GitHub. diff --git a/.changeset/metal-pianos-repeat.md b/.changeset/metal-pianos-repeat.md new file mode 100644 index 0000000000..e24320938a --- /dev/null +++ b/.changeset/metal-pianos-repeat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-kubernetes': patch +--- + +Support AWS auth provider on kubernetes FE plugin diff --git a/.changeset/new-mangos-tap.md b/.changeset/new-mangos-tap.md deleted file mode 100644 index 1b57510417..0000000000 --- a/.changeset/new-mangos-tap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/cli': patch ---- - -Tweak error message in lockfile parsing to include more information. diff --git a/.changeset/ninety-lemons-shake.md b/.changeset/ninety-lemons-shake.md deleted file mode 100644 index 960a4eaffe..0000000000 --- a/.changeset/ninety-lemons-shake.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Limit the props that are forwarded to the `Link` component in the `EntityRefLink`. diff --git a/.changeset/pink-coins-sniff.md b/.changeset/pink-coins-sniff.md deleted file mode 100644 index b9714f116a..0000000000 --- a/.changeset/pink-coins-sniff.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -'@backstage/plugin-api-docs': patch -'@backstage/plugin-catalog': patch -'@backstage/plugin-catalog-react': patch ---- - -Introduce new cards to `@backstage/plugin-catalog` that can be added to entity pages: - -- `EntityHasSystemsCard` to display systems of a domain. -- `EntityHasComponentsCard` to display components of a system. -- `EntityHasSubcomponentsCard` to display subcomponents of a subcomponent. -- In addition, `EntityHasApisCard` to display APIs of a system is added to `@backstage/plugin-api-docs`. - -`@backstage/plugin-catalog-react` now provides an `EntityTable` to build own cards for entities. -The styling of the tables and new cards was also applied to the existing `EntityConsumedApisCard`, -`EntityConsumingComponentsCard`, `EntityProvidedApisCard`, and `EntityProvidingComponentsCard`. diff --git a/.changeset/rich-apricots-lick.md b/.changeset/rich-apricots-lick.md deleted file mode 100644 index f9daf861d2..0000000000 --- a/.changeset/rich-apricots-lick.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Fixed parsing of OIDC key timestamps when using SQLite. diff --git a/.changeset/rich-socks-move.md b/.changeset/rich-socks-move.md new file mode 100644 index 0000000000..5aeb3121f1 --- /dev/null +++ b/.changeset/rich-socks-move.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-api': patch +'@backstage/core': patch +--- + +Internal refactor of how component data is access to avoid polluting components and make it possible to bridge across versions. diff --git a/.changeset/short-pots-report.md b/.changeset/short-pots-report.md deleted file mode 100644 index 4323a957f3..0000000000 --- a/.changeset/short-pots-report.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -'@backstage/plugin-org': patch ---- - -- Fixes padding in `MembersListCard` -- Fixes email icon size in `GroupProfileCard` -- Uniform sizing across `GroupProfileCard` and `UserProfileCard` diff --git a/.changeset/silver-balloons-play.md b/.changeset/silver-balloons-play.md new file mode 100644 index 0000000000..24a5c8ac46 --- /dev/null +++ b/.changeset/silver-balloons-play.md @@ -0,0 +1,5 @@ +--- +'@backstage/core': patch +--- + +Added a new useSupportConfig hook that reads a new `app.support` config key. Also updated the SupportButton and ErrorPage components to use the new config. diff --git a/.changeset/six-elephants-poke.md b/.changeset/six-elephants-poke.md deleted file mode 100644 index 23e575b67c..0000000000 --- a/.changeset/six-elephants-poke.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-catalog': patch ---- - -Migrate about card to new composability API, exporting the entity cards as `EntityAboutCard`. diff --git a/.changeset/soft-rings-obey.md b/.changeset/soft-rings-obey.md deleted file mode 100644 index 91308bdc50..0000000000 --- a/.changeset/soft-rings-obey.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fixed the `prepare` step for when using local templates that were added to the catalog using the `file:` target configuration. -No more `EPERM: operation not permitted` error messages. diff --git a/.changeset/spicy-pants-teach.md b/.changeset/spicy-pants-teach.md deleted file mode 100644 index 8e16ae089e..0000000000 --- a/.changeset/spicy-pants-teach.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/plugin-catalog-react': patch ---- - -Expose `useRelatedEntities` from `@backstage/plugin-catalog-react` to retrieve -entities references via relations from the API. diff --git a/.changeset/stale-ducks-sing.md b/.changeset/stale-ducks-sing.md new file mode 100644 index 0000000000..59eb956fd0 --- /dev/null +++ b/.changeset/stale-ducks-sing.md @@ -0,0 +1,56 @@ +--- +'@backstage/create-app': patch +--- + +Migrated away from using deprecated routes and router components at top-level in the app, and instead use routable extension pages. + +To apply this change to an existing app, make the following changes to `packages/app/src/App.tsx`: + +Update imports and remove the usage of the deprecated `app.getRoutes()`. + +```diff +-import { Router as DocsRouter } from '@backstage/plugin-techdocs'; ++import { TechdocsPage } from '@backstage/plugin-techdocs'; + import { CatalogImportPage } from '@backstage/plugin-catalog-import'; +-import { Router as TechRadarRouter } from '@backstage/plugin-tech-radar'; +-import { SearchPage as SearchRouter } from '@backstage/plugin-search'; +-import { Router as SettingsRouter } from '@backstage/plugin-user-settings'; ++import { TechRadarPage } from '@backstage/plugin-tech-radar'; ++import { SearchPage } from '@backstage/plugin-search'; ++import { UserSettingsPage } from '@backstage/plugin-user-settings'; ++import { ApiExplorerPage } from '@backstage/plugin-api-docs'; + import { EntityPage } from './components/catalog/EntityPage'; + import { scaffolderPlugin, ScaffolderPage } from '@backstage/plugin-scaffolder'; + + + const AppProvider = app.getProvider(); + const AppRouter = app.getRouter(); +-const deprecatedAppRoutes = app.getRoutes(); +``` + +As well as update or add the following routes: + +```diff + } /> +- } /> ++ } /> ++ } /> + } ++ element={} + /> + } /> +- } +- /> +- } /> +- {deprecatedAppRoutes} ++ } /> ++ } /> +``` + +If you have added additional plugins with registered routes or are using `Router` components from other plugins, these should be migrated to use the `*Page` components as well. See [this commit](https://github.com/backstage/backstage/commit/abd655e42d4ed416b70848ffdb1c4b99d189f13b) for more examples of how to migrate. + +For more information and the background to this change, see the [composability system migration docs](https://backstage.io/docs/plugins/composability). diff --git a/.changeset/strange-olives-unite.md b/.changeset/strange-olives-unite.md deleted file mode 100644 index 7490f6096a..0000000000 --- a/.changeset/strange-olives-unite.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-auth-backend': patch ---- - -Migrated the package from using moment to Luxon. #4278 diff --git a/.changeset/stupid-maps-do.md b/.changeset/stupid-maps-do.md deleted file mode 100644 index 1cbac64f68..0000000000 --- a/.changeset/stupid-maps-do.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Export Select component diff --git a/.changeset/sweet-experts-whisper.md b/.changeset/sweet-experts-whisper.md new file mode 100644 index 0000000000..ca2f89ceb7 --- /dev/null +++ b/.changeset/sweet-experts-whisper.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-auth-backend': patch +--- + +Expose a configuration option for the oidc scope diff --git a/.changeset/techdocs-fast-foxes-relate.md b/.changeset/techdocs-fast-foxes-relate.md deleted file mode 100644 index 7377946f86..0000000000 --- a/.changeset/techdocs-fast-foxes-relate.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/techdocs-common': patch ---- - -After TechDocs generate step, insert build timestamp to techdocs_metadata.json diff --git a/.changeset/techdocs-spotty-cooks-wait.md b/.changeset/techdocs-spotty-cooks-wait.md deleted file mode 100644 index 9ec35b9257..0000000000 --- a/.changeset/techdocs-spotty-cooks-wait.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/techdocs-common': patch -'@backstage/plugin-techdocs-backend': patch ---- - -Add etag of the prepared file tree to techdocs_metadata.json in the storage diff --git a/.changeset/thick-scissors-notice.md b/.changeset/thick-scissors-notice.md deleted file mode 100644 index ccf78f3abf..0000000000 --- a/.changeset/thick-scissors-notice.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Make sure that SidebarItems are also active when on sub route. diff --git a/.changeset/three-pumpkins-watch.md b/.changeset/three-pumpkins-watch.md new file mode 100644 index 0000000000..cb059a5381 --- /dev/null +++ b/.changeset/three-pumpkins-watch.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-jenkins': patch +--- + +- Add Jenkins logo to project page. +- Move to new Backstage breadcrumb component. +- Change references of deprecated "Job" terminology to "Project" per [Jenkins Glossary](https://www.jenkins.io/doc/book/glossary/). diff --git a/.changeset/tough-worms-clap.md b/.changeset/tough-worms-clap.md deleted file mode 100644 index 3a18593549..0000000000 --- a/.changeset/tough-worms-clap.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-scaffolder-backend': patch ---- - -Fix parsing of the path to default to empty string not undefined if git-url-parse throws something we don't expect. Fixes the error `The "path" argument must be of type string.` when preparing. diff --git a/.changeset/twenty-pandas-crash.md b/.changeset/twenty-pandas-crash.md new file mode 100644 index 0000000000..95ff389406 --- /dev/null +++ b/.changeset/twenty-pandas-crash.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Fixed EntityProvider setting 'loading' bool erroneously to true diff --git a/.changeset/violet-maps-occur.md b/.changeset/violet-maps-occur.md deleted file mode 100644 index 112095d257..0000000000 --- a/.changeset/violet-maps-occur.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -'@backstage/core': patch ---- - -Add support for custom empty state of `Table` components. - -You can now optionally pass `emptyContent` to `Table` that is displayed -if the table has now rows. diff --git a/.changeset/wicked-forks-sleep.md b/.changeset/wicked-forks-sleep.md deleted file mode 100644 index 8f7f034094..0000000000 --- a/.changeset/wicked-forks-sleep.md +++ /dev/null @@ -1,6 +0,0 @@ ---- -'@backstage/cli': patch -'@backstage/create-app': patch ---- - -Upgrading to lerna@4.0.0. diff --git a/.changeset/wise-books-turn.md b/.changeset/wise-books-turn.md deleted file mode 100644 index 77a42c5304..0000000000 --- a/.changeset/wise-books-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/core': patch ---- - -Add Breadcrumbs component diff --git a/.dockerignore b/.dockerignore index 7f7dee96c5..e34ae2c37d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,8 @@ .git +docs +cypress +microsite node_modules -packages/*/node_modules -plugins/*/node_modules -plugins/*/dist +packages +!packages/backend/dist +plugins diff --git a/.github/codecov.yml b/.github/codecov.yml index d39038615f..a15de165d0 100644 --- a/.github/codecov.yml +++ b/.github/codecov.yml @@ -1,5 +1,8 @@ comment: false # Ref: https://docs.codecov.io/docs/pull-request-comments +ignore: + - '**/*.stories.*' + coverage: status: project: diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index c5a0b02529..2e0bd13962 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -1,31 +1,116 @@ +Apdex +Api +Autoscaling +Avro +Balachandran +Bigtable +Billett +Blackbox +Chai +Changesets +Chanwit +Codecov +Codehilite +Config +Discoverability +Dockerfile +Dockerize +Docusaurus +Dominik +Ek +Env +Expedia +Figma +Firekube +Fiverr +Fredrik +Georgoulas +GitHub +GitLab +Grafana +GraphQL +Gustavsson +Hackathons +Henneke +Heroku +Hostname +Iain +Ioannis +JavaScript +Kaewkasi +Knex +Kumar +Lerna +Lundberg +Luxon +Malus +Minikube +Mkdocs +Monorepo +Namespaces +Niklas +OAuth +Okta +Oldsberg +Olle +Onboarding +Patrik +Phoen +Pomaceous +Preprarer +Protobuf +Proxying +Raghunandan +Readme +Recharts +Redash +Repo +Rollbar +Rollup +Rosaceae +Routable +Scaffolder +Serverless +Sinon +Sneha +Snyk +Splunk +Spotifiers +Spotify +Superfences +Talkdesk +Telenor +Templater +Templaters +Thauer +Tolerations +Tuite +Voi +WWW +Wealthsimple +Weaveworks +Webpack +Zalando +Zhou +Zolotusky abc +accessors adamdmharvey andrewthauer -Apdex api -Api apis args asciidoc async -Autoscaling autoscaling -Avro backrub -Balachandran benjdlambert -Bigtable -Billett -Blackbox bool boolean builtins -Chai changeset changesets -Changesets chanwit -Chanwit ci cisphobia cissexist @@ -34,14 +119,11 @@ cli cloudbuild cncf codeblocks -Codecov codehilite -Codehilite codeowners composability composable config -Config configmaps configs const @@ -56,97 +138,62 @@ devops devs dhenneke discoverability -Discoverability dls docgen -Dockerfile -Dockerize dockerode -Docusaurus -Dominik dtuite dzolotusky -Ek -etag env -Env esbuild eslint -Expedia +etag facto failover -Figma -Firekube -Fiverr freben -Fredrik -Georgoulas gitbeaker -GitHub -GitLab -Grafana -GraphQL graphql graphviz -Gustavsson -Hackathons haproxy -Henneke -Heroku horizontalpodautoscalers -Hostname hotspots html http https -Iain img incentivised inlined inlinehilite interop -Ioannis -JavaScript jq js json +jsonnet jsx -Kaewkasi -Knex kubectl kubernetes -Kumar learnings lerna -Lerna lockfile -Luxon magiclink mailto maintainership -Malus md microsite middleware minikube -Minikube misconfiguration misconfigured misgendering mkdocs -Mkdocs monorepo -Monorepo monorepos msw namespace namespaces -Namespaces namespacing neuro newrelic nginx -Niklas nodegit nohoist nonces @@ -154,49 +201,31 @@ noop npm nvarchar nvm -OAuth octokit oidc -Okta -Oldsberg onboarding -Onboarding pagerduty +pageview parallelization -Patrik -Phoen plantuml -Pomaceous postgres postpack pre prebaked preconfigured prepack -Preprarer productional -Protobuf proxying -Proxying pygments pymdownx -Raghunandan rankdir readme -Readme -Recharts -Redash replicasets repo -Repo repos rerender rollbar -Rollbar -Rollup -Rosaceae routable -Routable rst rsync rugvip @@ -204,19 +233,11 @@ ruleset sam scaffolded scaffolder -Scaffolder semlas semver -Serverless -Sinon -Sneha -Snyk sourcemaps sparklines -Splunk -Spotifiers spotify -Spotify sqlite squidfunk src @@ -227,30 +248,22 @@ subcomponents subkey subtree superfences -Superfences superset talkdesk -Talkdesk tasklist techdocs -Telenor templated templater -Templater templaters -Templaters -Thauer toc tolerations -Tolerations toolchain toolsets tooltip tooltips touchpoints -transpiled transpilation -Tuite +transpiled ui unmanaged unregister @@ -260,16 +273,8 @@ url utils validators varchar -Voi -Wealthsimple -Weaveworks -Webpack winston www -WWW xyz yaml -Zalando -Zhou -Zolotusky zoomable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 08ec67162b..2337a70e2c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -87,7 +87,7 @@ jobs: - name: build changed packages if: ${{ steps.yarn-lock.outcome == 'success' }} - run: yarn lerna -- run build --since origin/master + run: yarn lerna -- run build --since origin/master --include-dependencies - name: build all packages if: ${{ steps.yarn-lock.outcome == 'failure' }} diff --git a/ADOPTERS.md b/ADOPTERS.md index 363c092198..d76e24ef04 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -19,3 +19,4 @@ | [LegalZoom](https://legalzoom.com) | [@backjo](https://github.com/backjo) | Developer portal - hub for all engineering projects and metadata. | | [Expedia Group](https://www.expediagroup.com) | [Mike Turner](mailto:miturner@expediagroup.com), [Sneha Kumar](mailto:snkumar@expediagroup.com), [@guillermomanzo](https://github.com/guillermomanzo) | EG Common Developer Toolkit | | [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | +| [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d163a42954..ddc5449cd8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -66,7 +66,7 @@ Have you started using Backstage? Adding your company to [ADOPTERS](ADOPTERS.md) So...feel ready to jump in? Let's do this. 👏🏻💯 -Start by reading our [Getting Started](https://backstage.io/docs/getting-started/) page. If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2). +Start by reading our [Getting Started for Contributors](https://backstage.io/docs/getting-started/contributors) page to get yourself setup with a fresh copy of Backstage ready for your contributions. If you need help, just jump into our [Discord chatroom](https://discord.gg/MUpMjP2). ## Coding Guidelines diff --git a/app-config.yaml b/app-config.yaml index 672ba26cd2..f68f5a30a2 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -2,6 +2,19 @@ app: title: Backstage Example App baseUrl: http://localhost:3000 googleAnalyticsTrackingId: # UA-000000-0 + support: + url: https://github.com/backstage/backstage/issues # Used by common ErrorPage + items: # Used by common SupportButton component + - title: Issues + icon: github + links: + - url: https://github.com/backstage/backstage/issues + title: GitHub Issues + - title: Discord Chatroom + icon: chat + links: + - url: https://discord.gg/MUpMjP2 + title: '#backstage' backend: baseUrl: http://localhost:7000 diff --git a/docs/api/utility-apis.md b/docs/api/utility-apis.md index 5b5656f1b1..308544e0f1 100644 --- a/docs/api/utility-apis.md +++ b/docs/api/utility-apis.md @@ -181,7 +181,25 @@ The `IgnoringErrorApi` would then be imported in the app, and wired up like this: ```ts -builder.add(errorApiRef, new IgnoringErrorApi()); +const app = createApp({ + apis: [ + /* ApiFactories */ + createApiFactory(errorApiRef, new IgnoringErrorApi()), + + // OR + // If your API has dependencies, you use the object form + createApiFactory({ + api: errorApiRef, + deps: { configApi: configApiRef }, + factory({ configApi }) { + return new IgnoringErrorApi({ + reportingUrl: configApi.getString('error.reportingUrl'), + }); + }, + }), + ], + // ... other options +}); ``` Note that the above line will cause an error if `IgnoreErrorApi` does not fully diff --git a/docs/cli/commands.md b/docs/cli/commands.md index 562604db97..46cca46f78 100644 --- a/docs/cli/commands.md +++ b/docs/cli/commands.md @@ -24,7 +24,6 @@ Below is a cleaned up output of `yarn backstage-cli --help`. ```text app:build Build an app for a production release -app:diff Diff an existing app with the creation template app:serve Serve an app for local development backend:build Build a backend plugin @@ -113,25 +112,6 @@ Options: -h, --help display help for command ``` -## app:diff - -Scope: `app` - -Diff an existing app with the template used in `@backstage/create-app`. This -will verify that your app package has not diverged from the template, and can be -useful to run after updating the version of `@backstage/cli` in your app. - -This command is experimental and may be removed in the future. - -```text -Usage: backstage-cli app:diff - -Options: - --check Fail if changes are required - --yes Apply all changes - -h, --help display help for command -``` - ## app:serve Scope: `app` @@ -207,15 +187,15 @@ The following is an example of a `Dockerfile` that can be used to package the output of `backstage-cli backend:bundle` into an image: ```Dockerfile -FROM node:14-buster +FROM node:14-buster-slim WORKDIR /app ADD yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ -RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" +RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./ -CMD node packages/backend +CMD ["node", "packages/backend"] ``` ```text diff --git a/docs/dls/design.md b/docs/dls/design.md index 3bf283b77f..042312d0a3 100644 --- a/docs/dls/design.md +++ b/docs/dls/design.md @@ -33,15 +33,18 @@ our users. ### Transparent There are a lot of exciting things coming up and we want to keep you in the -loop! Keep an eye on our Milestones in GitHub to see where we’re headed. We’ll -also be posting updates in the _#design_ channel on -[Discord](https://discord.gg/EBHEGzX). Not only that, we want to keep you -informed on the decisions we’ve made and why we’ve made them. +loop! Keep an eye on our +[Milestones in GitHub](https://github.com/backstage/backstage/milestones) to see +where we're headed and review the +[open design issues](https://github.com/backstage/backstage/issues?q=is%3Aopen+is%3Aissue+label%3Adesign), +to see if you can help. We'll also be posting updates in the _#design_ channel +on [Discord](https://discord.gg/EBHEGzX). Not only that, we want to keep you +informed on the decisions we've made and why we've made them. ## 🛠 Our Practice -The chart below details how we work. **_Stay tuned_**: We are currently in the -process of securing a Figma workspace for Backstage Open Source, and we plan on +The chart below details how we work. We have a +[Figma workspace for Backstage Open Source](figma.md), and we plan on referencing Figma documents to share specs and prototypes with the community. ### Creating a New Design Component diff --git a/docs/features/software-catalog/installation.md b/docs/features/software-catalog/installation.md index b2afd62878..ae8c671ed1 100644 --- a/docs/features/software-catalog/installation.md +++ b/docs/features/software-catalog/installation.md @@ -30,33 +30,59 @@ it doesn't. Add the following entry to the head of your `packages/app/src/plugins.ts`: ```ts -export { plugin as CatalogPlugin } from '@backstage/plugin-catalog'; +export { catalogPlugin } from '@backstage/plugin-catalog'; ``` -Add the following to your `packages/app/src/apis.ts`: +Next we need to install the two pages that the catalog plugin provides. You can +choose any name for these routes, but we recommend the following: + +```tsx +import { + catalogPlugin, + CatalogIndexPage, + CatalogEntityPage, +} from '@backstage/plugin-catalog'; + +// Add to the top-level routes, directly within +} /> +}> + {/* + This is the root of the custom entity pages for your app, refer to the example app + in the main repo or the output of @backstage/create-app for an example + */} + + +``` + +The catalog plugin also has one external route that needs to be bound for it to +function: the `createComponent` route which should link to the page where the +user can create components. In a typical setup the create component route will +be linked to the Scaffolder plugin's template index page: ```ts -import { catalogApiRef, CatalogClient } from '@backstage/plugin-catalog'; +import { catalogPlugin } from '@backstage/plugin-catalog'; +import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; -// Inside the ApiRegistry builder function ... - -builder.add( - catalogApiRef, - new CatalogClient({ - apiOrigin: backendUrl, - basePath: '/catalog', - }), -); +const app = createApp({ + // ... + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + }, +}); ``` -Where `backendUrl` is the `backend.baseUrl` from config, i.e. -`const backendUrl = config.getString('backend.baseUrl')`. +You may also want to add a link to the catalog index page to your sidebar: -The catalog components depend on a number of other -[Utility APIs](../../api/utility-apis.md) to function, including at least the -`ErrorApi` and `StorageApi`. You can find an example of how to install these in -your app -[here](https://github.com/backstage/backstage/blob/61c3a7e5b750dc7c059ef16b188594d31b2c04c2/packages/app/src/apis.ts#L80). +```tsx +import HomeIcon from '@material-ui/icons/Home'; + +// Somewhere within the +; +``` + +This is all that is needed for the frontend part of the Catalog plugin to work! ## Gotchas that we will fix diff --git a/docs/features/software-catalog/well-known-annotations.md b/docs/features/software-catalog/well-known-annotations.md index 2cbb829554..8ec4813799 100644 --- a/docs/features/software-catalog/well-known-annotations.md +++ b/docs/features/software-catalog/well-known-annotations.md @@ -70,6 +70,35 @@ The value of this annotation is a location reference string (see above). If this annotation is specified, it is expected to point to a repository that the TechDocs system can read and generate docs from. +### backstage.io/view-url, backstage.io/edit-url + +```yaml +# Example: +metadata: + annotations: + backstage.io/view-url: https://some.website/catalog-info.yaml + backstage.io/edit-url: https://github.com/my-org/catalog/edit/master/my-service.jsonnet +``` + +These annotations allow customising links from the catalog pages. The view URL +should point to the canonical metadata YAML that governs this entity. The edit +URL should point to the source file for the metadata. In the example above, +`my-org` generates its catalog data from Jsonnet files in a monorepo, so the +view and edit links need changing. + +### backstage.io/source-location + +```yaml +# Example: +metadata: + annotations: + backstage.io/source-location: github:https://github.com/my-org/my-service +``` + +A `Location` reference that points to the source code of the entity (typically a +`Component`). Useful when catalog files do not get ingested from the source code +repository itself. + ### jenkins.io/github-folder ```yaml diff --git a/docs/features/software-templates/installation.md b/docs/features/software-templates/installation.md index 8d2fa7727d..9aa1d3bc8f 100644 --- a/docs/features/software-templates/installation.md +++ b/docs/features/software-templates/installation.md @@ -33,27 +33,27 @@ it doesn't. Add the following entry to the head of your `packages/app/src/plugins.ts`: ```ts -export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder'; +export { scaffolderPlugin } from '@backstage/plugin-scaffolder'; ``` -Add the following to your `packages/app/src/apis.ts`: +Next we need to install the root page that the Scaffolder plugin provides. You +can choose any path for the route, but we recommend the following: -```ts -import { scaffolderApiRef, ScaffolderApi } from '@backstage/plugin-scaffolder'; +```tsx +import { ScaffolderPage } from '@backstage/plugin-scaffolder'; -// Inside the ApiRegistry builder function ... - -builder.add( - scaffolderApiRef, - new ScaffolderApi({ - apiOrigin: backendUrl, - basePath: '/scaffolder/v1', - }), -); +// Add to the top-level routes, directly within +} />; ``` -Where `backendUrl` is the `backend.baseUrl` from config, i.e. -`const backendUrl = config.getString('backend.baseUrl')`. +You may also want to add a link to the template index page to your sidebar: + +```tsx +import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; + +// Somewhere within the +; +``` This is all that is needed for the frontend part of the Scaffolder plugin to work! @@ -85,29 +85,25 @@ following contents to get you up and running quickly. import { CookieCutter, createRouter, - FilePreparer, - GithubPreparer, - GitlabPreparer, Preparers, Publishers, - GithubPublisher, - GitlabPublisher, CreateReactAppTemplater, Templaters, - RepoVisibilityOptions, } from '@backstage/plugin-scaffolder-backend'; -import { Octokit } from '@octokit/rest'; -import { Gitlab } from '@gitbeaker/node'; +import { SingleHostDiscovery } from '@backstage/backend-common'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; +import { CatalogClient } from '@backstage/catalog-client'; export default async function createPlugin({ logger, config, + database, }: PluginEnvironment) { const cookiecutterTemplater = new CookieCutter(); const craTemplater = new CreateReactAppTemplater(); const templaters = new Templaters(); + templaters.register('cookiecutter', cookiecutterTemplater); templaters.register('cra', craTemplater); @@ -115,12 +111,19 @@ export default async function createPlugin({ const publishers = await Publishers.fromConfig(config, { logger }); const dockerClient = new Docker(); + + const discovery = SingleHostDiscovery.fromConfig(config); + const catalogClient = new CatalogClient({ discoveryApi: discovery }); + return await createRouter({ preparers, templaters, publishers, logger, + config, dockerClient, + database, + catalogClient, }); } ``` diff --git a/docs/features/techdocs/using-cloud-storage.md b/docs/features/techdocs/using-cloud-storage.md index de2d505c87..cd4b058644 100644 --- a/docs/features/techdocs/using-cloud-storage.md +++ b/docs/features/techdocs/using-cloud-storage.md @@ -142,6 +142,8 @@ variables** You should follow the [AWS security best practices guide for authentication](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html). +TechDocs needs access to read files and metadata of the S3 bucket. + If the environment variables - `AWS_ACCESS_KEY_ID` @@ -149,15 +151,21 @@ If the environment variables - `AWS_REGION` are set and can be used to access the bucket you created in step 2, they will be -used by the AWS SDK v3 Node.js client for authentication. -[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html) +used by the AWS SDK V2 Node.js client for authentication. +[Refer to the official documentation for loading credentials in Node.js from environment variables](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-environment.html). If the environment variables are missing, the AWS SDK tries to read the `~/.aws/credentials` file for credentials. -[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html) +[Refer to the official documentation.](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/loading-node-credentials-shared.html) -Note that the region of the bucket has to be set for the AWS SDK to work. -[See this](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html). +If you are using Amazon EC2 instance to deploy Backstage, you do not need to +obtain the access keys separately. They can be made available in the environment +automatically by defining appropriate IAM role with access to the bucket. Read +more in +[official AWS documentation for using IAM roles.](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#use-roles). + +The AWS Region of the bucket is optional since TechDocs uses AWS SDK V2 and not +V3. **3b. Authentication using app-config.yaml** @@ -181,13 +189,28 @@ techdocs: ``` Refer to the -[official AWS documentation for obtaining the credentials](https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/getting-your-credentials.html). +[official AWS documentation for obtaining the credentials](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/getting-your-credentials.html). -Note: If you are using Amazon EC2 instance to deploy Backstage, you do not need -to obtain the access keys separately. They can be made available in the -environment automatically by defining appropriate IAM role with access to the -bucket. Read more -[here](https://docs.aws.amazon.com/general/latest/gr/aws-access-keys-best-practices.html#use-roles). +**3c. Authentication using an assumed role** Users with multiple AWS accounts +may want to use a role for S3 storage that is in a different AWS account. Using +the `roleArn` parameter as seen below, you can instruct the TechDocs publisher +to assume a role before accessing S3. + +```yaml +techdocs: + publisher: + type: 'awsS3' + awsS3: + bucketName: 'name-of-techdocs-storage-bucket' + region: + $env: AWS_REGION + credentials: + roleArn: arn:aws:iam::123456789012:role/my-backstage-role +``` + +Note: Assuming a role requires that primary credentials are already configured +at `AWS.config.credentials`. Read more about +[assuming roles in AWS](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRole.html). **4. That's it!** diff --git a/docs/getting-started/development-environment.md b/docs/getting-started/contributors.md similarity index 71% rename from docs/getting-started/development-environment.md rename to docs/getting-started/contributors.md index 01fd7bdc24..a0c3f3dc4c 100644 --- a/docs/getting-started/development-environment.md +++ b/docs/getting-started/contributors.md @@ -1,6 +1,6 @@ --- -id: development-environment -title: Development Environment +id: contributors +title: Contributors # prettier-ignore description: Documentation on how to get set up for doing development on the Backstage repository --- @@ -10,6 +10,21 @@ repository. ## Cloning the Repository +Ok. So you're gonna want some code right? Go ahead and fork the repository into +your own GitHub account and clone that code to your local machine or you can +grab the one for the origin like so: + +```bash +git clone git@github.com/backstage/backstage --depth 1 +``` + +If you cloned a fork, you can add the upstream dependency like so: + +```bash +git remote add upstream git@github.com:backstage/backstage +git pull upstream master +``` + After you have cloned the Backstage repository, you should run the following commands once to set things up for development: @@ -25,9 +40,11 @@ Open a terminal window and start the web app by using the following command from the project root. Make sure you have run the above mentioned commands first. ```bash -$ yarn start +$ yarn dev ``` +This is going to start two things, the frontend (:3000) and the backend (:7000). + This should open a local instance of Backstage in your browser, otherwise open one of the URLs printed in the terminal. @@ -38,12 +55,27 @@ setting an environment variable `PORT` on your local machine. e.g. Once successfully started, you should see the following message in your terminal window: +```sh +$ concurrently "yarn start" "yarn start-backend" +$ yarn workspace example-app start +$ yarn workspace example-backend start +$ backstage-cli app:serve +$ backstage-cli backend:dev +[0] Loaded config from app-config.yaml +[1] Build succeeded +[0] ℹ 「wds」: Project is running at http://localhost:3000/ +[0] ℹ 「wds」: webpack output is served from / +[0] ℹ 「wds」: Content not from webpack is served from $BACKSTAGE_DIR/packages/app/public +[0] ℹ 「wds」: 404s will fallback to /index.html +[0] ℹ 「wdm」: wait until bundle finished: / +[1] 2021-02-12T20:58:17.614Z backstage info Loaded config from app-config.yaml ``` -You can now view example-app in the browser. - Local: http://localhost:8080 - On Your Network: http://192.168.1.224:8080 -``` +You'll see how you get both logs for the frontend `webpack-dev-server` which +serves the react app ([0]) and the backend ([1]); + +Visit http://localhost:3000 and you should see the bleeding edge of Backstage +ready for contributions! ## Editor diff --git a/docs/getting-started/create-an-app.md b/docs/getting-started/create-an-app.md index 0663d3faa1..9abb68d21b 100644 --- a/docs/getting-started/create-an-app.md +++ b/docs/getting-started/create-an-app.md @@ -177,20 +177,6 @@ the root directory: yarn workspace backend start ``` +Now you're free to hack away on your own Backstage installation! + ### Troubleshooting - -#### Cannot find module - -You may encounter an error similar to below: - -``` -internal/modules/cjs/loader.js:968 - throw err; - ^ - -Error: Cannot find module '../build/Debug/nodegit.node' -``` - -This can occur if an npm dependency is not completely installed. Because some -dependencies run a post-install script, ensure that both your npm and yarn -configs have the `ignore-scripts` flag set to `false`. diff --git a/docs/getting-started/deployment-docker.md b/docs/getting-started/deployment-docker.md new file mode 100644 index 0000000000..69158f9104 --- /dev/null +++ b/docs/getting-started/deployment-docker.md @@ -0,0 +1,238 @@ +--- +id: deployment-docker +title: Docker +description: Documentation on how to deploy Backstage as a Docker image +--- + +This section describes how to build a Backstage App into a deployable Docker +image. It is split into three sections, first covering the host build approach, +which is recommended due its speed and more efficient and often simpler caching. +The second section covers a full multi-stage Docker build, and the last section +covers how to split frontend content into a separate image. + +Something that goes for all of these docker deployment strategies is that they +are stateless, so for a production deployment you will want to set up and +connect to an external PostgreSQL instance where the backend plugins can store +their state, rather than using SQLite. + +### Host Build + +This section describes how to build a Docker image from a Backstage repo with +most of the build happening outside of Docker. This is almost always the faster +approach, as the build steps tend to execute faster, and it's possible to have +more efficient caching of dependencies on the host, where a single change won't +bust the entire cache. + +The required steps in the host build are to install dependencies with +`yarn install`, generate type definitions using `yarn tsc`, and build all +packages with `yarn build`. + +> NOTE: Using `yarn build` to build packages and bundle the backend assumes that +> you have migrated to using `backstage-cli backend:bundle` as your build script +> in the backend package. + +In a CI workflow it might look something like this: + +```bash +yarn install --frozen-lockfile + +# tsc outputs type definitions to dist-types/ in the repo root, which are then consumed by the build +yarn tsc + +# Build all packages and in the end bundle them all up into the packages/backend/dist folder. +yarn build +``` + +Once the host build is complete, we are ready to build our image. We use the +following `Dockerfile`, which is also included when creating a new app with +`@backstage/create-app`: + +```Dockerfile +# This dockerfile builds an image for the backend package. +# It should be executed with the root of the repo as docker context. +# +# Before building this image, be sure to have run the following commands in the repo root: +# +# yarn install +# yarn tsc +# yarn build +# +# Once the commands have been run, you can build the image using `yarn build-image` + +FROM node:14-buster-slim + +WORKDIR /app + +# Copy repo skeleton first, to avoid unnecessary docker cache invalidation. +# The skeleton contains the package.json of each package in the monorepo, +# and along with yarn.lock and the root package.json, that's enough to run yarn install. +ADD yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ + +RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" + +# Then copy the rest of the backend bundle, along with any other files we might want. +ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./ + +CMD ["node", "packages/backend", "--config", "app-config.yaml"] +``` + +For more details on how the `backend:bundle` command and the `skeleton.tar.gz` +file works, see the +[`backend:bundle` command docs](../cli/commands.md#backendbundle) + +The `Dockerfile` is typically placed at `packages/backend/Dockerfile`, but needs +to be executed with the root of the repo as the build context, in order to get +access to the root `yarn.lock` and `package.json`, along with any other files +that might be needed, such as `.npmrc`. + +In order to speed up the build we can significantly reduce the build context +size using the following `.dockerignore` in the root of the repo: + +```text +.git +node_modules +packages +!packages/backend/dist +plugins +``` + +With the project build and the `.dockerignore` and `Dockerfile` in place, we are +now ready to build the final image. Assuming we're at the root of the repo, we +execute the build like this: + +```bash +docker image build . -f packages/backend/Dockerfile --tag backstage +``` + +To try out the image locally you can run the following: + +```sh +docker run -it -p 7000:7000 backstage +``` + +You should then start to get logs in your terminal, and then you can open your +browser at `http://localhost:7000` + +### Multistage Build + +This section describes how to set up a multi-stage Docker build that builds the +entire project within Docker. This is typically slower than a host build, but is +sometimes desired because Docker in Docker is not available in the build +environment, or due to other requirements. + +The build is split into three different stages, where the first stage finds all +of the `package.json`s that are relevant for the initial install step enabling +us to cache the initial `yarn install` that installs all dependencies. The +second stage executes the build itself, and is similar to the steps we execute +on the host in the host build. The third and final stage then packages it all +together into the final image, and is similar to the `Dockerfile` of the host +build. + +The following `Dockerfile` executes the multi-stage build and should be added to +the repo root: + +```Dockerfile +# Stage 1 - Create yarn install skeleton layer +FROM node:14-buster-slim AS packages + +WORKDIR /app +COPY package.json yarn.lock ./ + +COPY packages packages +COPY plugins plugins + +RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -exec rm -rf {} \+ + +# Stage 2 - Install dependencies and build packages +FROM node:14-buster-slim AS build + +WORKDIR /app +COPY --from=packages /app . + +RUN yarn install --frozen-lockfile --network-timeout 600000 && rm -rf "$(yarn cache dir)" + +COPY . . + +RUN yarn tsc +RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies + +# Stage 3 - Build the actual backend image and install production dependencies +FROM node:14-buster-slim + +WORKDIR /app + +# Copy the install dependencies from the build stage and context +COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./ +RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz + +RUN yarn install --frozen-lockfile --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" + +# Copy the built packages from the build stage +COPY --from=build /app/packages/backend/dist/bundle.tar.gz . +RUN tar xzf bundle.tar.gz && rm bundle.tar.gz + +# Copy any other files that we need at runtime +COPY app-config.yaml ./ + +CMD ["node", "packages/backend", "--config", "app-config.yaml"] +``` + +Note that a newly created Backstage app will typically not have a `plugins/` +folder, so you will want to comment that line out. This build also does not work +in the main repo, since the `backstage-cli` which is used for the build doesn't +end up being properly installed. + +To speed up the build when not running in a fresh clone of the repo you should +set up a `.dockerignore`. This one is different than the host build one, because +we want to have access to the source code of all packages for the build, but can +ignore any existing build output or dependencies: + +```text +node_modules +packages/*/dist +packages/*/node_modules +plugins/*/dist +plugins/*/node_modules +``` + +Once you have added both the `Dockerfile` and `.dockerignore` to the root of +your project, run the following to build the container under a specified tag. + +```sh +docker image build -t backstage . +``` + +To try out the image locally you can run the following: + +```sh +docker run -it -p 7000:7000 backstage +``` + +You should then start to get logs in your terminal, and then you can open your +browser at `http://localhost:7000` + +### Separate Frontend + +It is sometimes desirable to serve the frontend separately from the backend, +either from a separate image or for example a static file serving provider. The +first step in doing so is to remove the `app-backend` plugin from the backend +package, which is done as follows: + +1. Delete `packages/backend/src/plugins/app.ts` +2. Remove the following lines from `packages/backend/src/index.ts`: + ```tsx + import app from './plugins/app'; + // ... + const appEnv = useHotMemoize(module, () => createEnv('app')); + // ... + .addRouter('', await app(appEnv)); + ``` +3. Remove the `@backstage/plugin-app-backend` and the app package dependency + (e.g. `app`) from `packages/backend/packages.json`. If you don't remove the + app package dependency the app will still be built and bundled with the + backend. + +Once the `app-backend` is removed from the backend, you can use your favorite +static file serving method for serving the frontend. An example of how to set up +an NGINX image is available in the +[contrib folder in the main repo](https://github.com/backstage/backstage/blob/master/contrib/docker/frontend-with-nginx/Dockerfile) diff --git a/docs/getting-started/deployment-other.md b/docs/getting-started/deployment-other.md index e92b54d08b..cc622d00f5 100644 --- a/docs/getting-started/deployment-other.md +++ b/docs/getting-started/deployment-other.md @@ -4,98 +4,6 @@ title: Other description: Documentation on different ways of Deployment --- -## Docker - -Here we have an example Dockerfile that you can use to build everything together -in one container. This Dockerfile uses multi-stage builds, and a -`backend:bundle` command from the CLI. - -It also provides caching on the `yarn install`'s so that you don't have to do it -unless absolutely necessary. - -> Note: This Dockerfile assumes that you're running SQLite, or your -> configuration is setup to connect to an external PostgreSQL Database. - -```Dockerfile -# Stage 1 - Create yarn install skeleton layer -FROM node:14-buster AS packages - -WORKDIR /app -COPY package.json yarn.lock ./ - -COPY packages packages - -# Uncomment this line if you have a local plugins folder -# COPY plugins plugins - -RUN find packages \! -name "package.json" -mindepth 2 -maxdepth 2 -print | xargs rm -rf - -# Stage 2 - Install dependencies and build packages -FROM node:14-buster AS build - -WORKDIR /app -COPY --from=packages /app . - -RUN yarn install --network-timeout 600000 && rm -rf "$(yarn cache dir)" - -COPY . . - -RUN yarn tsc -RUN yarn --cwd packages/backend backstage-cli backend:bundle --build-dependencies - -# Stage 3 - Build the actual backend image and install production dependencies -FROM node:14-buster - -WORKDIR /app - -# Copy from build stage -COPY --from=build /app/yarn.lock /app/package.json /app/packages/backend/dist/skeleton.tar.gz ./ -RUN tar xzf skeleton.tar.gz && rm skeleton.tar.gz - -RUN yarn install --production --network-timeout 600000 && rm -rf "$(yarn cache dir)" - -COPY --from=build /app/packages/backend/dist/bundle.tar.gz . -RUN tar xzf bundle.tar.gz && rm bundle.tar.gz - -COPY app-config.yaml app-config.production.yaml ./ - -CMD ["node", "packages/backend", "--config", "app-config.yaml", "--config", "app-config.production.yaml"] -``` - -Before building you should also include a `.dockerignore`. This will greatly -improve the context boot up time of Docker as we are no longer sending all of -the `node_modules` into the context. It also helps us avoid some limitations and -errors that may occur when trying to share the `node_modules` folder to inside -the build. - -You can add the following contents to the root of your repository at -`.dockerignore` and it might look something like the following: - -```dockerignore -.git -node_modules -packages/*/node_modules -plugins/*/node_modules -plugins/*/dist -``` - -Once you have added both the `Dockerfile` and `.dockerignore` to the root of -your project, and run the following to build the container under a specified -tag. - -```sh -$ docker build -t example-deployment . -``` - -To run the image locally you can run: - -```sh -$ docker run -it -p 7000:7000 example-deployment -``` - -You should then start to get logs in your terminal, and then you can open your -browser at `http://localhost:7000` - ## Heroku Deploying to Heroku is relatively easy following these steps. diff --git a/docs/getting-started/index.md b/docs/getting-started/index.md index 16c837d676..b946039dfb 100644 --- a/docs/getting-started/index.md +++ b/docs/getting-started/index.md @@ -19,7 +19,7 @@ general, it's easier to fork and clone this project. That will let you stay up to date with the latest changes, and gives you an easier path to make Pull Requests towards this repo. -### Creating a Standalone App +### Create your Backstage App Backstage provides the `@backstage/create-app` package to scaffold standalone instances of Backstage. You will need to have @@ -46,8 +46,3 @@ look something like this. You can read more about this process in You can read more in our [CONTRIBUTING](https://github.com/backstage/backstage/blob/master/CONTRIBUTING.md) guide, which can help you get setup with a Backstage development environment. - -### Next steps - -Take a look at the [Running Backstage Locally](./running-backstage-locally.md) -guide to learn how to set up Backstage, and how to develop on the platform. diff --git a/docs/getting-started/running-backstage-locally.md b/docs/getting-started/running-backstage-locally.md index 6de9d234e3..fe68937c4e 100644 --- a/docs/getting-started/running-backstage-locally.md +++ b/docs/getting-started/running-backstage-locally.md @@ -104,7 +104,6 @@ The value of Backstage grows with every new plugin that gets added. Here is a collection of tutorials that will guide you through setting up and extending an instance of Backstage with your own plugins. -- [Development Environment](development-environment.md) - [Create a Backstage Plugin](../plugins/create-a-plugin.md) - [Structure of a Plugin](../plugins/structure-of-a-plugin.md) - [Utility APIs](../api/utility-apis.md) diff --git a/docs/integrations/github/org.md b/docs/integrations/github/org.md new file mode 100644 index 0000000000..848a306381 --- /dev/null +++ b/docs/integrations/github/org.md @@ -0,0 +1,71 @@ +--- +id: org +title: GitHub Organizational Data +sidebar_label: Org Data +# prettier-ignore +description: Setting up ingestion of organizational data from GitHub +--- + +The Backstage catalog can be set up to ingest organizational data - teams and +users - directly from an organization in GitHub or GitHub Enterprise. The result +is a hierarchy of +[`User`](../../features/software-catalog/descriptor-format.md#kind-user) and +[`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind +entities that mirror your org setup. + +## Installation + +The processor that performs the import, `GithubOrgReaderProcessor`, comes +installed with the default setup of Backstage. + +If you replace the set of processors in your installation using that facility of +the catalog builder class, you can import and add it as follows. + +```ts +// Typically in packages/backend/src/plugins/catalog.ts +import { GithubOrgReaderProcessor } from '@backstage/plugin-catalog-backend'; + +builder.replaceProcessors( + GithubOrgReaderProcessor.fromConfig(config, { logger }), + // ... +); +``` + +## Configuration + +The following configuration enables an import of the teams and users under the +org `https://github.com/my-org-name` on public GitHub. + +```yaml +catalog: + locations: + - type: github-org + target: https://github.com/my-org-name + processors: + githubOrg: + providers: + - target: https://github.com + apiBaseUrl: https://api.github.com + token: + $env: GITHUB_TOKEN +``` + +Locations point out the specific org(s) you want to import. The `type` of these +locations must be `github-org`, and the `target` must point to the exact URL of +some organization. You can have several such location entries if you want, but +typically you will have just one. + +The processor itself is configured in the other block, under +`catalog.processors.githubOrg`. There may be many providers, each targeting a +specific `target` which is supposed to be the address of the home page of GitHub +or your GitHub Enterprise installation. + +The example above assumes that the backend is started with an environment +variable called `GITHUB_TOKEN` that contains a Personal Access Token. The token +needs to have at least the scopes `read:org`, `read:user`, and `user:email` in +the given `target`. + +If you want to address your own GitHub Enterprise instance, replace occurrences +of `https://github.com` in the configuration above with the address of your +GitHub Enterprise home page, and the `apiBaseUrl` to where your API endpoint +lives - commonly on the form `https:///api/v3`. diff --git a/docs/integrations/google-analytics/installation.md b/docs/integrations/google-analytics/installation.md new file mode 100644 index 0000000000..a0e6f69d13 --- /dev/null +++ b/docs/integrations/google-analytics/installation.md @@ -0,0 +1,24 @@ +--- +id: installation +title: Google Analytics Installation +sidebar_label: Installation +# prettier-ignore +description: Adding Google Analytics to Your App +--- + +There is a basic Google Analytics integration built into Backstage. You can +enable it by adding the following to your app configuration: + +```yaml +app: + googleAnalyticsTrackingId: UA-000000-0 +``` + +Replace the tracking ID with your own. + +For more information, learn about Google Analytics +[here](https://marketingplatform.google.com/about/analytics/). + +The default behavior is only to send a pageview hit to Google Analytics. To +record more, look at the developer documentation +[here](https://developers.google.com/analytics/devguides/collection/gtagjs). diff --git a/docs/integrations/ldap/org.md b/docs/integrations/ldap/org.md new file mode 100644 index 0000000000..dea33e4024 --- /dev/null +++ b/docs/integrations/ldap/org.md @@ -0,0 +1,280 @@ +--- +id: org +title: LDAP Organizational Data +sidebar_label: Org Data +# prettier-ignore +description: Setting up ingestion of organizational data from LDAP +--- + +The Backstage catalog can be set up to ingest organizational data - groups and +users - directly from an LDAP compatible service. The result is a hierarchy of +[`User`](../../features/software-catalog/descriptor-format.md#kind-user) and +[`Group`](../../features/software-catalog/descriptor-format.md#kind-group) kind +entities that mirror your org setup. + +## Installation + +The processor that performs the import, `LdapOrgReaderProcessor`, comes +installed with the default setup of Backstage. + +If you replace the set of processors in your installation using that facility of +the catalog builder class, you can import and add it as follows. + +```ts +// Typically in packages/backend/src/plugins/catalog.ts +import { LdapOrgReaderProcessor } from '@backstage/plugin-catalog-backend'; + +builder.replaceProcessors( + LdapOrgReaderProcessor.fromConfig(config, { logger }), + // ... +); +``` + +## Configuration + +The following configuration is a small example of how a setup could look for +importing groups and users from a corporate LDAP server. + +```yaml +catalog: + locations: + - type: ldap-org + target: ldaps://ds.example.net + processors: + ldapOrg: + providers: + - target: ldaps://ds.example.net + bind: + dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net + secret: + $env: LDAP_SECRET + users: + dn: ou=people,ou=example,dc=example,dc=net + options: + filter: (uid=*) + map: + description: l + set: + metadata.customField: 'hello' + groups: + dn: ou=access,ou=groups,ou=example,dc=example,dc=net + options: + filter: (&(objectClass=some-group-class)(!(groupType=email))) + map: + description: l + set: + metadata.customField: 'hello' +``` + +Locations point out the specific org(s) you want to import. The `type` of these +locations must be `ldap-org`, and the `target` must point to the exact URL +(starting with `ldap://` or `ldaps://`) of the targeted LDAP server. You can +have several such location entries if you want, but typically you will have just +one. + +The processor itself is configured in the other block, under +`catalog.processors.ldapOrg`. There may be many providers, each targeting a +specific `target` which is supposed to be on the same form as the location +`target`. + +These config blocks have a lot of options in them, so we will describe each +"root" key within the block separately. + +### target + +This is the URL of the targeted server, typically on the form +`ldaps://ds.example.net` for SSL enabled servers or `ldap://ds.example.net` +without SSL. + +### bind + +The bind block specifies how the plugin should bind (essentially, to +authenticate) towards the server. It has the following fields. + +```yaml +dn: uid=ldap-reader-user,ou=people,ou=example,dc=example,dc=net +secret: + $env: LDAP_SECRET +``` + +The `dn` is the full LDAP DN (distinguished name) for the user that the plugin +authenticates itself as. At this point, only regular user based authentication +is supported. + +The `secret` is the password of the same user. In this example, it is given in +the form of an environment variable `LDAP_SECRET`, that has to be set when the +backend starts. + +### users + +The `users` block defines the settings that govern the reading and +interpretation of users. Its fields are explained in separate sections below. + +### users.dn + +The DN under which users are stored, e.g. +`ou=people,ou=example,dc=example,dc=net`. + +### users.options + +The search options to use when sending the query to the server, when reading all +users. All of the options are shown below, with their default values, but they +are all optional. + +```yaml +options: + # One of 'base', 'one', or 'sub'. + scope: one + # The filter is the one that you commonly will want to specify explicitly. It + # is a string on the standard LDAP query format. Use it to select out the set + # of users that are of actual interest to ingest. For example, you may want + # to filter out disabled users. + filter: (uid=*) + # The attribute selectors for each item, as passed to the LDAP server. + attributes: ['*', '+'] + # This field is either 'false' to disable paging when reading from the + # server, or an object on the form '{ pageSize: 100, pagePause: true }' that + # specifies the details of how the paging shall work. + paged: false +``` + +### users.set + +This optional piece lets you specify a number of JSON paths (on a.b.c form) and +hard coded values to set on those paths. This can be useful for example if you +want to hard code a namespace or similar on the generated entities. + +```yaml +set: + # Just an example; the key and value can be anything + metadata.namespace: 'ldap' +``` + +### users.map + +Mappings from well known entity fields, to LDAP attribute names. This is where +you are able to define how to interpret the attributes of each LDAP result item, +and to move them into the corresponding entity fields. All of the options are +shown below, with their default values, but they are all optional. + +If you leave out an optional mapping, it will still be copied using that default +value. For example, even if you do not put in the field `displayName` in your +config, the processor will still copy the attribute `cn` into the entity field +`spec.profile.displayName`. + +```yaml +map: + # The name of the attribute that holds the relative + # distinguished name of each entry. + rdn: uid + # The name of the attribute that shall be used for the value of + # the metadata.name field of the entity. + name: uid + # The name of the attribute that shall be used for the value of + # the metadata.description field of the entity. + description: description + # The name of the attribute that shall be used for the value of + # the spec.profile.displayName field of the entity. + displayName: cn + # The name of the attribute that shall be used for the value of + # the spec.profile.email field of the entity. + email: mail + # The name of the attribute that shall be used for the value of + # the spec.profile.picture field of the entity. + picture: + # The name of the attribute that shall be used for the values of + # the spec.memberOf field of the entity. + memberOf: memberOf +``` + +### groups + +The `groups` block defines the settings that govern the reading and +interpretation of groups. Its fields are explained in separate sections below. + +### groups.dn + +The DN under which groups are stored, e.g. +`ou=people,ou=example,dc=example,dc=net`. + +### groups.options + +The search options to use when sending the query to the server, when reading all +groups. All of the options are shown below, with their default values, but they +are all optional. + +```yaml +options: + # One of 'base', 'one', or 'sub'. + scope: one + # The filter is the one that you commonly will want to specify explicitly. It + # is a string on the standard LDAP query format. Use it to select out the set + # of groups that are of actual interest to ingest. For example, you may want + # to filter out disabled groups. + filter: (&(objectClass=some-group-class)(!(groupType=email))) + # The attribute selectors for each item, as passed to the LDAP server. + attributes: ['*', '+'] + # This field is either 'false' to disable paging when reading from the + # server, or an object on the form '{ pageSize: 100, pagePause: true }' that + # specifies the details of how the paging shall work. + paged: false +``` + +### groups.set + +This optional piece lets you specify a number of JSON paths (on a.b.c form) and +hard coded values to set on those paths. This can be useful for example if you +want to hard code a namespace or similar on the generated entities. + +```yaml +set: + # Just an example; the key and value can be anything + metadata.namespace: 'ldap' +``` + +### groups.map + +Mappings from well known entity fields, to LDAP attribute names. This is where +you are able to define how to interpret the attributes of each LDAP result item, +and to move them into the corresponding entity fields. All of the options are +shown below, with their default values, but they are all optional. + +If you leave out an optional mapping, it will still be copied using that default +value. For example, even if you do not put in the field `displayName` in your +config, the processor will still copy the attribute `cn` into the entity field +`spec.profile.displayName`. If the target field is optional, such as the display +name, the importer will accept missing attributes and just leave the target +field unset. If the target field is mandatory, such as the name of the entity, +validation will fail if the source attribute is missing. + +```yaml +map: + # The name of the attribute that holds the relative + # distinguished name of each entry. This value is copied into a + # well known annotation to be able to query by it later. + rdn: cn + # The name of the attribute that shall be used for the value of + # the metadata.name field of the entity. + name: cn + # The name of the attribute that shall be used for the value of + # the metadata.description field of the entity. + description: description + # The name of the attribute that shall be used for the value of + # the spec.type field of the entity. + type: groupType + # The name of the attribute that shall be used for the value of + # the spec.profile.displayName field of the entity. + displayName: cn + # The name of the attribute that shall be used for the value of + # the spec.profile.email field of the entity. + email: + # The name of the attribute that shall be used for the value of + # the spec.profile.picture field of the entity. + picture: + # The name of the attribute that shall be used for the values of + # the spec.parent field of the entity. + memberOf: memberOf + # The name of the attribute that shall be used for the values of + # the spec.children field of the entity. + members: member +``` diff --git a/docs/plugins/observability.md b/docs/plugins/observability.md index 8b6668606f..de6f4bf988 100644 --- a/docs/plugins/observability.md +++ b/docs/plugins/observability.md @@ -10,18 +10,8 @@ Backstage integrator. ## Google Analytics -There is a basic Google Analytics integration built into Backstage. You can -enable it by adding the following to your app configuration: - -```yaml -app: - googleAnalyticsTrackingId: UA-000000-0 -``` - -Replace the tracking ID with your own. - -For more information, learn about Google Analytics -[here](https://marketingplatform.google.com/about/analytics/). +See how to install Google Analytics in your app +[here](../integrations/google-analytics/installation.md) ## Logging diff --git a/docs/plugins/testing.md b/docs/plugins/testing.md index 30c3bf0ecc..bd6061be19 100644 --- a/docs/plugins/testing.md +++ b/docs/plugins/testing.md @@ -274,7 +274,7 @@ export { }; ``` -**`./\_\_mocks\_\_/MyApi.js`** +**`./__mocks__/MyApi.js`** ```js export { diff --git a/docs/support/project-structure.md b/docs/support/project-structure.md index 5c8a8cd3bb..55a92da571 100644 --- a/docs/support/project-structure.md +++ b/docs/support/project-structure.md @@ -6,8 +6,8 @@ description: Introduction to files and folders in the Backstage Project reposito --- Backstage is a complex project, and the GitHub repository contains many -different files and folders. This document aims to clarify what purpose of those -files and folders are. +different files and folders. This document aims to clarify the purpose of those +files and folders. ## General purpose files and folders @@ -25,7 +25,7 @@ the code. Standard GitHub folder. It contains - amongst other things - our workflow definitions and templates. Worth noting is the [styles](https://github.com/backstage/backstage/tree/master/.github/styles) - folder which is used for a markdown spellchecker. + sub-folder which is used for a markdown spellchecker. - [`.yarn/`](https://github.com/backstage/backstage/tree/master/.yarn) - Backstage ships with it's own `yarn` implementation. This allows us to have @@ -33,7 +33,7 @@ the code. yarn versioning differences. - [`contrib/`](https://github.com/backstage/backstage/tree/master/contrib) - - Collection of examples or resources provided by the community. We really + Collection of examples or resources contributed by the community. We really appreciate contributions in here and encourage them being kept up to date. - [`docs/`](https://github.com/backstage/backstage/tree/master/docs) - This is @@ -43,10 +43,11 @@ the code. file may be needed as sections are added/removed. - [`.editorconfig`](https://github.com/backstage/backstage/tree/master/.editorconfig) - - A configuration file used by most common code editors. + A configuration file used by most common code editors. Learn more at + [EditorConfig.org](https://editorconfig.org/). - [`.imgbotconfig`](https://github.com/backstage/backstage/tree/master/.imgbotconfig) - - Configuration for a [bot](https://imgbot.net/) + Configuration for a [bot](https://imgbot.net/) which helps reduce image sizes. ## Monorepo packages @@ -103,16 +104,16 @@ are separated out into their own folder, see further down. diff, create-plugins and more. In the early days of this project, we started out with calling tools directly - such as `eslint` - through `package.json`. But as it was tricky to have a good development experience around that when we - change named tooling, we opted for wrapping those in our own cli. That way + change named tooling, we opted for wrapping those in our own CLI. That way everything looks the same in `package.json`. Much like [react-scripts](https://github.com/facebook/create-react-app/tree/master/packages/react-scripts). - [`cli-common/`](https://github.com/backstage/backstage/tree/master/packages/cli-common) - This package mainly handles path resolving. It is a separate package to reduce bugs in - [cli](https://github.com/backstage/backstage/tree/master/packages/cli). We + [CLI](https://github.com/backstage/backstage/tree/master/packages/cli). We also want as few dependencies as possible to reduce download time when running - the cli which is another reason this is a separate package. + the CLI which is another reason this is a separate package. - [`config/`](https://github.com/backstage/backstage/tree/master/packages/config) - The way we read configuration data. This package can take a bunch of config @@ -139,14 +140,6 @@ are separated out into their own folder, see further down. detail that we try to hide from our users, and no one should have to depend on it directly. -- [`test-utils/`](https://github.com/backstage/backstage/tree/master/packages/test-utils) - - This package contains specific testing facilities used when testing - `core-api`. - -- [`test-utils-core/`](https://github.com/backstage/backstage/tree/master/packages/test-utils-core) - - This package contains more general purpose testing facilities for testing a - Backstage App. - - [`create-app/`](https://github.com/backstage/backstage/tree/master/packages/create-app) - An CLI to specifically scaffold a new Backstage App. It does so by using a [template](https://github.com/backstage/backstage/tree/master/packages/create-app/templates/default-app). @@ -161,26 +154,30 @@ are separated out into their own folder, see further down. to read out definitions and generate documentation for it. - [`e2e-test/`](https://github.com/backstage/backstage/tree/master/packages/e2e-test) - - Another CLI that can be run to try out what would happen if you built all the - packages, publish them, created a new app, and the run it. CI uses this for + Another CLI that can be run to try out what would happen if you build all the + packages, publish them, create a new app, and then run them. CI uses this for e2e-tests. - [`integration/`](https://github.com/backstage/backstage/tree/master/packages/integration) - Common functionalities of integrations like GitHub, GitLab, etc. - [`storybook/`](https://github.com/backstage/backstage/tree/master/packages/storybook) - - This folder contains only the storybook config. Stories are within the core - package. The Backstage Storybook is found - [here](https://backstage.io/storybook) + This folder contains only the Storybook config which helps visualize our + reusable React components. Stories are within the core package, and are + published in the [Backstage Storybook](https://backstage.io/storybook). - [`techdocs-common/`](https://github.com/backstage/backstage/tree/master/packages/techdocs-common) - Common functionalities for TechDocs, to be shared between [techdocs-backend](https://github.com/backstage/backstage/tree/master/plugins/techdocs-backend) plugin and [techdocs-cli](https://github.com/backstage/techdocs-cli). -- [`test-utils-core/`](https://github.com/backstage/backstage/tree/master/packages/test-utils-core) +- [`test-utils/`](https://github.com/backstage/backstage/tree/master/packages/test-utils) - + This package contains specific testing facilities used when testing + `core-api`. -- [`test-utils/`](https://github.com/backstage/backstage/tree/master/packages/test-utils) +- [`test-utils-core/`](https://github.com/backstage/backstage/tree/master/packages/test-utils-core) - + This package contains more general purpose testing facilities for testing a + Backstage App. - [`theme/`](https://github.com/backstage/backstage/tree/master/packages/theme) - Holds the Backstage Theme. @@ -196,10 +193,10 @@ We can categorize plugins into three different types; **Frontend**, **Backend** and **GraphQL**. We differentiate these types of plugins when we name them, with a dash-suffix. `-backend` means it’s a backend plugin and so on. -One reason for splitting a plugin is because of to it's dependencies. Another -reason is for clear separation of concerns. +One reason for splitting a plugin is because of its dependencies. Another reason +is for clear separation of concerns. -Take a look at our [Plugin Gallery](https://backstage.io/plugins) or browse +Take a look at our [Plugin Marketplace](https://backstage.io/plugins) or browse through the [`plugins/`](https://github.com/backstage/backstage/tree/master/plugins) folder. @@ -212,7 +209,7 @@ monorepo setup. This folder contains the source code for backstage.io. It is built with [Docusaurus](https://docusaurus.io/). This folder is not part of the monorepo due to dependency reasons. Look at the - [README](https://github.com/backstage/backstage/blob/master/microsite/README.md) + [microsite README](https://github.com/backstage/backstage/blob/master/microsite/README.md) for instructions on how to run it locally. ## Root files specifically used by the `app` @@ -222,8 +219,8 @@ Some of these files may be subject to be moved out of the root sometime in the future. - [`.npmrc`](https://github.com/backstage/backstage/tree/master/.npmrc) - It's - common for companies to have their own npm registry, this files makes sure - that this folder use the public registry. + common for companies to have their own npm registry, and this file makes sure + that this folder always uses the public registry. - [`.vale.ini`](https://github.com/backstage/backstage/tree/master/.vale.ini) - [Spell checker](https://github.com/errata-ai/vale) for Markdown files. diff --git a/microsite/data/plugins/splunk-on-call.yaml b/microsite/data/plugins/splunk-on-call.yaml index 2d6deb0180..15ceb86a1b 100644 --- a/microsite/data/plugins/splunk-on-call.yaml +++ b/microsite/data/plugins/splunk-on-call.yaml @@ -5,7 +5,7 @@ authorUrl: https://github.com/spotify category: Monitoring description: Splunk On-Call offers a simple way to identify incidents and escalation policies. documentation: https://github.com/backstage/backstage/tree/master/plugins/splunk-on-call -iconUrl: data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMiIgaGVpZ2h0PSIzMiIgdmlld0JveD0iMCAwIDMyIDMyIiBjbGFzcz0idm8tbGVhZi1ibGFjayI+PHBhdGggZD0iTTE1Ljk5OTcgMzJDMjQuODM2MSAzMiAzMiAyNC44MzY4IDMyIDE2LjAwMDJDMzIgNy4xNjM1NiAyNC44MzYxIDAgMTUuOTk5NyAwQzcuMTYzNCAwIDAgNy4xNjM1NiAwIDE2LjAwMDJDMCAyNC44MzY4IDcuMTYzNCAzMiAxNS45OTk3IDMyWk0yMi41Njc1IDE0LjY5MDRMMjMuNjI1OSAxNi41MTU4TDE4Ljg4NTEgMTguOTE0NUwxOC44MSAxOS43MTQxTDIyLjQwMzMgMTguNTc2MUwyMC41OTcyIDIwLjg0MzJMMjAuNDQ2MiAyMS41NjI4TDE4LjI0MzkgMjIuNTUyNUwxOC4wMTc2IDIzLjIzMjJMMTkuMDA0NSAyMi44MTU3TDE2LjM0MjIgMjUuOTEwNFYyOS42MDA0SDE1LjY1NzdWMjUuOTEwNEwxMi45OTU5IDIyLjgxNTdMMTMuOTgyOCAyMy4yMzIyTDEzLjc1NjYgMjIuNTUyNUwxMS41NTQ1IDIxLjU2MjhMMTEuNDAzNiAyMC44NDMyTDkuNTk2OTIgMTguNTc2MUwxMy4xOTA2IDE5LjcxNDFMMTMuMTE0OSAxOC45MTQ1TDguMzc0NDkgMTYuNTE1OEw5LjQzMzM5IDE0LjY5MDRMNy4yMDAyIDExLjUxODlMMTMuMzcxOCAxNC41MDA3TDEzLjQ5NyAxMy42MDM3TDExLjY5MzYgMTAuNjYyTDExLjk1MiAxMC4xNzc0TDExLjExOTUgNi43MTg2N0wxNC4zNTUzIDEwLjEyNjdMMTQuNTQ5MyA4LjI0NDU0TDEzLjU4MTggNS40MjI1M0wxNC44NzMgNS44NTYyNEwxNiAyLjQwMDM5TDE3LjEyNzMgNS44NTYyNEwxOC40MTg2IDUuNDIyNTNMMTcuNDUxMiA4LjI0NDU0TDE3LjY0NTQgMTAuMTI2N0wyMC44ODA3IDYuNzE4NjdMMjAuMDQ4MiAxMC4xNzc0TDIwLjMwNjggMTAuNjYyTDE4LjUwMzQgMTMuNjAzN0wxOC42Mjg4IDE0LjUwMDdMMjQuODAwMiAxMS41MTg5TDIyLjU2NzUgMTQuNjkwNFoiIGZpbGw9JyMyQzJDMkMnLz48L3N2Zz4K +iconUrl: img/splunk-black-white-bg.png npmPackageName: '@backstage/plugin-splunk-on-call' tags: - monitoring diff --git a/microsite/pages/en/demos.js b/microsite/pages/en/demos.js index 47ea1ee9ee..86cb6c786d 100644 --- a/microsite/pages/en/demos.js +++ b/microsite/pages/en/demos.js @@ -21,6 +21,8 @@ const Background = props => { To explore the UI and basic features of Backstage firsthand, go to: demo.backstage.io. + (Tip: click “All” to view all the example components in the + service catalog.) Watch the videos below to get an introduction to Backstage and to diff --git a/microsite/sidebars.json b/microsite/sidebars.json index ebce8d900c..799433b8ef 100644 --- a/microsite/sidebars.json +++ b/microsite/sidebars.json @@ -13,7 +13,7 @@ "Getting Started": [ "getting-started/index", "getting-started/running-backstage-locally", - "getting-started/development-environment", + "getting-started/contributing", "getting-started/create-an-app", { "type": "subcategory", @@ -27,6 +27,7 @@ "type": "subcategory", "label": "Deployment", "ids": [ + "getting-started/deployment-docker", "getting-started/deployment-k8s", "getting-started/deployment-helm", "getting-started/deployment-other" @@ -101,6 +102,23 @@ ] } ], + "Integrations": [ + { + "type": "subcategory", + "label": "GitHub", + "ids": ["integrations/github/org"] + }, + { + "type": "subcategory", + "label": "LDAP", + "ids": ["integrations/ldap/org"] + }, + { + "type": "subcategory", + "label": "Google Analytics", + "ids": ["integrations/google-analytics/installation"] + } + ], "Plugins": [ "plugins/index", "plugins/existing-plugins", diff --git a/microsite/static/img/splunk-black-white-bg.png b/microsite/static/img/splunk-black-white-bg.png new file mode 100644 index 0000000000..76eb7ef0f8 Binary files /dev/null and b/microsite/static/img/splunk-black-white-bg.png differ diff --git a/mkdocs.yml b/mkdocs.yml index a303fd9978..6b1971ce03 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,7 +16,7 @@ nav: - Getting started: - Getting Started: 'getting-started/index.md' - Running Backstage locally: 'getting-started/running-backstage-locally.md' - - Local development: 'getting-started/development-environment.md' + - Contributors: 'getting-started/contributors.md' - Demo deployment: https://backstage-demo.roadie.io - Production deployments: - Create an App: 'getting-started/create-an-app.md' @@ -24,6 +24,7 @@ nav: - Configuring App with plugins: 'getting-started/configure-app-with-plugins.md' - Customize the look-and-feel of your App: 'getting-started/app-custom-theme.md' - Deployment scenarios: + - Docker: 'getting-started/deployment-docker.md' - Kubernetes: 'getting-started/deployment-k8s.md' - Kubernetes and Helm: 'getting-started/deployment-helm.md' - Other: 'getting-started/deployment-other.md' @@ -64,6 +65,13 @@ nav: - FAQ: 'features/techdocs/FAQ.md' - Kubernetes: - Overview: 'features/kubernetes/index.md' + - Integrations: + - GitHub: + - Org Data: 'integrations/github/org.md' + - LDAP: + - Org Data: 'integrations/ldap/org.md' + - Google Analytics: + - Installation: 'integrations/google-analytics/installation.md' - Plugins: - Overview: 'plugins/index.md' - Existing plugins: 'plugins/existing-plugins.md' diff --git a/package.json b/package.json index 59fe9d8f18..527304b4f3 100644 --- a/package.json +++ b/package.json @@ -20,10 +20,10 @@ "lint:all": "lerna run lint --", "lint:type-deps": "node scripts/check-type-dependencies.js", "docgen": "lerna run docgen", - "docker-build": "yarn tsc && yarn workspace example-backend build-image", + "docker-build": "yarn tsc && yarn workspace example-backend build --build-dependencies && yarn workspace example-backend build-image", "create-plugin": "backstage-cli create-plugin --scope backstage --no-private", "remove-plugin": "backstage-cli remove-plugin", - "release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install --frozen-lockfile", + "release": "changeset version && yarn diff --yes && yarn prettier --write '{packages,plugins}/*/{package.json,CHANGELOG.md}' && yarn install", "prettier:check": "prettier --check .", "lerna": "lerna", "storybook": "yarn workspace storybook start", diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md index 6190dda9da..f9166939c6 100644 --- a/packages/app/CHANGELOG.md +++ b/packages/app/CHANGELOG.md @@ -1,5 +1,112 @@ # example-app +## 0.2.17 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [a5f42cf66] +- Updated dependencies [38205492a] +- Updated dependencies [e488f0502] +- Updated dependencies [e799e74d4] +- Updated dependencies [e3bc5aad7] +- Updated dependencies [a5f42cf66] +- Updated dependencies [a8953a9c9] +- Updated dependencies [f37992797] +- Updated dependencies [347137ccf] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [d6593abe6] +- Updated dependencies [bad21a085] +- Updated dependencies [e8e35fb5f] +- Updated dependencies [9615e68fb] +- Updated dependencies [e780e119c] +- Updated dependencies [437bac549] +- Updated dependencies [9f2b3a26e] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [968b588f7] +- Updated dependencies [3a58084b6] +- Updated dependencies [2499f6cde] +- Updated dependencies [5469a9761] +- Updated dependencies [60d1bc3e7] +- Updated dependencies [2c1f2a7c2] +- Updated dependencies [6266ddd11] + - @backstage/core@0.6.3 + - @backstage/plugin-scaffolder@0.6.0 + - @backstage/plugin-cost-insights@0.8.2 + - @backstage/plugin-org@0.3.8 + - @backstage/plugin-catalog@0.4.0 + - @backstage/plugin-catalog-import@0.4.2 + - @backstage/plugin-techdocs@0.5.8 + - @backstage/plugin-explore@0.2.7 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/plugin-api-docs@0.4.7 + - @backstage/catalog-model@0.7.2 + - @backstage/cli@0.6.2 + - @backstage/plugin-tech-radar@0.3.6 + - @backstage/plugin-circleci@0.2.10 + - @backstage/plugin-cloudbuild@0.2.11 + - @backstage/plugin-github-actions@0.3.4 + - @backstage/plugin-jenkins@0.3.11 + - @backstage/plugin-kafka@0.2.4 + - @backstage/plugin-kubernetes@0.3.11 + - @backstage/plugin-lighthouse@0.2.12 + - @backstage/plugin-pagerduty@0.3.1 + - @backstage/plugin-register-component@0.2.11 + - @backstage/plugin-rollbar@0.3.2 + - @backstage/plugin-search@0.3.2 + - @backstage/plugin-sentry@0.3.7 + +## 0.2.16 + +### Patch Changes + +- Updated dependencies [6c4a76c59] +- Updated dependencies [32a950409] +- Updated dependencies [f10950bd2] +- Updated dependencies [914c89b13] +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [257a753ff] +- Updated dependencies [d872f662d] +- Updated dependencies [9337f509d] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [e8692df4a] +- Updated dependencies [53b69236d] +- Updated dependencies [549a859ac] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [532bc0ec0] +- Updated dependencies [688b73110] + - @backstage/plugin-scaffolder@0.5.1 + - @backstage/plugin-catalog@0.3.2 + - @backstage/core@0.6.2 + - @backstage/cli@0.6.1 + - @backstage/plugin-user-settings@0.2.6 + - @backstage/plugin-catalog-react@0.0.4 + - @backstage/plugin-api-docs@0.4.6 + - @backstage/plugin-catalog-import@0.4.1 + - @backstage/plugin-github-actions@0.3.3 + - @backstage/plugin-jenkins@0.3.10 + - @backstage/plugin-lighthouse@0.2.11 + - @backstage/plugin-org@0.3.7 + - @backstage/plugin-sentry@0.3.6 + - @backstage/plugin-pagerduty@0.3.0 + - @backstage/plugin-circleci@0.2.9 + - @backstage/plugin-cloudbuild@0.2.10 + - @backstage/plugin-explore@0.2.6 + - @backstage/plugin-kafka@0.2.3 + - @backstage/plugin-kubernetes@0.3.10 + - @backstage/plugin-register-component@0.2.10 + - @backstage/plugin-rollbar@0.3.1 + - @backstage/plugin-search@0.3.1 + - @backstage/plugin-techdocs@0.5.7 + ## 0.2.15 ### Patch Changes diff --git a/packages/app/package.json b/packages/app/package.json index 88d039e7c8..4882662016 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -1,46 +1,46 @@ { "name": "example-app", - "version": "0.2.15", + "version": "0.2.17", "private": true, "bundled": true, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/cli": "^0.6.0", - "@backstage/core": "^0.6.1", - "@backstage/plugin-api-docs": "^0.4.5", - "@backstage/plugin-catalog": "^0.3.1", - "@backstage/plugin-catalog-react": "^0.0.3", - "@backstage/plugin-catalog-import": "^0.4.0", - "@backstage/plugin-circleci": "^0.2.8", - "@backstage/plugin-cloudbuild": "^0.2.9", - "@backstage/plugin-cost-insights": "^0.8.1", - "@backstage/plugin-explore": "^0.2.5", + "@backstage/catalog-model": "^0.7.2", + "@backstage/cli": "^0.6.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-api-docs": "^0.4.7", + "@backstage/plugin-catalog": "^0.4.0", + "@backstage/plugin-catalog-import": "^0.4.2", + "@backstage/plugin-catalog-react": "^0.1.0", + "@backstage/plugin-circleci": "^0.2.10", + "@backstage/plugin-cloudbuild": "^0.2.11", + "@backstage/plugin-cost-insights": "^0.8.2", + "@backstage/plugin-explore": "^0.2.7", "@backstage/plugin-gcp-projects": "^0.2.4", - "@backstage/plugin-github-actions": "^0.3.2", + "@backstage/plugin-github-actions": "^0.3.4", "@backstage/plugin-gitops-profiles": "^0.2.5", "@backstage/plugin-graphiql": "^0.2.7", - "@backstage/plugin-org": "^0.3.6", - "@backstage/plugin-jenkins": "^0.3.9", - "@backstage/plugin-kafka": "^0.2.2", - "@backstage/plugin-kubernetes": "^0.3.9", - "@backstage/plugin-lighthouse": "^0.2.10", + "@backstage/plugin-jenkins": "^0.3.11", + "@backstage/plugin-kafka": "^0.2.4", + "@backstage/plugin-kubernetes": "^0.3.11", + "@backstage/plugin-lighthouse": "^0.2.12", "@backstage/plugin-newrelic": "^0.2.5", - "@backstage/plugin-pagerduty": "0.2.8", - "@backstage/plugin-register-component": "^0.2.9", - "@backstage/plugin-rollbar": "^0.3.0", - "@backstage/plugin-scaffolder": "^0.5.0", - "@backstage/plugin-sentry": "^0.3.5", - "@backstage/plugin-search": "^0.3.0", - "@backstage/plugin-tech-radar": "^0.3.5", - "@backstage/plugin-techdocs": "^0.5.6", - "@backstage/plugin-user-settings": "^0.2.5", + "@backstage/plugin-org": "^0.3.8", + "@backstage/plugin-pagerduty": "0.3.1", + "@backstage/plugin-register-component": "^0.2.11", + "@backstage/plugin-rollbar": "^0.3.2", + "@backstage/plugin-scaffolder": "^0.6.0", + "@backstage/plugin-sentry": "^0.3.7", + "@backstage/plugin-search": "^0.3.2", + "@backstage/plugin-tech-radar": "^0.3.6", + "@backstage/plugin-techdocs": "^0.5.8", + "@backstage/plugin-user-settings": "^0.2.6", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@octokit/rest": "^18.0.12", "@roadiehq/backstage-plugin-buildkite": "^0.1.3", "@roadiehq/backstage-plugin-github-insights": "^0.2.16", - "@roadiehq/backstage-plugin-github-pull-requests": "^0.6.3", + "@roadiehq/backstage-plugin-github-pull-requests": "^0.7.6", "@roadiehq/backstage-plugin-travis-ci": "^0.2.8", "history": "^5.0.0", "prop-types": "^15.7.2", @@ -53,7 +53,7 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/test-utils": "^0.1.7", + "@backstage/test-utils": "^0.1.8", "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index cc673c7769..9a7c857167 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -17,20 +17,23 @@ import { AlertDisplay, createApp, - createRouteRef, FlatRoutes, OAuthRequestDialog, SignInPage, } from '@backstage/core'; -import { Router as CatalogRouter } from '@backstage/plugin-catalog'; +import { + catalogPlugin, + CatalogIndexPage, + CatalogEntityPage, +} from '@backstage/plugin-catalog'; import { CatalogImportPage } from '@backstage/plugin-catalog-import'; import { ExplorePage } from '@backstage/plugin-explore'; -import { Router as GraphiQLRouter } from '@backstage/plugin-graphiql'; -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 { GraphiQLPage } from '@backstage/plugin-graphiql'; +import { LighthousePage } from '@backstage/plugin-lighthouse'; +import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { TechRadarPage } from '@backstage/plugin-tech-radar'; +import { TechdocsPage } from '@backstage/plugin-techdocs'; +import { UserSettingsPage } from '@backstage/plugin-user-settings'; import React from 'react'; import { hot } from 'react-hot-loader/root'; import { Navigate, Route } from 'react-router'; @@ -40,6 +43,15 @@ import Root from './components/Root'; import { providers } from './identityProviders'; import * as plugins from './plugins'; import AlarmIcon from '@material-ui/icons/Alarm'; +import { ApiExplorerPage } from '@backstage/plugin-api-docs'; +import { GcpProjectsPage } from '@backstage/plugin-gcp-projects'; +import { NewRelicPage } from '@backstage/plugin-newrelic'; +import { SearchPage } from '@backstage/plugin-search'; +import { + CostInsightsLabelDataflowInstructionsPage, + CostInsightsPage, + CostInsightsProjectGrowthInstructionsPage, +} from '@backstage/plugin-cost-insights'; const app = createApp({ apis, @@ -60,39 +72,50 @@ const app = createApp({ ); }, }, + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + }, }); const AppProvider = app.getProvider(); const AppRouter = app.getRouter(); -const deprecatedAppRoutes = app.getRoutes(); - -const catalogRouteRef = createRouteRef({ - path: '/catalog', - title: 'Service Catalog', -}); const routes = ( - } /> + } /> } - /> - } /> + path="/catalog/:namespace/:kind/:name" + element={} + > + + + } /> + } /> + } /> } /> } + element={} /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + } /> + } /> } + path="/cost-insights/investigating-growth" + element={} /> - } /> - {...deprecatedAppRoutes} + } + /> + } /> ); diff --git a/packages/app/src/components/catalog/EntityPage.tsx b/packages/app/src/components/catalog/EntityPage.tsx index 7fb624b9e9..449d018fa3 100644 --- a/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/app/src/components/catalog/EntityPage.tsx @@ -39,7 +39,7 @@ import { EntityLinksCard, EntityPageLayout, } from '@backstage/plugin-catalog'; -import { useEntity } from '@backstage/plugin-catalog-react'; +import { EntityProvider, useEntity } from '@backstage/plugin-catalog-react'; import { isPluginApplicableToEntity as isCircleCIAvailable, Router as CircleCIRouter, @@ -184,7 +184,9 @@ const ComponentOverviewContent = ({ entity }: { entity: Entity }) => ( {isPagerDutyAvailable(entity) && ( - + + + )} diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index c62c0435a2..862d821ed6 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -14,8 +14,8 @@ * limitations under the License. */ export { plugin as LighthousePlugin } from '@backstage/plugin-lighthouse'; -export { plugin as CatalogPlugin } from '@backstage/plugin-catalog'; -export { plugin as ScaffolderPlugin } from '@backstage/plugin-scaffolder'; +export { catalogPlugin } from '@backstage/plugin-catalog'; +export { scaffolderPlugin } from '@backstage/plugin-scaffolder'; export { plugin as TechRadar } from '@backstage/plugin-tech-radar'; export { explorePlugin } from '@backstage/plugin-explore'; export { plugin as Circleci } from '@backstage/plugin-circleci'; diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md index 42f1fa4a42..bb913c8fdb 100644 --- a/packages/backend-common/CHANGELOG.md +++ b/packages/backend-common/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/backend-common +## 0.5.4 + +### Patch Changes + +- 16fb1d03a: pass registered logger to requestLoggingHandler +- 491f3a0ec: Implement `UrlReader.search` for the other providers (Azure, Bitbucket, GitLab) as well. + + The `UrlReader` subclasses now are implemented in terms of the respective `Integration` class. + +- 434b4e81a: Support globs in `FileReaderProcessor`. +- fb28da212: Switched to using `'x-access-token'` for authenticating Git over HTTPS towards GitHub. +- Updated dependencies [491f3a0ec] + - @backstage/integration@0.5.0 + ## 0.5.3 ### Patch Changes diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json index 7b1c6870ab..3cd817427f 100644 --- a/packages/backend-common/package.json +++ b/packages/backend-common/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/backend-common", "description": "Common functionality library for Backstage backends", - "version": "0.5.3", + "version": "0.5.4", "main": "src/index.ts", "types": "src/index.ts", "private": false, @@ -32,7 +32,7 @@ "@backstage/cli-common": "^0.1.1", "@backstage/config": "^0.1.2", "@backstage/config-loader": "^0.5.1", - "@backstage/integration": "^0.4.0", + "@backstage/integration": "^0.5.0", "@octokit/rest": "^18.0.12", "@types/cors": "^2.8.6", "@types/express": "^4.17.6", @@ -68,7 +68,7 @@ } }, "devDependencies": { - "@backstage/cli": "^0.6.0", + "@backstage/cli": "^0.6.1", "@backstage/test-utils": "^0.1.7", "@types/archiver": "^5.1.0", "@types/compression": "^1.7.0", diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md index 05549b2085..84e51d6bcf 100644 --- a/packages/backend/CHANGELOG.md +++ b/packages/backend/CHANGELOG.md @@ -1,5 +1,27 @@ # example-backend +## 0.2.17 + +### Patch Changes + +- Updated dependencies [a70af22a2] +- Updated dependencies [ec504e7b4] +- Updated dependencies [a5f42cf66] +- Updated dependencies [f37992797] +- Updated dependencies [bad21a085] +- Updated dependencies [1c06cb312] +- Updated dependencies [2499f6cde] +- Updated dependencies [a1f5e6545] + - @backstage/plugin-kubernetes-backend@0.2.7 + - @backstage/plugin-auth-backend@0.3.2 + - @backstage/plugin-scaffolder-backend@0.8.0 + - @backstage/plugin-techdocs-backend@0.6.2 + - @backstage/catalog-model@0.7.2 + - @backstage/plugin-app-backend@0.3.8 + - @backstage/plugin-catalog-backend@0.6.3 + - @backstage/config@0.1.3 + - example-app@0.2.17 + ## 0.2.15 ### Patch Changes diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile index f1bc764fd0..acef405c8a 100644 --- a/packages/backend/Dockerfile +++ b/packages/backend/Dockerfile @@ -1,16 +1,26 @@ -FROM node:14-buster +# This dockerfile builds an image for the backend package. +# It should be executed with the root of the repo as docker context. +# +# Before building this image, be sure to have run the following commands in the repo root: +# +# yarn install +# yarn tsc +# yarn build +# +# Once the commands have been run, you can build the image using `yarn build-image` -WORKDIR /usr/src/app +FROM node:14-buster-slim + +WORKDIR /app # Copy repo skeleton first, to avoid unnecessary docker cache invalidation. # The skeleton contains the package.json of each package in the monorepo, # and along with yarn.lock and the root package.json, that's enough to run yarn install. -ADD yarn.lock package.json skeleton.tar ./ +ADD yarn.lock package.json packages/backend/dist/skeleton.tar.gz ./ RUN yarn install --frozen-lockfile --production --network-timeout 300000 && rm -rf "$(yarn cache dir)" -# This will copy the contents of the dist-workspace when running the build-image command. -# Do not use this Dockerfile outside of that command, as it will copy in the source code instead. -COPY . . +# Then copy the rest of the backend bundle, along with any other files we might want. +ADD packages/backend/dist/bundle.tar.gz app-config.yaml ./ -CMD ["node", "packages/backend"] +CMD ["node", "packages/backend", "--config", "app-config.yaml"] diff --git a/packages/backend/package.json b/packages/backend/package.json index e5271f0109..9220b71866 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -1,6 +1,6 @@ { "name": "example-backend", - "version": "0.2.15", + "version": "0.2.17", "main": "dist/index.cjs.js", "types": "src/index.ts", "license": "Apache-2.0", @@ -18,8 +18,8 @@ "backstage" ], "scripts": { - "build": "backstage-cli backend:build", - "build-image": "backstage-cli backend:build-image --build --tag example-backend", + "build": "backstage-cli backend:bundle", + "build-image": "docker build ../.. -f Dockerfile --tag example-backend", "start": "backstage-cli backend:dev", "lint": "backstage-cli lint", "test": "backstage-cli test", @@ -28,23 +28,24 @@ }, "dependencies": { "@backstage/backend-common": "^0.5.3", - "@backstage/catalog-model": "^0.7.1", - "@backstage/config": "^0.1.2", - "@backstage/plugin-app-backend": "^0.3.7", - "@backstage/plugin-auth-backend": "^0.3.0", - "@backstage/plugin-catalog-backend": "^0.6.1", + "@backstage/catalog-client": "^0.3.6", + "@backstage/catalog-model": "^0.7.2", + "@backstage/config": "^0.1.3", + "@backstage/plugin-app-backend": "^0.3.8", + "@backstage/plugin-auth-backend": "^0.3.2", + "@backstage/plugin-catalog-backend": "^0.6.3", "@backstage/plugin-graphql-backend": "^0.1.5", - "@backstage/plugin-kubernetes-backend": "^0.2.6", + "@backstage/plugin-kubernetes-backend": "^0.2.7", "@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.7.0", - "@backstage/plugin-techdocs-backend": "^0.6.0", + "@backstage/plugin-scaffolder-backend": "^0.8.0", + "@backstage/plugin-techdocs-backend": "^0.6.2", "@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.15", + "example-app": "^0.2.17", "express": "^4.17.1", "express-promise-router": "^3.0.3", "knex": "^0.21.6", @@ -54,7 +55,7 @@ "winston": "^3.2.1" }, "devDependencies": { - "@backstage/cli": "^0.6.0", + "@backstage/cli": "^0.6.2", "@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 723165ff82..10da5f0e15 100644 --- a/packages/backend/src/plugins/scaffolder.ts +++ b/packages/backend/src/plugins/scaffolder.ts @@ -21,11 +21,11 @@ import { Publishers, CreateReactAppTemplater, Templaters, - CatalogEntityClient, } from '@backstage/plugin-scaffolder-backend'; import { SingleHostDiscovery } from '@backstage/backend-common'; import type { PluginEnvironment } from '../types'; import Docker from 'dockerode'; +import { CatalogClient } from '@backstage/catalog-client'; export default async function createPlugin({ logger, @@ -45,7 +45,7 @@ export default async function createPlugin({ const dockerClient = new Docker(); const discovery = SingleHostDiscovery.fromConfig(config); - const entityClient = new CatalogEntityClient({ discovery }); + const catalogClient = new CatalogClient({ discoveryApi: discovery }); return await createRouter({ preparers, @@ -54,7 +54,7 @@ export default async function createPlugin({ logger, config, dockerClient, - entityClient, database, + catalogClient, }); } diff --git a/packages/catalog-model/CHANGELOG.md b/packages/catalog-model/CHANGELOG.md index 8d581dcb0c..3351ed317a 100644 --- a/packages/catalog-model/CHANGELOG.md +++ b/packages/catalog-model/CHANGELOG.md @@ -1,5 +1,13 @@ # @backstage/catalog-model +## 0.7.2 + +### Patch Changes + +- bad21a085: Implement annotations for customising Entity URLs in the Catalog pages. +- Updated dependencies [a1f5e6545] + - @backstage/config@0.1.3 + ## 0.7.1 ### Patch Changes diff --git a/packages/catalog-model/examples/components/petstore-component.yaml b/packages/catalog-model/examples/components/petstore-component.yaml index 48286f9b74..878fabd551 100644 --- a/packages/catalog-model/examples/components/petstore-component.yaml +++ b/packages/catalog-model/examples/components/petstore-component.yaml @@ -2,7 +2,8 @@ apiVersion: backstage.io/v1alpha1 kind: Component metadata: name: petstore - description: Petstore + # This is an extra long description + description: The Petstore is an example API used to show features of the OpenAPI spec. links: - url: https://github.com/swagger-api/swagger-petstore title: GitHub Repo diff --git a/packages/catalog-model/package.json b/packages/catalog-model/package.json index af74b78a08..554eea0937 100644 --- a/packages/catalog-model/package.json +++ b/packages/catalog-model/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/catalog-model", - "version": "0.7.1", + "version": "0.7.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.2", + "@backstage/config": "^0.1.3", "@types/json-schema": "^7.0.5", "@types/yup": "^0.29.8", "ajv": "^7.0.3", @@ -39,7 +39,7 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", + "@backstage/cli": "^0.6.2", "@types/express": "^4.17.6", "@types/jest": "^26.0.7", "@types/lodash": "^4.14.151", diff --git a/packages/catalog-model/src/entity/constants.ts b/packages/catalog-model/src/entity/constants.ts index 42ea2ae8ba..c8f88e3b0c 100644 --- a/packages/catalog-model/src/entity/constants.ts +++ b/packages/catalog-model/src/entity/constants.ts @@ -27,3 +27,9 @@ export const ENTITY_META_GENERATED_FIELDS = [ 'etag', 'generation', ] as const; + +/** + * Annotations for linking to entity from catalog pages. + */ +export const VIEW_URL_ANNOTATION = 'backstage.io/view-url'; +export const EDIT_URL_ANNOTATION = 'backstage.io/edit-url'; diff --git a/packages/catalog-model/src/entity/index.ts b/packages/catalog-model/src/entity/index.ts index e80e14f7a4..e267c607e1 100644 --- a/packages/catalog-model/src/entity/index.ts +++ b/packages/catalog-model/src/entity/index.ts @@ -17,6 +17,8 @@ export { ENTITY_DEFAULT_NAMESPACE, ENTITY_META_GENERATED_FIELDS, + VIEW_URL_ANNOTATION, + EDIT_URL_ANNOTATION, } from './constants'; export type { Entity, diff --git a/packages/catalog-model/src/location/annotation.ts b/packages/catalog-model/src/location/annotation.ts index 93f2fabea4..ba875c3edf 100644 --- a/packages/catalog-model/src/location/annotation.ts +++ b/packages/catalog-model/src/location/annotation.ts @@ -17,3 +17,5 @@ export const LOCATION_ANNOTATION = 'backstage.io/managed-by-location'; export const ORIGIN_LOCATION_ANNOTATION = 'backstage.io/managed-by-origin-location'; + +export const SOURCE_LOCATION_ANNOTATION = 'backstage.io/source-location'; diff --git a/packages/catalog-model/src/location/index.ts b/packages/catalog-model/src/location/index.ts index 8fd516120a..6cfb074613 100644 --- a/packages/catalog-model/src/location/index.ts +++ b/packages/catalog-model/src/location/index.ts @@ -20,4 +20,8 @@ export { locationSpecSchema, analyzeLocationSchema, } from './validation'; -export { LOCATION_ANNOTATION, ORIGIN_LOCATION_ANNOTATION } from './annotation'; +export { + LOCATION_ANNOTATION, + ORIGIN_LOCATION_ANNOTATION, + SOURCE_LOCATION_ANNOTATION, +} from './annotation'; diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index c40f7605eb..148af6fa8a 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,22 @@ # @backstage/cli +## 0.6.2 + +### Patch Changes + +- e780e119c: Add missing `file-loader` dependency which could cause issues with loading images and other assets. +- 6266ddd11: The `yarn backstage-cli app:diff` has been broken since a couple of months. The command to perform updates `yarn backstage-cli versions:bump` prints change logs which seems to be a good replacement for this command. +- Updated dependencies [a1f5e6545] + - @backstage/config@0.1.3 + +## 0.6.1 + +### Patch Changes + +- 257a753ff: Updated transform of `.esm.js` files to be able to handle dynamic imports. +- 9337f509d: Tweak error message in lockfile parsing to include more information. +- 532bc0ec0: Upgrading to lerna@4.0.0. + ## 0.6.0 ### Minor Changes diff --git a/packages/cli/config/jest.js b/packages/cli/config/jest.js index 61cd83b7b0..94cf059cbd 100644 --- a/packages/cli/config/jest.js +++ b/packages/cli/config/jest.js @@ -83,10 +83,8 @@ async function getConfig() { }, }, - // We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed - // TODO: jest is working on module support, it's possible that we can remove this in the future transform: { - '\\.esm\\.js$': require.resolve('jest-esm-transformer'), + '\\.esm\\.js$': require.resolve('./jestEsmTransform.js'), // See jestEsmTransform.js '\\.(js|jsx|ts|tsx)$': require.resolve('ts-jest'), '\\.(bmp|gif|jpg|jpeg|png|frag|xml|svg)$': require.resolve( './jestFileTransform.js', diff --git a/packages/cli/config/jestEsmTransform.js b/packages/cli/config/jestEsmTransform.js new file mode 100644 index 0000000000..99f1a600bc --- /dev/null +++ b/packages/cli/config/jestEsmTransform.js @@ -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. + */ + +const babel = require('@babel/core'); + +// We build .esm.js files with plugin:build, so to be able to load these in tests they need to be transformed +// TODO: jest is working on module support, it's possible that we can remove this in the future +module.exports = { + process(src) { + const result = babel.transform(src, { + babelrc: false, + compact: false, + plugins: [ + // This transforms the regular ESM syntax, import and export statements + require.resolve('@babel/plugin-transform-modules-commonjs'), + // This transforms dynamic `import()`, which is not supported yet in the Node.js VM API + require.resolve('babel-plugin-dynamic-import-node'), + ], + }); + + return result.code; + }, +}; diff --git a/packages/cli/package.json b/packages/cli/package.json index e54696ce43..4a95df0f38 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.6.0", + "version": "0.6.2", "private": false, "publishConfig": { "access": "public" @@ -28,8 +28,10 @@ "backstage-cli": "bin/backstage-cli" }, "dependencies": { + "@babel/core": "^7.4.4", + "@babel/plugin-transform-modules-commonjs": "^7.4.4", "@backstage/cli-common": "^0.1.1", - "@backstage/config": "^0.1.2", + "@backstage/config": "^0.1.3", "@backstage/config-loader": "^0.5.1", "@hot-loader/react-dom": "^16.13.0", "@lerna/package-graph": "^4.0.0", @@ -53,6 +55,7 @@ "@typescript-eslint/eslint-plugin": "^v4.14.0", "@typescript-eslint/parser": "^v4.14.0", "@yarnpkg/lockfile": "^1.1.0", + "babel-plugin-dynamic-import-node": "^2.3.3", "bfj": "^7.0.2", "chalk": "^4.0.0", "chokidar": "^3.3.1", @@ -71,6 +74,7 @@ "eslint-plugin-react": "^7.12.4", "eslint-plugin-react-hooks": "^4.0.0", "express": "^4.17.1", + "file-loader": "^6.2.0", "fork-ts-checker-webpack-plugin": "^4.0.5", "fs-extra": "^9.0.0", "handlebars": "^4.7.3", @@ -78,7 +82,6 @@ "inquirer": "^7.0.4", "jest": "^26.0.1", "jest-css-modules": "^2.1.0", - "jest-esm-transformer": "^1.0.0", "lodash": "^4.17.19", "mini-css-extract-plugin": "^0.9.0", "ora": "^4.0.3", @@ -113,11 +116,11 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/backend-common": "^0.5.2", - "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.0", - "@backstage/dev-utils": "^0.1.9", - "@backstage/test-utils": "^0.1.6", + "@backstage/backend-common": "^0.5.4", + "@backstage/config": "^0.1.3", + "@backstage/core": "^0.6.3", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@backstage/theme": "^0.2.3", "@types/diff": "^4.0.2", "@types/express": "^4.17.6", diff --git a/packages/cli/src/commands/app/diff.ts b/packages/cli/src/commands/app/diff.ts deleted file mode 100644 index a667407f23..0000000000 --- a/packages/cli/src/commands/app/diff.ts +++ /dev/null @@ -1,74 +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 { Command } from 'commander'; -import { - diffTemplateFiles, - handlers, - handleAllFiles, - inquirerPromptFunc, - makeCheckPromptFunc, - yesPromptFunc, -} from '../../lib/diff'; -import { version } from '../../lib/version'; - -const fileHandlers = [ - { - patterns: ['packages/app/package.json'], - handler: handlers.appPackageJson, - }, - { - patterns: [/tsconfig\.json$/], - handler: handlers.exactMatch, - }, - { - patterns: [ - /README\.md$/, - /\.eslintrc\.js$/, - // make sure files in 1st level of src/ and dev/ exist - /^packages\/app\/(src|dev)\/[^/]+$/, - ], - handler: handlers.exists, - }, - { - patterns: [ - 'lerna.json', - /^src\//, - /^patches\//, - /^packages\/app\/public\//, - /^packages\/app\/cypress/, - // Let plugin:diff take care of the plugins - /^plugins/, - /package\.json$/, - ], - handler: handlers.skip, - }, -]; - -export default async (cmd: Command) => { - let promptFunc = inquirerPromptFunc; - let finalize = () => {}; - - if (cmd.check) { - [promptFunc, finalize] = makeCheckPromptFunc(); - } else if (cmd.yes) { - promptFunc = yesPromptFunc; - } - - const templateFiles = await diffTemplateFiles('default-app', { version }); - await handleAllFiles(fileHandlers, templateFiles, promptFunc); - await finalize(); -}; diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts index 5fd7817a83..3a8ef36a97 100644 --- a/packages/cli/src/commands/index.ts +++ b/packages/cli/src/commands/index.ts @@ -76,13 +76,6 @@ export function registerCommands(program: CommanderStatic) { .option(...configOption) .action(lazy(() => import('./backend/dev').then(m => m.default))); - program - .command('app:diff') - .option('--check', 'Fail if changes are required') - .option('--yes', 'Apply all changes') - .description('Diff an existing app with the creation template') - .action(lazy(() => import('./app/diff').then(m => m.default))); - program .command('create-plugin') .option( diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 0b024b2c84..b905d3e8a4 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -36,7 +36,7 @@ "fs-extra": "^9.0.0", "json-schema": "^0.2.5", "json-schema-merge-allof": "^0.7.0", - "typescript-json-schema": "^0.47.0", + "typescript-json-schema": "^0.49.0", "yaml": "^1.9.2", "yup": "^0.29.3" }, diff --git a/packages/config/CHANGELOG.md b/packages/config/CHANGELOG.md index 06a9d8e4f1..cfc48fe21a 100644 --- a/packages/config/CHANGELOG.md +++ b/packages/config/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/config +## 0.1.3 + +### Patch Changes + +- a1f5e6545: Adds an optional type to `config.get` & `config.getOptional`. This avoids the need for casting. For example: + + ```ts + const config = useApi(configApiRef); + + const myConfig = config.get('myPlugin.complexConfig'); + // vs + const myConfig config.get('myPlugin.complexConfig') as SomeTypeDefinition; + ``` + ## 0.1.2 ### Patch Changes diff --git a/packages/config/package.json b/packages/config/package.json index 4e6c2f7157..1eadb5d4e5 100644 --- a/packages/config/package.json +++ b/packages/config/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/config", "description": "Config API used by Backstage core, backend, and CLI", - "version": "0.1.2", + "version": "0.1.3", "private": false, "publishConfig": { "access": "public", diff --git a/packages/config/src/reader.ts b/packages/config/src/reader.ts index c690e0bbc9..99977d19cd 100644 --- a/packages/config/src/reader.ts +++ b/packages/config/src/reader.ts @@ -88,22 +88,22 @@ export class ConfigReader implements Config { return [...new Set([...localKeys, ...fallbackKeys])]; } - get(key?: string): JsonValue { + get(key?: string): T { const value = this.getOptional(key); if (value === undefined) { throw new Error(errors.missing(this.fullKey(key ?? ''))); } - return value; + return value as T; } - getOptional(key?: string): JsonValue | undefined { + getOptional(key?: string): T | undefined { const value = this.readValue(key); - const fallbackValue = this.fallback?.getOptional(key); + const fallbackValue = this.fallback?.getOptional(key); if (value === undefined) { return fallbackValue; } else if (fallbackValue === undefined) { - return value; + return value as T; } // Avoid merging arrays and primitive values, since that's how merging works for other @@ -113,7 +113,7 @@ export class ConfigReader implements Config { { value: cloneDeep(fallbackValue) }, { value }, (into, from) => (!isObject(from) || !isObject(into) ? from : undefined), - ).value; + ).value as T; } getConfig(key: string): ConfigReader { diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 840cc36173..92293a3f26 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -34,8 +34,8 @@ export type Config = { keys(): string[]; - get(key?: string): JsonValue; - getOptional(key?: string): JsonValue | undefined; + get(key?: string): T; + getOptional(key?: string): T | undefined; getConfig(key: string): Config; getOptionalConfig(key: string): Config | undefined; diff --git a/packages/core-api/CHANGELOG.md b/packages/core-api/CHANGELOG.md index a40e25b2a8..7fa7e71828 100644 --- a/packages/core-api/CHANGELOG.md +++ b/packages/core-api/CHANGELOG.md @@ -1,5 +1,37 @@ # @backstage/core-api +## 0.2.11 + +### Patch Changes + +- 3a58084b6: The `FlatRoutes` components now renders the not found page of the app if no routes are matched. +- 1407b34c6: More informative error message for missing ApiContext. +- b6c4f485d: Fix error when querying Backstage Identity with SAML authentication +- 3a58084b6: Created separate `AppContext` type to be returned from `useApp` rather than the `BackstageApp` itself. The `AppContext` type includes but deprecates `getPlugins`, `getProvider`, `getRouter`, and `getRoutes`. In addition, the `AppContext` adds a new `getComponents` method which providers access to the app components. +- Updated dependencies [a1f5e6545] + - @backstage/config@0.1.3 + +## 0.2.10 + +### Patch Changes + +- f10950bd2: Minor refactoring of BackstageApp.getSystemIcons to support custom registered + icons. Custom Icons can be added using: + + ```tsx + import AlarmIcon from '@material-ui/icons/Alarm'; + import MyPersonIcon from './MyPerson'; + + const app = createApp({ + icons: { + user: MyPersonIcon // override system icon + alert: AlarmIcon, // Custom icon + }, + }); + ``` + +- fd3f2a8c0: Export `createExternalRouteRef`, as well as give it an `id` for easier debugging, and fix parameter requirements when used with `useRouteRef`. + ## 0.2.9 ### Patch Changes diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 927eca3490..d3eb0d7791 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,7 +1,7 @@ { "name": "@backstage/core-api", "description": "Internal Core API used by Backstage plugins and apps", - "version": "0.2.9", + "version": "0.2.11", "private": false, "publishConfig": { "access": "public", @@ -29,7 +29,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.2", + "@backstage/config": "^0.1.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,8 +42,8 @@ "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.6", + "@backstage/cli": "^0.6.2", + "@backstage/test-utils": "^0.1.8", "@backstage/test-utils-core": "^0.1.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", diff --git a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts index e05f6266d6..af97ad8f36 100644 --- a/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/saml/SamlAuth.ts @@ -83,7 +83,7 @@ class SamlAuth implements ProfileInfoApi, BackstageIdentityApi, SessionApi { } async getBackstageIdentity( - options: AuthRequestOptions, + options: AuthRequestOptions = {}, ): Promise { const session = await this.sessionManager.getSession(options); return session?.backstageIdentity; diff --git a/packages/core-api/src/apis/system/ApiProvider.test.tsx b/packages/core-api/src/apis/system/ApiProvider.test.tsx index ac64aca3a7..71269697c2 100644 --- a/packages/core-api/src/apis/system/ApiProvider.test.tsx +++ b/packages/core-api/src/apis/system/ApiProvider.test.tsx @@ -108,11 +108,11 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).toThrow('No ApiProvider available in react context'); + }).toThrow(/^No ApiProvider available in react context. /); }).error, ).toEqual([ expect.stringMatching( - /^Error: Uncaught \[Error: No ApiProvider available in react context\]/, + /^Error: Uncaught \[Error: No ApiProvider available in react context. /, ), expect.stringMatching( /^The above error occurred in the component/, @@ -123,11 +123,11 @@ describe('ApiProvider', () => { withLogCollector(['error'], () => { expect(() => { render(); - }).toThrow('No ApiProvider available in react context'); + }).toThrow(/^No ApiProvider available in react context. /); }).error, ).toEqual([ expect.stringMatching( - /^Error: Uncaught \[Error: No ApiProvider available in react context\]/, + /^Error: Uncaught \[Error: No ApiProvider available in react context. /, ), expect.stringMatching( /^The above error occurred in the component/, diff --git a/packages/core-api/src/apis/system/ApiProvider.tsx b/packages/core-api/src/apis/system/ApiProvider.tsx index 91d35e5ee7..d41730cb59 100644 --- a/packages/core-api/src/apis/system/ApiProvider.tsx +++ b/packages/core-api/src/apis/system/ApiProvider.tsx @@ -24,6 +24,12 @@ import PropTypes from 'prop-types'; import { ApiRef, ApiHolder, TypesToApiRefs } from './types'; import { ApiAggregator } from './ApiAggregator'; +const missingHolderMessage = + 'No ApiProvider available in react context. ' + + 'A common cause of this error is that multiple versions of @backstage/core-api are installed. ' + + `You can check if that is the case using 'yarn backstage-cli versions:check', and can in many cases ` + + `fix the issue either with the --fix flag or using 'yarn backstage-cli versions:bump'`; + type ApiProviderProps = { apis: ApiHolder; children: ReactNode; @@ -50,7 +56,7 @@ export function useApiHolder(): ApiHolder { const apiHolder = useContext(Context); if (!apiHolder) { - throw new Error('No ApiProvider available in react context'); + throw new Error(missingHolderMessage); } return apiHolder; @@ -74,7 +80,7 @@ export function withApis(apis: TypesToApiRefs) { const apiHolder = useContext(Context); if (!apiHolder) { - throw new Error('No ApiProvider available in react context'); + throw new Error(missingHolderMessage); } const impls = {} as T; diff --git a/packages/core-api/src/app/App.tsx b/packages/core-api/src/app/App.tsx index f50bcdd015..1cf2d892d0 100644 --- a/packages/core-api/src/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -64,6 +64,7 @@ import { AppThemeProvider } from './AppThemeProvider'; import { AppComponents, AppConfigLoader, + AppContext, AppOptions, AppRouteBinder, BackstageApp, @@ -139,6 +140,42 @@ function useConfigLoader( return { api: configReader }; } +class AppContextImpl implements AppContext { + constructor(private readonly app: PrivateAppImpl) {} + + getPlugins(): BackstagePlugin[] { + // eslint-disable-next-line no-console + console.warn('appContext.getPlugins() is deprecated and will be removed'); + return this.app.getPlugins(); + } + + getSystemIcon(key: IconKey): IconComponent | undefined { + return this.app.getSystemIcon(key); + } + + getComponents(): AppComponents { + return this.app.getComponents(); + } + + getProvider(): React.ComponentType<{}> { + // eslint-disable-next-line no-console + console.warn('appContext.getProvider() is deprecated and will be removed'); + return this.app.getProvider(); + } + + getRouter(): React.ComponentType<{}> { + // eslint-disable-next-line no-console + console.warn('appContext.getRouter() is deprecated and will be removed'); + return this.app.getRouter(); + } + + getRoutes(): JSX.Element[] { + // eslint-disable-next-line no-console + console.warn('appContext.getRoutes() is deprecated and will be removed'); + return this.app.getRoutes(); + } +} + export class PrivateAppImpl implements BackstageApp { private apiHolder?: ApiHolder; private configApi?: ConfigApi; @@ -169,15 +206,17 @@ export class PrivateAppImpl implements BackstageApp { return this.plugins; } - getSystemIcon(key: IconKey): IconComponent { + getSystemIcon(key: IconKey): IconComponent | undefined { return this.icons[key]; } + getComponents(): AppComponents { + return this.components; + } + getRoutes(): JSX.Element[] { const routes = new Array(); - const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!; - const { NotFoundErrorPage } = this.components; for (const plugin of this.plugins.values()) { @@ -211,13 +250,6 @@ export class PrivateAppImpl implements BackstageApp { routes.push(); break; } - case 'feature-flag': { - featureFlagsApi.registerFlag({ - name: output.name, - pluginId: plugin.getId(), - }); - break; - } default: break; } @@ -236,6 +268,27 @@ export class PrivateAppImpl implements BackstageApp { } getProvider(): ComponentType<{}> { + const appContext = new AppContextImpl(this); + const apiHolder = this.getApiHolder(); + + const featureFlagsApi = this.getApiHolder().get(featureFlagsApiRef)!; + + for (const plugin of this.plugins.values()) { + for (const output of plugin.output()) { + switch (output.type) { + case 'feature-flag': { + featureFlagsApi.registerFlag({ + name: output.name, + pluginId: plugin.getId(), + }); + break; + } + default: + break; + } + } + } + const Provider = ({ children }: PropsWithChildren<{}>) => { const appThemeApi = useMemo( () => AppThemeSelector.createWithStorage(this.themes), @@ -272,8 +325,8 @@ export class PrivateAppImpl implements BackstageApp { this.configApi = loadedConfig.api; return ( - - + + (undefined); +const Context = createContext(undefined); type Props = { - app: BackstageApp; + appContext: AppContext; }; export const AppContextProvider = ({ - app, + appContext, children, }: PropsWithChildren) => ( - + ); -export const useApp = (): BackstageApp => { - const app = useContext(Context); - if (!app) { +export const useApp = (): AppContext => { + const appContext = useContext(Context); + if (!appContext) { throw new Error('No app context available'); } - return app; + return appContext; }; diff --git a/packages/core-api/src/app/types.ts b/packages/core-api/src/app/types.ts index 1970868ca6..ec7b70689d 100644 --- a/packages/core-api/src/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -171,7 +171,7 @@ export type BackstageApp = { /** * Get a common or custom icon for this app. */ - getSystemIcon(key: IconKey): IconComponent; + getSystemIcon(key: IconKey): IconComponent | undefined; /** * Provider component that should wrap the Router created with getRouter() @@ -187,6 +187,40 @@ export type BackstageApp = { /** * Routes component that contains all routes for plugin pages in the app. + * + * @deprecated Registering routes in plugins is deprecated and this method will be removed. + */ + getRoutes(): JSX.Element[]; +}; + +export type AppContext = { + /** + * @deprecated Will be removed + */ + getPlugins(): BackstagePlugin[]; + + /** + * Get a common or custom icon for this app. + */ + getSystemIcon(key: IconKey): IconComponent | undefined; + + /** + * Get the components registered for various purposes in the app. + */ + getComponents(): AppComponents; + + /** + * @deprecated Will be removed + */ + getProvider(): ComponentType<{}>; + + /** + * @deprecated Will be removed + */ + getRouter(): ComponentType<{}>; + + /** + * @deprecated Will be removed */ getRoutes(): JSX.Element[]; }; diff --git a/packages/core-api/src/extensions/componentData.test.tsx b/packages/core-api/src/extensions/componentData.test.tsx index 6c4abda40f..cb0918b290 100644 --- a/packages/core-api/src/extensions/componentData.test.tsx +++ b/packages/core-api/src/extensions/componentData.test.tsx @@ -59,4 +59,63 @@ describe('elementData', () => { 'Attempted to attach duplicate data "my-data" to component "MyComponent"', ); }); + + describe('works across versions', () => { + function getDataSymbol() { + const Component = () => null; + attachComponentData(Component, 'my-data', {}); + const [symbol] = Object.getOwnPropertySymbols(Component); + return symbol; + } + + it('should should be able to get data from older versions', () => { + const symbol = getDataSymbol(); + + const data = { foo: 'bar' }; + const Component = () => null; + attachComponentData(Component, 'my-data', data); + + const element = ; + expect((element as any).type[symbol].map.get('my-data')).toBe(data); + }); + + it('should should be able to attach data for older versions', () => { + const symbol = getDataSymbol(); + + const data = { foo: 'bar' }; + const Component = () => null; + (Component as any)[symbol] = { + map: new Map([['my-data', data]]), + }; + + const element = ; + expect(getComponentData(element, 'my-data')).toBe(data); + }); + + it('should be able to get data from newer versions', () => { + const data = { foo: 'bar' }; + const Component = () => null; + attachComponentData(Component, 'my-data', data); + + const element = ; + const container = (global as any)[ + '__@backstage/component-data-store__' + ].store.get(element.type); + expect(container.map.get('my-data')).toBe(data); + }); + + it('should should be able to attach data for newer versions', () => { + const data = { foo: 'bar' }; + const Component = () => null; + (global as any)['__@backstage/component-data-store__'].store.set( + Component, + { + map: new Map([['my-data', data]]), + }, + ); + + const element = ; + expect(getComponentData(element, 'my-data')).toBe(data); + }); + }); }); diff --git a/packages/core-api/src/extensions/componentData.tsx b/packages/core-api/src/extensions/componentData.tsx index 1f88356a48..3d2558276d 100644 --- a/packages/core-api/src/extensions/componentData.tsx +++ b/packages/core-api/src/extensions/componentData.tsx @@ -15,21 +15,40 @@ */ import { ComponentType, ReactNode } from 'react'; +import { globalObject } from '../lib/globalObject'; +// TODO(Rugvip): Access via symbol is deprecated, remove once on 0.3.x const DATA_KEY = Symbol('backstage-component-data'); -type DataContainer = { - map: Map; -}; - type ComponentWithData

= ComponentType

& { [DATA_KEY]?: DataContainer; }; -type ReactNodeWithData = ReactNode & { - type?: { [DATA_KEY]?: DataContainer }; +type DataContainer = { + map: Map; }; +type MaybeComponentNode = ReactNode & { + type?: ComponentType & { [DATA_KEY]?: DataContainer }; +}; + +const GLOBAL_KEY = '__@backstage/component-data-store__'; + +// The store is bridged across versions using the global object +function getStore() { + let storeObj = globalObject[GLOBAL_KEY] as + | { store: WeakMap, DataContainer> } + | undefined; + if (!storeObj) { + const store = new WeakMap, DataContainer>(); + storeObj = { store }; + globalObject[GLOBAL_KEY] = storeObj; + } + return storeObj.store; +} + +const store = getStore(); + export function attachComponentData

( component: ComponentType

, type: string, @@ -37,9 +56,11 @@ export function attachComponentData

( ) { const dataComponent = component as ComponentWithData

; - let container = dataComponent[DATA_KEY]; + let container = store.get(component) || dataComponent[DATA_KEY]; if (!container) { - container = dataComponent[DATA_KEY] = { map: new Map() }; + container = { map: new Map() }; + store.set(component, container); + dataComponent[DATA_KEY] = container; } if (container.map.has(type)) { @@ -60,7 +81,12 @@ export function getComponentData( return undefined; } - const container = (node as ReactNodeWithData).type?.[DATA_KEY]; + const component = (node as MaybeComponentNode).type; + if (!component) { + return undefined; + } + + const container = store.get(component) || component[DATA_KEY]; if (!container) { return undefined; } diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx index 0c2b580ea7..49db90efcc 100644 --- a/packages/core-api/src/icons/icons.tsx +++ b/packages/core-api/src/icons/icons.tsx @@ -15,31 +15,46 @@ */ import { SvgIconProps } from '@material-ui/core'; +import MuiBrokenImageIcon from '@material-ui/icons/BrokenImage'; +import MuiChatIcon from '@material-ui/icons/Chat'; import MuiDashboardIcon from '@material-ui/icons/Dashboard'; +import MuiEmailIcon from '@material-ui/icons/Email'; +import MuiGitHubIcon from '@material-ui/icons/GitHub'; import MuiHelpIcon from '@material-ui/icons/Help'; -import PeopleIcon from '@material-ui/icons/People'; -import PersonIcon from '@material-ui/icons/Person'; +import MuiPeopleIcon from '@material-ui/icons/People'; +import MuiPersonIcon from '@material-ui/icons/Person'; +import MuiWarningIcon from '@material-ui/icons/Warning'; import React from 'react'; import { useApp } from '../app/AppContext'; -import { IconComponent, SystemIconKey, IconComponentMap } from './types'; +import { IconComponent, IconComponentMap, SystemIconKey } from './types'; export const defaultSystemIcons: IconComponentMap = { - user: PersonIcon, - group: PeopleIcon, + brokenImage: MuiBrokenImageIcon, + chat: MuiChatIcon, dashboard: MuiDashboardIcon, + email: MuiEmailIcon, + github: MuiGitHubIcon, + group: MuiPeopleIcon, help: MuiHelpIcon, + user: MuiPersonIcon, + warning: MuiWarningIcon, }; const overridableSystemIcon = (key: SystemIconKey): IconComponent => { const Component = (props: SvgIconProps) => { const app = useApp(); const Icon = app.getSystemIcon(key); - return ; + return Icon ? : ; }; return Component; }; +export const BrokenImageIcon = overridableSystemIcon('brokenImage'); +export const ChatIcon = overridableSystemIcon('chat'); export const DashboardIcon = overridableSystemIcon('dashboard'); +export const EmailIcon = overridableSystemIcon('email'); +export const GitHubIcon = overridableSystemIcon('github'); export const GroupIcon = overridableSystemIcon('group'); export const HelpIcon = overridableSystemIcon('help'); export const UserIcon = overridableSystemIcon('user'); +export const WarningIcon = overridableSystemIcon('warning'); diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts index be24b223ae..c4634bd00a 100644 --- a/packages/core-api/src/icons/types.ts +++ b/packages/core-api/src/icons/types.ts @@ -17,7 +17,16 @@ import { ComponentType } from 'react'; import { SvgIconProps } from '@material-ui/core'; -export type SystemIconKey = 'user' | 'group' | 'dashboard' | 'help'; +export type SystemIconKey = + | 'brokenImage' + | 'chat' + | 'dashboard' + | 'email' + | 'github' + | 'group' + | 'help' + | 'user' + | 'warning'; export type IconComponent = ComponentType; export type IconKey = SystemIconKey | string; diff --git a/packages/core-api/src/lib/globalObject.ts b/packages/core-api/src/lib/globalObject.ts new file mode 100644 index 0000000000..85f16212fe --- /dev/null +++ b/packages/core-api/src/lib/globalObject.ts @@ -0,0 +1,29 @@ +/* + * 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. + */ + +// https://github.com/zloirock/core-js/issues/86#issuecomment-115759028 +function getGlobal() { + if (typeof window !== 'undefined' && window.Math === Math) { + return window; + } + if (typeof self !== 'undefined' && self.Math === Math) { + return self; + } + // eslint-disable-next-line no-new-func + return Function('return this')(); +} + +export const globalObject = getGlobal(); diff --git a/packages/core-api/src/routing/FlatRoutes.tsx b/packages/core-api/src/routing/FlatRoutes.tsx index cefb347290..3702442811 100644 --- a/packages/core-api/src/routing/FlatRoutes.tsx +++ b/packages/core-api/src/routing/FlatRoutes.tsx @@ -14,8 +14,9 @@ * limitations under the License. */ -import { ReactNode, Children, isValidElement, Fragment } from 'react'; +import React, { ReactNode, Children, isValidElement, Fragment } from 'react'; import { useRoutes } from 'react-router-dom'; +import { useApp } from '../app'; type RouteObject = { path: string; @@ -71,6 +72,15 @@ type FlatRoutesProps = { }; export const FlatRoutes = (props: FlatRoutesProps): JSX.Element | null => { + const app = useApp(); + const { NotFoundErrorPage } = app.getComponents(); const routes = createRoutesFromChildren(props.children); + + // TODO(Rugvip): Possibly add a way to skip this, like a noNotFoundPage prop + routes.push({ + element: , + path: '/*', + }); + return useRoutes(routes); }; diff --git a/packages/core-api/src/routing/index.ts b/packages/core-api/src/routing/index.ts index ad88f8ff02..100158c3c5 100644 --- a/packages/core-api/src/routing/index.ts +++ b/packages/core-api/src/routing/index.ts @@ -21,6 +21,10 @@ export type { MutableRouteRef, } from './types'; export { FlatRoutes } from './FlatRoutes'; -export { createRouteRef, createExternalRouteRef } from './RouteRef'; +export { + createRouteRef, + createExternalRouteRef, + ExternalRouteRef, +} from './RouteRef'; export type { RouteRefConfig } from './RouteRef'; export { useRouteRef } from './hooks'; diff --git a/packages/core/CHANGELOG.md b/packages/core/CHANGELOG.md index a5a56f633a..81fe728101 100644 --- a/packages/core/CHANGELOG.md +++ b/packages/core/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/core +## 0.6.3 + +### Patch Changes + +- 3a58084b6: The `FlatRoutes` components now renders the not found page of the app if no routes are matched. +- e799e74d4: Fix `OverflowTooltip` cutting off the bottom of letters like "g" and "y". +- 1407b34c6: More informative error message for missing ApiContext. +- 9615e68fb: Forward link styling of `EntityRefLink` and `EnriryRefLinks` into the underling + `Link`. +- 49f9b7346: Deprecate `type` of `ItemCard` and introduce new `subtitle` which allows passing + react nodes. +- 3a58084b6: Created separate `AppContext` type to be returned from `useApp` rather than the `BackstageApp` itself. The `AppContext` type includes but deprecates `getPlugins`, `getProvider`, `getRouter`, and `getRoutes`. In addition, the `AppContext` adds a new `getComponents` method which providers access to the app components. +- 2c1f2a7c2: Introduced generic OverflowTooltip component for cases where longer text needs to be truncated with ellipsis and show hover tooltip with full text. This is particularly useful in the cases where longer description text is rendered in table. e.g. CatalogTable and ApiExplorerTable. +- Updated dependencies [3a58084b6] +- Updated dependencies [1407b34c6] +- Updated dependencies [b6c4f485d] +- Updated dependencies [3a58084b6] +- Updated dependencies [a1f5e6545] + - @backstage/core-api@0.2.11 + - @backstage/config@0.1.3 + +## 0.6.2 + +### Patch Changes + +- fd3f2a8c0: Export `createExternalRouteRef`, as well as give it an `id` for easier debugging, and fix parameter requirements when used with `useRouteRef`. +- f4c2bcf54: Use a more strict type for `variant` of cards. +- 07e226872: Export Select component +- f62e7abe5: Make sure that SidebarItems are also active when on sub route. +- 96f378d10: Add support for custom empty state of `Table` components. + + You can now optionally pass `emptyContent` to `Table` that is displayed + if the table has now rows. + +- 688b73110: Add Breadcrumbs component +- Updated dependencies [f10950bd2] +- Updated dependencies [fd3f2a8c0] + - @backstage/core-api@0.2.10 + ## 0.6.1 ### Patch Changes diff --git a/packages/core/config.d.ts b/packages/core/config.d.ts index a6f95ae71c..9e43c6f3ad 100644 --- a/packages/core/config.d.ts +++ b/packages/core/config.d.ts @@ -30,6 +30,41 @@ export interface Config { * @visibility frontend */ title?: string; + + /** + * Information about support of this Backstage instance and how to contact the integrator team. + */ + support?: { + /** + * The primary support url. + * @visibility frontend + */ + url: string; + /** + * A list of categorized support item groupings. + */ + items: { + /** + * The title of the support item grouping. + * @visibility frontend + */ + title: string; + /** + * An optional icon for the support item grouping. + * @visibility frontend + */ + icon?: string; + /** + * A list of support links for the Backstage instance. + */ + links: { + /** @visibility frontend */ + url: string; + /** @visibility frontend */ + title?: string; + }[]; + }[]; + }; }; /** diff --git a/packages/core/package.json b/packages/core/package.json index c9045c6465..c2fae32d6c 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.6.1", + "version": "0.6.3", "private": false, "publishConfig": { "access": "public", @@ -29,28 +29,29 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.2", - "@backstage/core-api": "^0.2.8", + "@backstage/config": "^0.1.3", + "@backstage/core-api": "^0.2.11", "@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", "@testing-library/react-hooks": "^3.4.2", "@types/dagre": "^0.7.44", + "@types/prop-types": "^15.7.3", "@types/react": "^16.9", "@types/react-sparklines": "^1.7.0", - "@types/prop-types": "^15.7.3", + "@types/react-text-truncate": "^0.14.0", "classnames": "^2.2.6", "clsx": "^1.1.0", "d3-selection": "^2.0.0", "d3-shape": "^2.0.0", "d3-zoom": "^2.0.0", "dagre": "^0.8.5", - "qs": "^6.9.4", "immer": "^8.0.1", "lodash": "^4.17.15", "material-table": "^1.69.1", "prop-types": "^15.7.2", + "qs": "^6.9.4", "rc-progress": "^3.0.0", "react": "^16.12.0", "react-dom": "^16.12.0", @@ -61,13 +62,14 @@ "react-router-dom": "6.0.0-beta.0", "react-sparklines": "^1.7.0", "react-syntax-highlighter": "^13.5.1", + "react-text-truncate": "^0.16.0", "react-use": "^15.3.3", "remark-gfm": "^1.0.0", "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/packages/core/src/components/Link/Link.tsx b/packages/core/src/components/Link/Link.tsx index b551b0511d..cd74b681db 100644 --- a/packages/core/src/components/Link/Link.tsx +++ b/packages/core/src/components/Link/Link.tsx @@ -14,17 +14,17 @@ * limitations under the License. */ -import React, { ElementType } from 'react'; import { Link as MaterialLink, LinkProps as MaterialLinkProps, } from '@material-ui/core'; +import React, { ElementType } from 'react'; import { Link as RouterLink, LinkProps as RouterLinkProps, } from 'react-router-dom'; -type Props = MaterialLinkProps & +export type LinkProps = MaterialLinkProps & RouterLinkProps & { component?: ElementType; }; @@ -33,7 +33,7 @@ type Props = MaterialLinkProps & * Thin wrapper on top of material-ui's Link component * Makes the Link to utilise react-router */ -export const Link = React.forwardRef((props, ref) => { +export const Link = React.forwardRef((props, ref) => { const to = String(props.to); return /^https?:\/\//.test(to) ? ( // External links diff --git a/packages/core/src/components/Link/index.ts b/packages/core/src/components/Link/index.ts index 18e990181c..9be779feb7 100644 --- a/packages/core/src/components/Link/index.ts +++ b/packages/core/src/components/Link/index.ts @@ -15,3 +15,4 @@ */ export { Link } from './Link'; +export type { LinkProps } from './Link'; diff --git a/packages/core/src/components/OverflowTooltip/OverflowTooltip.stories.tsx b/packages/core/src/components/OverflowTooltip/OverflowTooltip.stories.tsx new file mode 100644 index 0000000000..e0cbe7c814 --- /dev/null +++ b/packages/core/src/components/OverflowTooltip/OverflowTooltip.stories.tsx @@ -0,0 +1,48 @@ +/* + * 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 { Box } from '@material-ui/core'; +import React from 'react'; +import { OverflowTooltip } from './OverflowTooltip'; + +export default { + title: 'Data Display/OverflowTooltip', + component: OverflowTooltip, +}; + +const text = + 'Lorem Ipsum is simply dummy text of the printing and typesetting industry.'; + +export const Default = () => ( + + + +); + +export const MultiLine = () => ( + + + +); + +export const DifferentTitle = () => ( + + + +); diff --git a/packages/core/src/components/OverflowTooltip/OverflowTooltip.tsx b/packages/core/src/components/OverflowTooltip/OverflowTooltip.tsx new file mode 100644 index 0000000000..faf064f9f4 --- /dev/null +++ b/packages/core/src/components/OverflowTooltip/OverflowTooltip.tsx @@ -0,0 +1,57 @@ +/* + * 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 { makeStyles, Tooltip, TooltipProps } from '@material-ui/core'; +import React, { useState } from 'react'; +import TextTruncate, { TextTruncateProps } from 'react-text-truncate'; + +type Props = { + text: TextTruncateProps['text']; + line?: TextTruncateProps['line']; + element?: TextTruncateProps['element']; + title?: TooltipProps['title']; + placement?: TooltipProps['placement']; +}; + +const useStyles = makeStyles({ + container: { + overflow: 'visible !important', + }, +}); + +export const OverflowTooltip = (props: Props) => { + const [hover, setHover] = useState(false); + const classes = useStyles(); + + const handleToggled = (truncated: boolean) => { + setHover(truncated); + }; + + return ( + + + + ); +}; diff --git a/plugins/scaffolder/src/components/JobStage/index.ts b/packages/core/src/components/OverflowTooltip/index.ts similarity index 91% rename from plugins/scaffolder/src/components/JobStage/index.ts rename to packages/core/src/components/OverflowTooltip/index.ts index d6d3534a88..fe51e8267f 100644 --- a/plugins/scaffolder/src/components/JobStage/index.ts +++ b/packages/core/src/components/OverflowTooltip/index.ts @@ -13,4 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { JobStage } from './JobStage'; +export { OverflowTooltip } from './OverflowTooltip'; diff --git a/packages/core/src/components/ProgressBars/GaugeCard.tsx b/packages/core/src/components/ProgressBars/GaugeCard.tsx index 7f281c67a5..2154619b01 100644 --- a/packages/core/src/components/ProgressBars/GaugeCard.tsx +++ b/packages/core/src/components/ProgressBars/GaugeCard.tsx @@ -16,14 +16,14 @@ import React from 'react'; import { makeStyles } from '@material-ui/core'; -import { InfoCard } from '../../layout/InfoCard'; +import { InfoCard, InfoCardVariants } from '../../layout/InfoCard'; import { BottomLinkProps } from '../../layout/BottomLink'; import { Gauge } from './Gauge'; type Props = { title: string; subheader?: string; - variant?: string; + variant?: InfoCardVariants; /** Progress in % specified as decimal, e.g. "0.23" */ progress: number; deepLink?: BottomLinkProps; diff --git a/packages/core/src/components/SupportButton/SupportButton.tsx b/packages/core/src/components/SupportButton/SupportButton.tsx index 2a1b810582..7c532c0f8f 100644 --- a/packages/core/src/components/SupportButton/SupportButton.tsx +++ b/packages/core/src/components/SupportButton/SupportButton.tsx @@ -14,35 +14,26 @@ * limitations under the License. */ -import React, { - Fragment, - useState, - MouseEventHandler, - PropsWithChildren, -} from 'react'; +import { HelpIcon, useApp } from '@backstage/core-api'; import { Button, - Link, List, ListItem, ListItemIcon, - Popover, - Typography, - makeStyles, ListItemText, + makeStyles, + Popover, } from '@material-ui/core'; -import GroupIcon from '@material-ui/icons/Group'; -import HelpIcon from '@material-ui/icons/Help'; +import React, { + Fragment, + MouseEventHandler, + PropsWithChildren, + useState, +} from 'react'; +import { SupportItem, SupportItemLink, useSupportConfig } from '../../hooks'; +import { Link } from '../Link'; -// import { EmailIcon, SlackIcon, SupportIcon } from 'shared/icons'; -// import { Button, Link } from 'shared/components'; -// import { StackOverflow, StackOverflowTag } from 'shared/components/layout'; - -type Props = { - slackChannel?: string | string[]; - email?: string | string[]; - plugin?: any; -}; +type Props = {}; const useStyles = makeStyles(theme => ({ leftIcon: { @@ -50,17 +41,45 @@ const useStyles = makeStyles(theme => ({ }, popoverList: { minWidth: 260, - maxWidth: 320, + maxWidth: 400, }, })); -export const SupportButton = ({ - slackChannel = '#backstage', - email = [], - children, -}: // plugin, -PropsWithChildren) => { - // TODO: get plugin manifest with hook +const SupportIcon = ({ icon }: { icon: string | undefined }) => { + const app = useApp(); + const Icon = icon ? app.getSystemIcon(icon) ?? HelpIcon : HelpIcon; + return ; +}; + +const SupportLink = ({ link }: { link: SupportItemLink }) => ( + + {link.title ?? link.url} + +); + +const SupportListItem = ({ item }: { item: SupportItem }) => { + return ( + + + + + + {item.links && + item.links.map(link => ( + + ))} + + } + /> + + ); +}; + +export const SupportButton = ({ children }: PropsWithChildren) => { + const { items } = useSupportConfig(); const [popoverOpen, setPopoverOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(null); @@ -75,12 +94,6 @@ PropsWithChildren) => { setPopoverOpen(false); }; - // const tags = plugin ? plugin.stackoverflowTags : undefined; - const slackChannels = Array.isArray(slackChannel) - ? slackChannel - : [slackChannel]; - const contactEmails = Array.isArray(email) ? email : [email]; - return ( diff --git a/plugins/catalog/src/components/CatalogPage/utils/locales/goodMorning.locales.json b/plugins/catalog/src/components/CatalogPage/utils/locales/goodMorning.locales.json index a67c292d83..febbc32c49 100644 --- a/plugins/catalog/src/components/CatalogPage/utils/locales/goodMorning.locales.json +++ b/plugins/catalog/src/components/CatalogPage/utils/locales/goodMorning.locales.json @@ -27,7 +27,7 @@ "Indonesian": "Selamat pagi", "Irish": "Dia dhuit", "Italian": "Buongiorno", - "Japanese": "お早う", + "Japanese": "おはよう", "Korean": "안녕하세요", "Kazakh": "Kayırlı tan", "Kurdish": "Beyanî baş", diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 16c91c0d6c..ffd5f3d190 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -14,7 +14,12 @@ * limitations under the License. */ -import { Entity } from '@backstage/catalog-model'; +import { + Entity, + VIEW_URL_ANNOTATION, + EDIT_URL_ANNOTATION, +} from '@backstage/catalog-model'; +import { act, fireEvent } from '@testing-library/react'; import { renderWithEffects, wrapInTestApp } from '@backstage/test-utils'; import * as React from 'react'; import { CatalogTable } from './CatalogTable'; @@ -38,6 +43,14 @@ const entities: Entity[] = [ ]; describe('CatalogTable component', () => { + beforeEach(() => { + window.open = jest.fn(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + it('should render error message when error is passed in props', async () => { const rendered = await renderWithEffects( wrapInTestApp( @@ -70,4 +83,62 @@ describe('CatalogTable component', () => { expect(rendered.getByText(/component2/)).toBeInTheDocument(); expect(rendered.getByText(/component3/)).toBeInTheDocument(); }); + + it('should use specified edit URL if in annotation', async () => { + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'component1', + annotations: { [EDIT_URL_ANNOTATION]: 'https://other.place' }, + }, + }; + + const { getByTitle } = await renderWithEffects( + wrapInTestApp( + , + ), + ); + + const editButton = getByTitle('Edit'); + + await act(async () => { + fireEvent.click(editButton); + }); + + expect(window.open).toHaveBeenCalledWith('https://other.place', '_blank'); + }); + + it('should use specified view URL if in annotation', async () => { + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'component1', + annotations: { [VIEW_URL_ANNOTATION]: 'https://other.place' }, + }, + }; + + const { getByTitle } = await renderWithEffects( + wrapInTestApp( + , + ), + ); + + const viewButton = getByTitle('View'); + + await act(async () => { + fireEvent.click(viewButton); + }); + + expect(window.open).toHaveBeenCalledWith('https://other.place', '_blank'); + }); }); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index cdc7451601..5a8040cd5b 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -25,20 +25,20 @@ import { TableColumn, TableProps, WarningPanel, + OverflowTooltip, } from '@backstage/core'; import { EntityRefLink, EntityRefLinks, formatEntityRefTitle, getEntityRelations, + useStarredEntities, } from '@backstage/plugin-catalog-react'; import { Chip } from '@material-ui/core'; import Edit from '@material-ui/icons/Edit'; import OpenInNew from '@material-ui/icons/OpenInNew'; import React from 'react'; -import { findLocationForEntityMeta } from '../../data/utils'; -import { useStarredEntities } from '../../hooks/useStarredEntities'; -import { createEditLink } from '../createEditLink'; +import { findViewUrl, findEditUrl } from '../actions'; import { favouriteEntityIcon, favouriteEntityTooltip, @@ -91,6 +91,13 @@ const columns: TableColumn[] = [ { title: 'Description', field: 'entity.metadata.description', + render: ({ entity }) => ( + + ), + width: 'auto', }, { title: 'Tags', @@ -145,24 +152,24 @@ export const CatalogTable = ({ const actions: TableProps['actions'] = [ ({ entity }) => { - const location = findLocationForEntityMeta(entity.metadata); + const url = findViewUrl(entity); return { icon: () => , tooltip: 'View', onClick: () => { - if (!location) return; - window.open(location.target, '_blank'); + if (!url) return; + window.open(url, '_blank'); }, }; }, ({ entity }) => { - const location = findLocationForEntityMeta(entity.metadata); + const url = findEditUrl(entity); return { icon: () => , tooltip: 'Edit', onClick: () => { - if (!location) return; - window.open(createEditLink(location), '_blank'); + if (!url) return; + window.open(url, '_blank'); }, }; }, diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index b0c3db2333..f7e558e285 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -33,9 +33,8 @@ export const EntityLinksCard = ({ cols = undefined }: Props) => { const { entity } = useEntity(); const app = useApp(); - const iconResolver = (key: IconKey | undefined): IconComponent => { - return app.getSystemIcon(key ?? '') ?? LanguageIcon; - }; + const iconResolver = (key: IconKey | undefined): IconComponent => + key ? app.getSystemIcon(key) ?? LanguageIcon : LanguageIcon; const links = entity?.metadata?.links; diff --git a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx index 970ce32ece..1c414414aa 100644 --- a/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx +++ b/plugins/catalog/src/components/FavouriteEntity/FavouriteEntity.tsx @@ -15,10 +15,10 @@ */ import React, { ComponentProps } from 'react'; +import { useStarredEntities } from '@backstage/plugin-catalog-react'; import { IconButton, Tooltip, withStyles } from '@material-ui/core'; import StarBorder from '@material-ui/icons/StarBorder'; import Star from '@material-ui/icons/Star'; -import { useStarredEntities } from '../../hooks/useStarredEntities'; import { Entity } from '@backstage/catalog-model'; type Props = ComponentProps & { entity: Entity }; diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 6485d4f525..eb8e96d9b3 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -33,6 +33,14 @@ type Props = { variant?: 'gridItem'; }; +const columns = [ + EntityTable.columns.createEntityRefColumn({ defaultKind: 'component' }), + EntityTable.columns.createOwnerColumn(), + EntityTable.columns.createSpecTypeColumn(), + EntityTable.columns.createSpecLifecycleColumn(), + EntityTable.columns.createMetadataDescriptionColumn(), +]; + export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => { const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { @@ -72,7 +80,7 @@ export const HasComponentsCard = ({ variant = 'gridItem' }: Props) => { } - columns={EntityTable.componentEntityColumns} + columns={columns} entities={entities as ComponentEntity[]} /> ); diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx index c2635b68e1..778ec1dec5 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -33,6 +33,14 @@ type Props = { variant?: 'gridItem'; }; +const columns = [ + EntityTable.columns.createEntityRefColumn({ defaultKind: 'component' }), + EntityTable.columns.createOwnerColumn(), + EntityTable.columns.createSpecTypeColumn(), + EntityTable.columns.createSpecLifecycleColumn(), + EntityTable.columns.createMetadataDescriptionColumn(), +]; + export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => { const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { @@ -72,7 +80,7 @@ export const HasSubcomponentsCard = ({ variant = 'gridItem' }: Props) => { } - columns={EntityTable.componentEntityColumns} + columns={columns} entities={entities as ComponentEntity[]} /> ); diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx index 9faa640060..5b18711d44 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx @@ -33,6 +33,12 @@ type Props = { variant?: 'gridItem'; }; +const columns = [ + EntityTable.columns.createEntityRefColumn({ defaultKind: 'system' }), + EntityTable.columns.createOwnerColumn(), + EntityTable.columns.createMetadataDescriptionColumn(), +]; + export const HasSystemsCard = ({ variant = 'gridItem' }: Props) => { const { entity } = useEntity(); const { entities, loading, error } = useRelatedEntities(entity, { @@ -71,7 +77,7 @@ export const HasSystemsCard = ({ variant = 'gridItem' }: Props) => { } - columns={EntityTable.systemEntityColumns} + columns={columns} entities={entities as SystemEntity[]} /> ); diff --git a/plugins/catalog/src/components/createEditLink.ts b/plugins/catalog/src/components/actions.ts similarity index 76% rename from plugins/catalog/src/components/createEditLink.ts rename to plugins/catalog/src/components/actions.ts index 57fc876704..72e1bf67ec 100644 --- a/plugins/catalog/src/components/createEditLink.ts +++ b/plugins/catalog/src/components/actions.ts @@ -14,7 +14,13 @@ * limitations under the License. */ -import { LocationSpec } from '@backstage/catalog-model'; +import { + LocationSpec, + Entity, + EDIT_URL_ANNOTATION, + VIEW_URL_ANNOTATION, +} from '@backstage/catalog-model'; +import { findLocationForEntityMeta } from '../data/utils'; import parseGitUrl from 'git-url-parse'; /** @@ -75,3 +81,22 @@ export const determineUrlType = (url: string): string => { } return 'url'; }; + +export const findEditUrl = ({ metadata }: Entity): string | undefined => { + const annotations = metadata.annotations || {}; + + const editUrl = annotations[EDIT_URL_ANNOTATION]; + + if (editUrl) return editUrl; + + const location = findLocationForEntityMeta(metadata); + + return location && createEditLink(location); +}; + +export const findViewUrl = ({ metadata }: Entity): string | undefined => { + const annotations = metadata.annotations || {}; + const location = findLocationForEntityMeta(metadata); + + return annotations[VIEW_URL_ANNOTATION] || location?.target; +}; diff --git a/plugins/catalog/src/data/utils.ts b/plugins/catalog/src/data/utils.ts index df14875092..ec63d94754 100644 --- a/plugins/catalog/src/data/utils.ts +++ b/plugins/catalog/src/data/utils.ts @@ -32,13 +32,17 @@ export function findLocationForEntityMeta( return undefined; } - const separatorIndex = annotation.indexOf(':'); + return parseLocation(annotation); +} + +export function parseLocation(reference: string): LocationSpec | undefined { + const separatorIndex = reference.indexOf(':'); if (separatorIndex === -1) { return undefined; } return { - type: annotation.substring(0, separatorIndex), - target: annotation.substring(separatorIndex + 1), + type: reference.substring(0, separatorIndex), + target: reference.substring(separatorIndex + 1), }; } diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index a0932a8dba..19dc778280 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -29,6 +29,7 @@ import { entityRouteRef, } from '@backstage/plugin-catalog-react'; import { CatalogClientWrapper } from './CatalogClientWrapper'; +import { createComponentRouteRef } from './routes'; export const catalogPlugin = createPlugin({ id: 'catalog', @@ -47,6 +48,9 @@ export const catalogPlugin = createPlugin({ catalogIndex: catalogRouteRef, catalogEntity: entityRouteRef, }, + externalRoutes: { + createComponent: createComponentRouteRef, + }, }); export const CatalogIndexPage = catalogPlugin.provide( diff --git a/plugins/catalog/src/routes.ts b/plugins/catalog/src/routes.ts new file mode 100644 index 0000000000..40e1784235 --- /dev/null +++ b/plugins/catalog/src/routes.ts @@ -0,0 +1,21 @@ +/* + * 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 { createExternalRouteRef } from '@backstage/core'; + +export const createComponentRouteRef = createExternalRouteRef({ + id: 'create-component', +}); diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md index 1c5d35aa70..b849abe379 100644 --- a/plugins/circleci/CHANGELOG.md +++ b/plugins/circleci/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-circleci +## 0.2.10 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.2.9 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.2.8 ### Patch Changes diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index 6f741ab1ce..3c46020d3a 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-circleci", - "version": "0.2.8", + "version": "0.2.10", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "postpack": "backstage-cli postpack" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -50,9 +50,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md index 8dccd1dbbd..f79281be18 100644 --- a/plugins/cloudbuild/CHANGELOG.md +++ b/plugins/cloudbuild/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-cloudbuild +## 0.2.11 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.2.10 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.2.9 ### Patch Changes diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json index a00fe13fe0..07f7c03a0c 100644 --- a/plugins/cloudbuild/package.json +++ b/plugins/cloudbuild/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cloudbuild", - "version": "0.2.9", + "version": "0.2.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.3", - "@backstage/core": "^0.6.1", + "@backstage/catalog-model": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.1.0", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md index e672b33976..622454d2a9 100644 --- a/plugins/cost-insights/CHANGELOG.md +++ b/plugins/cost-insights/CHANGELOG.md @@ -1,5 +1,21 @@ # @backstage/plugin-cost-insights +## 0.8.2 + +### Patch Changes + +- 38205492a: Default alert properties can be overridden using accessors +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [1407b34c6] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [3a58084b6] +- Updated dependencies [a1f5e6545] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/config@0.1.3 + ## 0.8.1 ### Patch Changes diff --git a/plugins/cost-insights/README.md b/plugins/cost-insights/README.md index 86adb8673f..c10d483f3f 100644 --- a/plugins/cost-insights/README.md +++ b/plugins/cost-insights/README.md @@ -56,6 +56,38 @@ export const apis = [ export { plugin as CostInsights } from '@backstage/plugin-cost-insights'; ``` +5. Add Cost Insights to your app Sidebar. + +To expose the plugin to your users, you can integrate the `cost-insights` route anyway that suits your application, but most commonly it is added to the Sidebar. + +```diff +// packages/app/src/sidebar.tsx ++ import MoneyIcon from '@material-ui/icons/MonetizationOn'; + + ... + + export const AppSidebar = () => ( + + + + + {/* Global nav, not org-specific */} + + + + + + ++ + {/* End global nav */} + + + + + + ); +``` + ## Configuration Cost Insights has only two required configuration fields: a map of cloud `products` for showing cost breakdowns and `engineerCost` - the average yearly cost of an engineer including benefits. Products must be defined as keys on the `products` field. diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json index 2f0511c217..e306a7fa6e 100644 --- a/plugins/cost-insights/package.json +++ b/plugins/cost-insights/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-cost-insights", - "version": "0.8.1", + "version": "0.8.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,8 +30,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.1", + "@backstage/config": "^0.1.3", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -55,9 +55,9 @@ "yup": "^0.29.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx new file mode 100644 index 0000000000..05a10bb2f0 --- /dev/null +++ b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.test.tsx @@ -0,0 +1,101 @@ +/* + * 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 pluralize from 'pluralize'; +import { renderInTestApp } from '@backstage/test-utils'; +import { ProjectGrowthAlert } from './ProjectGrowthAlert'; +import { ProjectGrowthData } from '../types'; +import { + MockCurrencyProvider, + MockConfigProvider, + MockBillingDateProvider, +} from '../utils/tests'; + +const mockData: ProjectGrowthData = { + project: 'test-project', + periodStart: '2021-01-01', + periodEnd: '2021-02-01', + aggregation: [0, 0], + change: { + ratio: 0, + amount: 0, + }, + products: [ + { + id: 'product-a', + aggregation: [0, 0], + }, + ], +}; + +// suppress recharts componentDidUpdate deprecation warnings +jest.spyOn(console, 'warn').mockImplementation(() => {}); + +async function renderInContext(children: JSX.Element) { + return renderInTestApp( + + + {children} + + , + ); +} + +class CustomProjectGrowthAlert extends ProjectGrowthAlert { + get url() { + return 'path/to/resource'; + } + get title() { + return `Investigate cost growth in ${pluralize( + 'project', + this.data.products.length, + true, + )}`; + } +} + +describe('ProjectGrowthAlert', () => { + describe('constructor', () => { + it('should create a project growth alert', async () => { + const alert = new ProjectGrowthAlert(mockData); + const { getByText, queryByText } = await renderInContext(alert.element); + + expect(alert.url).toBe('/cost-insights/investigating-growth'); + expect(alert.title).toBe( + 'Investigate cost growth in project test-project', + ); + expect(alert.subtitle).toBe( + 'Cost growth outpacing business growth is unsustainable long-term.', + ); + expect(getByText('1 product')).toBeInTheDocument(); + expect(queryByText('sorted by cost')).not.toBeInTheDocument(); + }); + + it('a subclass can inherit and override defaults using accessors', async () => { + const alert = new CustomProjectGrowthAlert(mockData); + const { getByText, queryByText } = await renderInContext(alert.element); + + expect(alert.url).toBe('path/to/resource'); + expect(alert.title).toBe('Investigate cost growth in 1 project'); + expect(alert.subtitle).toBe( + 'Cost growth outpacing business growth is unsustainable long-term.', + ); + expect(getByText('1 product')).toBeInTheDocument(); + expect(queryByText('sorted by cost')).not.toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx index 67eb1b5071..088e85939d 100644 --- a/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx +++ b/plugins/cost-insights/src/alerts/ProjectGrowthAlert.tsx @@ -27,18 +27,22 @@ import { Alert, ProjectGrowthData } from '../types'; export class ProjectGrowthAlert implements Alert { data: ProjectGrowthData; - url = '/cost-insights/investigating-growth'; - subtitle = - 'Cost growth outpacing business growth is unsustainable long-term.'; - constructor(data: ProjectGrowthData) { this.data = data; } + get url() { + return '/cost-insights/investigating-growth'; + } + get title() { return `Investigate cost growth in project ${this.data.project}`; } + get subtitle() { + return 'Cost growth outpacing business growth is unsustainable long-term.'; + } + get element() { return ; } diff --git a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx new file mode 100644 index 0000000000..95e213f06a --- /dev/null +++ b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 pluralize from 'pluralize'; +import { renderInTestApp } from '@backstage/test-utils'; +import { UnlabeledDataflowAlert } from './UnlabeledDataflowAlert'; +import { UnlabeledDataflowData } from '../types'; +import { + MockCurrencyProvider, + MockConfigProvider, + MockBillingDateProvider, +} from '../utils/tests'; + +const mockData: UnlabeledDataflowData = { + periodStart: '2021-02-01', + periodEnd: '2021-03-31', + unlabeledCost: 0, + labeledCost: 0, + projects: [ + { + id: 'project-a', + labeledCost: 0, + unlabeledCost: 0, + }, + ], +}; + +// suppress recharts componentDidUpdate deprecation warnings +jest.spyOn(console, 'warn').mockImplementation(() => {}); + +async function renderInContext(children: JSX.Element) { + return renderInTestApp( + + + {children} + + , + ); +} + +class CustomUnlabeledDataflowAlert extends UnlabeledDataflowAlert { + get url() { + return 'path/to/resource'; + } + get title() { + return `Add labels to ${pluralize( + 'workflow', + this.data.projects.length, + true, + )}`; + } +} + +describe('UnlabeledDataflowAlert', () => { + describe('constructor', () => { + it('should create an unlabeled dataflow alert', async () => { + const alert = new UnlabeledDataflowAlert(mockData); + const { getByText } = await renderInContext(alert.element); + + expect(alert.url).toBe('/cost-insights/labeling-jobs'); + expect(alert.title).toBe('Add labels to workflows'); + expect(alert.subtitle).toBe( + 'Labels show in billing data, enabling cost insights for each workflow.', + ); + expect( + getByText( + 'Showing costs from 1 project with unlabeled Dataflow jobs in the last 30 days.', + ), + ).toBeInTheDocument(); + }); + + it('a subclass can inherit and override defaults using accessors', async () => { + const alert = new CustomUnlabeledDataflowAlert(mockData); + const { getByText } = await renderInContext(alert.element); + + expect(alert.url).toBe('path/to/resource'); + expect(alert.title).toBe('Add labels to 1 workflow'); + expect(alert.subtitle).toBe( + 'Labels show in billing data, enabling cost insights for each workflow.', + ); + expect( + getByText( + 'Showing costs from 1 project with unlabeled Dataflow jobs in the last 30 days.', + ), + ).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx index e889e0e3d4..7d11f3cc81 100644 --- a/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx +++ b/plugins/cost-insights/src/alerts/UnlabeledDataflowAlert.tsx @@ -28,15 +28,22 @@ export class UnlabeledDataflowAlert implements Alert { data: UnlabeledDataflowData; status?: AlertStatus; - url = '/cost-insights/labeling-jobs'; - title = 'Add labels to workflows'; - subtitle = - 'Labels show in billing data, enabling cost insights for each workflow.'; - constructor(data: UnlabeledDataflowData) { this.data = data; } + get url() { + return '/cost-insights/labeling-jobs'; + } + + get title() { + return 'Add labels to workflows'; + } + + get subtitle() { + return 'Labels show in billing data, enabling cost insights for each workflow.'; + } + get element() { return ; } diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md index b0334effe1..be284dfa64 100644 --- a/plugins/explore/CHANGELOG.md +++ b/plugins/explore/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-explore +## 0.2.7 + +### Patch Changes + +- 347137ccf: Display the owner of a domain on the domain card. +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.2.6 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.2.5 ### Patch Changes diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 497e108b6f..5c370ecfac 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-explore", - "version": "0.2.5", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/plugin-explore-react": "^0.0.2", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", @@ -45,9 +45,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/explore/src/components/DomainCard/DomainCard.tsx b/plugins/explore/src/components/DomainCard/DomainCard.tsx index 2c0126b94a..e5efc543b2 100644 --- a/plugins/explore/src/components/DomainCard/DomainCard.tsx +++ b/plugins/explore/src/components/DomainCard/DomainCard.tsx @@ -13,11 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { DomainEntity } from '@backstage/catalog-model'; +import { DomainEntity, RELATION_OWNED_BY } from '@backstage/catalog-model'; import { ItemCard } from '@backstage/core'; import { + EntityRefLinks, entityRoute, entityRouteParams, + getEntityRelations, } from '@backstage/plugin-catalog-react'; import React from 'react'; import { generatePath } from 'react-router-dom'; @@ -26,16 +28,27 @@ type DomainCardProps = { entity: DomainEntity; }; -export const DomainCard = ({ entity }: DomainCardProps) => ( - -); +export const DomainCard = ({ entity }: DomainCardProps) => { + const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY); + + return ( + + } + label="Explore" + // TODO: Use useRouteRef here to generate the path + href={generatePath( + `/catalog/${entityRoute.path}`, + entityRouteParams(entity), + )} + /> + ); +}; diff --git a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx index a52b0d8f2c..f29ad2cd6a 100644 --- a/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx +++ b/plugins/explore/src/components/DomainExplorerContent/DomainExplorerContent.test.tsx @@ -17,9 +17,9 @@ 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 { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { DomainExplorerContent } from './DomainExplorerContent'; describe('', () => { @@ -33,11 +33,9 @@ describe('', () => { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - - - {children} - - + + {children} + ); beforeEach(() => { @@ -69,9 +67,11 @@ describe('', () => { ]; catalogApi.getEntities.mockResolvedValue({ items: entities }); - const { getByText } = render(, { - wrapper: Wrapper, - }); + const { getByText } = await renderInTestApp( + + + , + ); await waitFor(() => { expect(getByText('artists')).toBeInTheDocument(); @@ -82,9 +82,11 @@ describe('', () => { it('renders empty state', async () => { catalogApi.getEntities.mockResolvedValue({ items: [] }); - const { getByText } = render(, { - wrapper: Wrapper, - }); + const { getByText } = await renderInTestApp( + + + , + ); await waitFor(() => expect(getByText('No domains to display')).toBeInTheDocument(), @@ -95,9 +97,11 @@ describe('', () => { const catalogError = new Error('Network timeout'); catalogApi.getEntities.mockRejectedValueOnce(catalogError); - const { getByText } = render(, { - wrapper: Wrapper, - }); + const { getByText } = await renderInTestApp( + + + , + ); await waitFor(() => expect(getByText(/Could not load domains/)).toBeInTheDocument(), diff --git a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx index b0e1330ff3..125a72eb12 100644 --- a/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx +++ b/plugins/explore/src/components/ToolExplorerContent/ToolExplorerContent.test.tsx @@ -19,11 +19,11 @@ import { ExploreTool, exploreToolsConfigRef, } from '@backstage/plugin-explore-react'; +import { renderInTestApp } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; -import { render, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { ToolExplorerContent } from './ToolExplorerContent'; describe('', () => { @@ -33,13 +33,11 @@ describe('', () => { const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - - - {children} - - + + {children} + ); @@ -70,9 +68,11 @@ describe('', () => { ]; exploreToolsConfigApi.getTools.mockResolvedValue(tools); - const { getByText } = render(, { - wrapper: Wrapper, - }); + const { getByText } = await renderInTestApp( + + + , + ); await waitFor(() => { expect(getByText('Lighthouse')).toBeInTheDocument(); @@ -83,9 +83,11 @@ describe('', () => { it('renders empty state', async () => { exploreToolsConfigApi.getTools.mockResolvedValue([]); - const { getByText } = render(, { - wrapper: Wrapper, - }); + const { getByText } = await renderInTestApp( + + + , + ); await waitFor(() => expect(getByText('No tools to display')).toBeInTheDocument(), diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md index 1e9e08d7f3..23129f83f9 100644 --- a/plugins/fossa/CHANGELOG.md +++ b/plugins/fossa/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-fossa +## 0.2.3 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.2.2 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.2.1 ### Patch Changes diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json index 334469184f..803b7ddf30 100644 --- a/plugins/fossa/package.json +++ b/plugins/fossa/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-fossa", - "version": "0.2.1", + "version": "0.2.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -44,9 +44,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json index 36dc403d7b..b1f14753f2 100644 --- a/plugins/gcp-projects/package.json +++ b/plugins/gcp-projects/package.json @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.1", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md index 8aba49fedc..ac717c1059 100644 --- a/plugins/github-actions/CHANGELOG.md +++ b/plugins/github-actions/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-github-actions +## 0.3.4 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.3.3 + +### Patch Changes + +- f4c2bcf54: Use a more strict type for `variant` of cards. +- Updated dependencies [491f3a0ec] +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/integration@0.5.0 + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.3.2 ### Patch Changes diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json index e5498563a5..eb203aaac8 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.2", + "version": "0.3.4", "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.1", - "@backstage/plugin-catalog-react": "^0.0.3", - "@backstage/core": "^0.6.1", - "@backstage/integration": "^0.4.0", + "@backstage/catalog-model": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.1.0", + "@backstage/core": "^0.6.3", + "@backstage/integration": "^0.5.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -50,9 +50,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/github-actions/src/components/Cards/Cards.tsx b/plugins/github-actions/src/components/Cards/Cards.tsx index 202bf5aa92..4ce667eb93 100644 --- a/plugins/github-actions/src/components/Cards/Cards.tsx +++ b/plugins/github-actions/src/components/Cards/Cards.tsx @@ -13,29 +13,30 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -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 { - Link, - Theme, - makeStyles, - LinearProgress, - Typography, -} from '@material-ui/core'; -import { - InfoCard, - StructuredMetadataTable, configApiRef, errorApiRef, + InfoCard, + InfoCardVariants, + StructuredMetadataTable, useApi, } from '@backstage/core'; +import { readGitHubIntegrationConfigs } from '@backstage/integration'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { + LinearProgress, + Link, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; +import React, { useEffect } from 'react'; import { GITHUB_ACTIONS_ANNOTATION } from '../useProjectName'; +import { useWorkflowRuns } from '../useWorkflowRuns'; +import { WorkflowRun, WorkflowRunsTable } from '../WorkflowRunsTable'; +import { WorkflowRunStatus } from '../WorkflowRunStatus'; const useStyles = makeStyles({ externalLinkIcon: { @@ -125,7 +126,7 @@ type Props = { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; branch: string; - variant?: string; + variant?: InfoCardVariants; }; export const LatestWorkflowsForBranchCard = ({ diff --git a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx index 4425bc8364..30807482a4 100644 --- a/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx +++ b/plugins/github-actions/src/components/Cards/RecentWorkflowRunsCard.tsx @@ -19,6 +19,7 @@ import { EmptyState, errorApiRef, InfoCard, + InfoCardVariants, Table, useApi, } from '@backstage/core'; @@ -39,7 +40,7 @@ export type Props = { branch?: string; dense?: boolean; limit?: number; - variant?: string; + variant?: InfoCardVariants; }; export const RecentWorkflowRunsCard = ({ diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index 251185b799..1902bebc70 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.1", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -42,9 +42,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx index f01ac70e64..046fa58600 100644 --- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx +++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx @@ -14,23 +14,23 @@ * limitations under the License. */ -import React from 'react'; -import { render } from '@testing-library/react'; -import ProfileCatalog from './ProfileCatalog'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; import { ApiProvider, ApiRegistry, - githubAuthApiRef, GithubAuth, + githubAuthApiRef, OAuthRequestManager, UrlPatternDiscovery, } from '@backstage/core'; +import { renderInTestApp } from '@backstage/test-utils'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import React from 'react'; import { gitOpsApiRef, GitOpsRestApi } from '../../api'; +import ProfileCatalog from './ProfileCatalog'; describe('ProfileCatalog', () => { - it('should render', () => { + it('should render', async () => { const oauthRequestApi = new OAuthRequestManager(); const apis = ApiRegistry.from([ [gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')], @@ -44,15 +44,15 @@ describe('ProfileCatalog', () => { }), ], ]); - const rendered = render( + + const { getByText } = await renderInTestApp( , ); - expect( - rendered.getByText('Create GitOps-managed Cluster'), - ).toBeInTheDocument(); + + expect(getByText('Create GitOps-managed Cluster')).toBeInTheDocument(); }); }); diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 307cef0dee..90936e7317 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -31,21 +31,21 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.1", + "@backstage/core": "^0.6.3", "@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", "graphiql": "^1.0.0-alpha.10", - "graphql": "15.4.0", + "graphql": "15.5.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md index 26c838d97a..8ba44c30f5 100644 --- a/plugins/jenkins/CHANGELOG.md +++ b/plugins/jenkins/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-jenkins +## 0.3.11 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.3.10 + +### Patch Changes + +- f4c2bcf54: Use a more strict type for `variant` of cards. +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.3.9 ### Patch Changes diff --git a/plugins/jenkins/README.md b/plugins/jenkins/README.md index 154fcf913d..13ff9b3547 100644 --- a/plugins/jenkins/README.md +++ b/plugins/jenkins/README.md @@ -50,7 +50,7 @@ metadata: name: 'your-component' description: 'a description' annotations: - jenkins.io/github-folder: 'folder-name/job-name' + jenkins.io/github-folder: 'folder-name/project-name' spec: type: service lifecycle: experimental @@ -84,5 +84,5 @@ YWRtaW46MTFlYzI1NmU0Mzg1MDFjM2Y1Yzc2Yjc1MWE3ZTQ3YWY4Mw== is the base64 of user a ## Limitations -- Only works with organization folder jobs backed by GitHub -- No pagination support currently, limited to 50 jobs - don't run this on a Jenkins with lots of builds +- Only works with organization folder projects backed by GitHub +- No pagination support currently, limited to 50 projects - don't run this on a Jenkins with lots of builds diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json index fb3e719858..817e6ae1a3 100644 --- a/plugins/jenkins/package.json +++ b/plugins/jenkins/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-jenkins", - "version": "0.3.9", + "version": "0.3.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/jenkins/src/assets/JenkinsLogo.svg b/plugins/jenkins/src/assets/JenkinsLogo.svg new file mode 100644 index 0000000000..4a4538daf6 --- /dev/null +++ b/plugins/jenkins/src/assets/JenkinsLogo.svg @@ -0,0 +1,283 @@ + + + +image/svg+xml \ No newline at end of file diff --git a/plugins/jenkins/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/jenkins/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx index d8f728bb27..5d9d81c985 100644 --- a/plugins/jenkins/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/jenkins/src/components/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -15,10 +15,10 @@ */ import React from 'react'; import { useParams } from 'react-router-dom'; -import { Content, Link } from '@backstage/core'; +import { Breadcrumbs, Content, Link } from '@backstage/core'; import { + Box, Typography, - Breadcrumbs, Paper, TableContainer, Table, @@ -36,7 +36,6 @@ import ExternalLinkIcon from '@material-ui/icons/Launch'; const useStyles = makeStyles(theme => ({ root: { maxWidth: 720, - margin: theme.spacing(2), }, table: { padding: theme.spacing(1), @@ -57,9 +56,10 @@ const BuildWithStepsView = () => { return (

- Jobs + Projects Run + diff --git a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx index 2d8f43ca80..3044094ccc 100644 --- a/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx +++ b/plugins/jenkins/src/components/BuildsPage/lib/CITable/CITable.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { Box, IconButton, Link, Typography, Tooltip } from '@material-ui/core'; import RetryIcon from '@material-ui/icons/Replay'; -import GitHubIcon from '@material-ui/icons/GitHub'; +import JenkinsLogo from '../../../../assets/JenkinsLogo.svg'; import { generatePath, Link as RouterLink } from 'react-router-dom'; import { Table, TableColumn } from '@backstage/core'; import { JenkinsRunStatus } from '../Status'; @@ -231,9 +231,9 @@ export const CITableView = ({ onChangeRowsPerPage={onChangePageSize} title={ - - - {projectName} + Jenkins logo + + Project: {projectName} } columns={generatedColumns} diff --git a/plugins/jenkins/src/components/Cards/Cards.tsx b/plugins/jenkins/src/components/Cards/Cards.tsx index 6fc544bcea..0d7c2fe919 100644 --- a/plugins/jenkins/src/components/Cards/Cards.tsx +++ b/plugins/jenkins/src/components/Cards/Cards.tsx @@ -13,13 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { DateTime, Duration } from 'luxon'; -import { Link, Theme, makeStyles, LinearProgress } from '@material-ui/core'; -import { InfoCard, StructuredMetadataTable } from '@backstage/core'; +import { + InfoCard, + InfoCardVariants, + StructuredMetadataTable, +} from '@backstage/core'; +import { LinearProgress, Link, makeStyles, Theme } from '@material-ui/core'; import ExternalLinkIcon from '@material-ui/icons/Launch'; -import { useBuilds } from '../useBuilds'; +import { DateTime, Duration } from 'luxon'; +import React from 'react'; import { JenkinsRunStatus } from '../BuildsPage/lib/Status'; +import { useBuilds } from '../useBuilds'; import { useProjectSlugFromEntity } from '../useProjectSlugFromEntity'; const useStyles = makeStyles({ @@ -76,7 +80,7 @@ export const LatestRunCard = ({ variant, }: { branch: string; - variant?: string; + variant?: InfoCardVariants; }) => { const { owner, repo } = useProjectSlugFromEntity(); const [{ builds, loading }] = useBuilds(owner, repo, branch); diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json index 1eaa1d8316..1280e30633 100644 --- a/plugins/kafka-backend/package.json +++ b/plugins/kafka-backend/package.json @@ -50,6 +50,6 @@ }, "files": [ "dist", - "schema.d.ts" + "config.d.ts" ] } diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md index 3d78708345..a8153002c9 100644 --- a/plugins/kafka/CHANGELOG.md +++ b/plugins/kafka/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-kafka +## 0.2.4 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.2.3 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.2.2 ### Patch Changes diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json index 46097f1c29..5c8dc9e934 100644 --- a/plugins/kafka/package.json +++ b/plugins/kafka/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kafka", - "version": "0.2.2", + "version": "0.2.4", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -33,9 +33,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.4.2", diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md index 08a3a66657..e02c3e14a0 100644 --- a/plugins/kubernetes-backend/CHANGELOG.md +++ b/plugins/kubernetes-backend/CHANGELOG.md @@ -1,5 +1,15 @@ # @backstage/plugin-kubernetes-backend +## 0.2.7 + +### Patch Changes + +- a70af22a2: update kubernetes plugin backend function to use classes +- Updated dependencies [bad21a085] +- Updated dependencies [a1f5e6545] + - @backstage/catalog-model@0.7.2 + - @backstage/config@0.1.3 + ## 0.2.6 ### Patch Changes diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json index 3823ee1899..9d5a0c25e0 100644 --- a/plugins/kubernetes-backend/package.json +++ b/plugins/kubernetes-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes-backend", - "version": "0.2.6", + "version": "0.2.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -33,8 +33,8 @@ "dependencies": { "@aws-sdk/credential-provider-node": "^3.3.0", "@backstage/backend-common": "^0.5.2", - "@backstage/catalog-model": "^0.7.1", - "@backstage/config": "^0.1.2", + "@backstage/catalog-model": "^0.7.2", + "@backstage/config": "^0.1.3", "@kubernetes/client-node": "^0.13.2", "@types/express": "^4.17.6", "aws4": "^1.11.0", @@ -51,7 +51,7 @@ "yn": "^4.0.0" }, "devDependencies": { - "@backstage/cli": "^0.6.0", + "@backstage/cli": "^0.6.2", "@types/aws4": "^1.5.1", "supertest": "^4.0.2" }, diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts similarity index 73% rename from plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts rename to plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts index 5c6513e57c..95ec4f0aba 100644 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsByServiceIdHandler.test.ts +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.test.ts @@ -14,11 +14,9 @@ * limitations under the License. */ -import { handleGetKubernetesObjectsForService } from './getKubernetesObjectsForServiceHandler'; import { getVoidLogger } from '@backstage/backend-common'; import { ObjectFetchParams } from '..'; - -const TEST_SERVICE_ID = 'my-service'; +import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; const fetchObjectsForService = jest.fn(); @@ -81,34 +79,34 @@ describe('handleGetKubernetesObjectsForService', () => { mockFetch(fetchObjectsForService); - const result = await handleGetKubernetesObjectsForService( - TEST_SERVICE_ID, + const sut = new KubernetesFanOutHandler( + getVoidLogger(), { - fetchObjectsForService: fetchObjectsForService, + fetchObjectsForService, }, { getClustersByServiceId, }, - getVoidLogger(), - { - entity: { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { - name: 'test-component', - annotations: { - 'backstage.io/kubernetes-labels-selector': - 'backstage.io/test-label=test-component', - }, - }, - spec: { - type: 'service', - lifecycle: 'production', - owner: 'joe', + ); + + const result = await sut.getKubernetesObjectsByEntity({ + entity: { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'backstage.io/kubernetes-labels-selector': + 'backstage.io/test-label=test-component', }, }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'joe', + }, }, - ); + }); expect(getClustersByServiceId.mock.calls.length).toBe(1); expect(fetchObjectsForService.mock.calls.length).toBe(1); @@ -124,7 +122,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-pods-my-service-test-cluster', + name: 'my-pods-test-component-test-cluster', }, }, ], @@ -134,7 +132,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-configmaps-my-service-test-cluster', + name: 'my-configmaps-test-component-test-cluster', }, }, ], @@ -144,7 +142,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-services-my-service-test-cluster', + name: 'my-services-test-component-test-cluster', }, }, ], @@ -172,37 +170,37 @@ describe('handleGetKubernetesObjectsForService', () => { mockFetch(fetchObjectsForService); - const result = await handleGetKubernetesObjectsForService( - TEST_SERVICE_ID, + const sut = new KubernetesFanOutHandler( + getVoidLogger(), { - fetchObjectsForService: fetchObjectsForService, + fetchObjectsForService, }, { getClustersByServiceId, }, - getVoidLogger(), - { - auth: { - google: 'google_token_123', + ); + + const result = await sut.getKubernetesObjectsByEntity({ + auth: { + google: 'google_token_123', + }, + entity: { + apiVersion: 'backstage.io/v1beta1', + kind: 'Component', + metadata: { + name: 'test-component', + annotations: { + 'backstage.io/kubernetes-labels-selector': + 'backstage.io/test-label=test-component', + }, }, - entity: { - apiVersion: 'backstage.io/v1beta1', - kind: 'Component', - metadata: { - name: 'test-component', - annotations: { - 'backstage.io/kubernetes-labels-selector': - 'backstage.io/test-label=test-component', - }, - }, - spec: { - type: 'service', - lifecycle: 'production', - owner: 'joe', - }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'joe', }, }, - ); + }); expect(getClustersByServiceId.mock.calls.length).toBe(1); expect(fetchObjectsForService.mock.calls.length).toBe(2); @@ -218,7 +216,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-pods-my-service-test-cluster', + name: 'my-pods-test-component-test-cluster', }, }, ], @@ -228,7 +226,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-configmaps-my-service-test-cluster', + name: 'my-configmaps-test-component-test-cluster', }, }, ], @@ -238,7 +236,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-services-my-service-test-cluster', + name: 'my-services-test-component-test-cluster', }, }, ], @@ -256,7 +254,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-pods-my-service-other-cluster', + name: 'my-pods-test-component-other-cluster', }, }, ], @@ -266,7 +264,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-configmaps-my-service-other-cluster', + name: 'my-configmaps-test-component-other-cluster', }, }, ], @@ -276,7 +274,7 @@ describe('handleGetKubernetesObjectsForService', () => { resources: [ { metadata: { - name: 'my-services-my-service-other-cluster', + name: 'my-services-test-component-other-cluster', }, }, ], diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts new file mode 100644 index 0000000000..0a2fc1488a --- /dev/null +++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts @@ -0,0 +1,109 @@ +/* + * 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 { + ClusterDetails, + KubernetesFetcher, + KubernetesObjectTypes, + KubernetesRequestBody, + KubernetesServiceLocator, +} from '../types/types'; +import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; +import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; + +const DEFAULT_OBJECTS = new Set([ + 'pods', + 'services', + 'configmaps', + 'deployments', + 'replicasets', + 'horizontalpodautoscalers', + 'ingresses', +]); + +export class KubernetesFanOutHandler { + private readonly logger: Logger; + private readonly fetcher: KubernetesFetcher; + private readonly serviceLocator: KubernetesServiceLocator; + + constructor( + logger: Logger, + fetcher: KubernetesFetcher, + serviceLocator: KubernetesServiceLocator, + ) { + this.logger = logger; + this.fetcher = fetcher; + this.serviceLocator = serviceLocator; + } + + async getKubernetesObjectsByEntity( + requestBody: KubernetesRequestBody, + objectTypesToFetch: Set = DEFAULT_OBJECTS, + ) { + const entityName = requestBody.entity.metadata.name; + + const clusterDetails: ClusterDetails[] = await this.serviceLocator.getClustersByServiceId( + entityName, + ); + + // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them + const promises: Promise[] = clusterDetails.map(cd => { + const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance( + cd.authProvider, + ); + return kubernetesAuthTranslator.decorateClusterDetailsWithAuth( + cd, + requestBody, + ); + }); + const clusterDetailsDecoratedForAuth: ClusterDetails[] = await Promise.all( + promises, + ); + + this.logger.info( + `entity.metadata.name=${entityName} clusterDetails=[${clusterDetailsDecoratedForAuth + .map(c => c.name) + .join(', ')}]`, + ); + + const labelSelector: string = + requestBody.entity?.metadata?.annotations?.[ + 'backstage.io/kubernetes-label-selector' + ] || `backstage.io/kubernetes-id=${entityName}`; + + return Promise.all( + clusterDetailsDecoratedForAuth.map(clusterDetailsItem => { + return this.fetcher + .fetchObjectsForService({ + serviceId: entityName, + clusterDetails: clusterDetailsItem, + objectTypesToFetch, + labelSelector, + }) + .then(result => { + return { + cluster: { + name: clusterDetailsItem.name, + }, + resources: result.responses, + errors: result.errors, + }; + }); + }), + ).then(r => ({ items: r })); + } +} diff --git a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts b/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts deleted file mode 100644 index 811a3c8acb..0000000000 --- a/plugins/kubernetes-backend/src/service/getKubernetesObjectsForServiceHandler.ts +++ /dev/null @@ -1,107 +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 { Logger } from 'winston'; -import { - KubernetesRequestBody, - ClusterDetails, - KubernetesServiceLocator, - KubernetesFetcher, - KubernetesObjectTypes, - ObjectsByEntityResponse, - ObjectFetchParams, -} from '../types/types'; -import { KubernetesAuthTranslator } from '../kubernetes-auth-translator/types'; -import { KubernetesAuthTranslatorGenerator } from '../kubernetes-auth-translator/KubernetesAuthTranslatorGenerator'; - -export type GetKubernetesObjectsForServiceHandler = ( - serviceId: string, - fetcher: KubernetesFetcher, - serviceLocator: KubernetesServiceLocator, - logger: Logger, - requestBody: KubernetesRequestBody, - objectTypesToFetch?: Set, -) => Promise; - -const DEFAULT_OBJECTS = new Set([ - 'pods', - 'services', - 'configmaps', - 'deployments', - 'replicasets', - 'horizontalpodautoscalers', - 'ingresses', -]); - -// Fans out the request to all clusters that the service lives in, aggregates their responses together -export const handleGetKubernetesObjectsForService: GetKubernetesObjectsForServiceHandler = async ( - serviceId, - fetcher, - serviceLocator, - logger, - requestBody, - objectTypesToFetch = DEFAULT_OBJECTS, -) => { - const clusterDetails: ClusterDetails[] = await serviceLocator.getClustersByServiceId( - serviceId, - ); - - // Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them - const promises: Promise[] = clusterDetails.map(cd => { - const kubernetesAuthTranslator: KubernetesAuthTranslator = KubernetesAuthTranslatorGenerator.getKubernetesAuthTranslatorInstance( - cd.authProvider, - ); - return kubernetesAuthTranslator.decorateClusterDetailsWithAuth( - cd, - requestBody, - ); - }); - const clusterDetailsDecoratedForAuth: ClusterDetails[] = await Promise.all( - promises, - ); - - logger.info( - `serviceId=${serviceId} clusterDetails=[${clusterDetailsDecoratedForAuth - .map(c => c.name) - .join(', ')}]`, - ); - - const labelSelector: string = - requestBody.entity?.metadata?.annotations?.[ - 'backstage.io/kubernetes-label-selector' - ] || `backstage.io/kubernetes-id=${requestBody.entity.metadata.name}`; - - return Promise.all( - clusterDetailsDecoratedForAuth.map(clusterDetailsItem => { - return fetcher - .fetchObjectsForService({ - serviceId, - clusterDetails: clusterDetailsItem, - objectTypesToFetch, - labelSelector, - } as ObjectFetchParams) - .then(result => { - return { - cluster: { - name: clusterDetailsItem.name, - }, - resources: result.responses, - errors: result.errors, - }; - }); - }), - ).then(r => ({ items: r })); -}; diff --git a/plugins/kubernetes-backend/src/service/router.test.ts b/plugins/kubernetes-backend/src/service/router.test.ts index a407ce9693..c91cae27df 100644 --- a/plugins/kubernetes-backend/src/service/router.test.ts +++ b/plugins/kubernetes-backend/src/service/router.test.ts @@ -18,35 +18,18 @@ import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; import { makeRouter } from './router'; -import { - KubernetesServiceLocator, - KubernetesFetcher, - ObjectsByEntityResponse, -} from '..'; +import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; describe('router', () => { let app: express.Express; - let kubernetesFetcher: jest.Mocked; - let kubernetesServiceLocator: jest.Mocked; - let handleGetByServiceId: jest.Mock>; + let kubernetesFanOutHandler: jest.Mocked; beforeAll(async () => { - kubernetesFetcher = { - fetchObjectsForService: jest.fn(), - }; + kubernetesFanOutHandler = { + getKubernetesObjectsByEntity: jest.fn(), + } as any; - kubernetesServiceLocator = { - getClustersByServiceId: jest.fn(), - }; - - handleGetByServiceId = jest.fn(); - - const router = makeRouter( - getVoidLogger(), - kubernetesFetcher, - kubernetesServiceLocator, - handleGetByServiceId as any, - ); + const router = makeRouter(getVoidLogger(), kubernetesFanOutHandler); app = express().use(router); }); @@ -67,7 +50,9 @@ describe('router', () => { ], }, } as any; - handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result)); + kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockReturnValueOnce( + Promise.resolve(result), + ); const response = await request(app).post('/services/test-service'); @@ -87,7 +72,9 @@ describe('router', () => { ], }, } as any; - handleGetByServiceId.mockReturnValueOnce(Promise.resolve(result)); + kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockReturnValueOnce( + Promise.resolve(result), + ); const response = await request(app) .post('/services/test-service') @@ -103,7 +90,9 @@ describe('router', () => { }); it('internal error: lists kubernetes objects', async () => { - handleGetByServiceId.mockRejectedValue(Error('some internal error')); + kubernetesFanOutHandler.getKubernetesObjectsByEntity.mockRejectedValue( + Error('some internal error'), + ); const response = await request(app).post('/services/test-service'); diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts index 38a02ad970..32b6dfb832 100644 --- a/plugins/kubernetes-backend/src/service/router.ts +++ b/plugins/kubernetes-backend/src/service/router.ts @@ -21,19 +21,15 @@ import { Config } from '@backstage/config'; import { MultiTenantServiceLocator } from '../service-locator/MultiTenantServiceLocator'; import { KubernetesClientBasedFetcher } from './KubernetesFetcher'; import { KubernetesClientProvider } from './KubernetesClientProvider'; -import { - GetKubernetesObjectsForServiceHandler, - handleGetKubernetesObjectsForService, -} from './getKubernetesObjectsForServiceHandler'; import { KubernetesRequestBody, KubernetesServiceLocator, - KubernetesFetcher, ServiceLocatorMethod, ClusterLocatorMethod, ClusterDetails, } from '..'; import { getCombinedClusterDetails } from '../cluster-locator'; +import { KubernetesFanOutHandler } from './KubernetesFanOutHandler'; export interface RouterOptions { logger: Logger; @@ -62,9 +58,7 @@ const getServiceLocator = ( export const makeRouter = ( logger: Logger, - fetcher: KubernetesFetcher, - serviceLocator: KubernetesServiceLocator, - handleGetByEntity: GetKubernetesObjectsForServiceHandler, + kubernetesFanOutHandler: KubernetesFanOutHandler, ): express.Router => { const router = Router(); router.use(express.json()); @@ -73,11 +67,7 @@ export const makeRouter = ( const serviceId = req.params.serviceId; const requestBody: KubernetesRequestBody = req.body; try { - const response = await handleGetByEntity( - serviceId, - fetcher, - serviceLocator, - logger, + const response = await kubernetesFanOutHandler.getKubernetesObjectsByEntity( requestBody, ); res.send(response); @@ -115,10 +105,11 @@ export async function createRouter( const serviceLocator = getServiceLocator(options.config, clusterDetails); - return makeRouter( + const kubernetesFanOutHandler = new KubernetesFanOutHandler( logger, fetcher, serviceLocator, - handleGetKubernetesObjectsForService, ); + + return makeRouter(logger, kubernetesFanOutHandler); } diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md index 91f6f8c672..75df5943dd 100644 --- a/plugins/kubernetes/CHANGELOG.md +++ b/plugins/kubernetes/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-kubernetes +## 0.3.11 + +### Patch Changes + +- Updated dependencies [a70af22a2] +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [a1f5e6545] +- Updated dependencies [2c1f2a7c2] + - @backstage/plugin-kubernetes-backend@0.2.7 + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + - @backstage/config@0.1.3 + +## 0.3.10 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.3.9 ### Patch Changes diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json index 3d7e30373b..d38b56653f 100644 --- a/plugins/kubernetes/package.json +++ b/plugins/kubernetes/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-kubernetes", - "version": "0.3.9", + "version": "0.3.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,11 +31,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.3", - "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.1", - "@backstage/plugin-kubernetes-backend": "^0.2.6", + "@backstage/catalog-model": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.1.0", + "@backstage/config": "^0.1.3", + "@backstage/core": "^0.6.3", + "@backstage/plugin-kubernetes-backend": "^0.2.7", "@backstage/theme": "^0.2.3", "@kubernetes/client-node": "^0.13.2", "@material-ui/core": "^4.11.0", @@ -48,9 +48,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/kubernetes/src/Router.tsx b/plugins/kubernetes/src/Router.tsx index 36851b3c46..838bfef60a 100644 --- a/plugins/kubernetes/src/Router.tsx +++ b/plugins/kubernetes/src/Router.tsx @@ -21,8 +21,11 @@ import { Route, Routes } from 'react-router-dom'; import { rootCatalogKubernetesRouteRef } from './plugin'; import { KubernetesContent } from './components/KubernetesContent'; import { MissingAnnotationEmptyState } from '@backstage/core'; +import { Button } from '@material-ui/core'; const KUBERNETES_ANNOTATION = 'backstage.io/kubernetes-id'; +const KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION = + 'backstage.io/kubernetes-label-selector'; type Props = { /** @deprecated The entity is now grabbed from context instead */ @@ -35,16 +38,37 @@ export const Router = (_props: Props) => { const kubernetesAnnotationValue = entity.metadata.annotations?.[KUBERNETES_ANNOTATION]; - if (!kubernetesAnnotationValue) { - return ; + const kubernetesLabelSelectorQueryAnnotationValue = + entity.metadata.annotations?.[KUBERNETES_LABEL_SELECTOR_QUERY_ANNOTATION]; + + if ( + kubernetesAnnotationValue || + kubernetesLabelSelectorQueryAnnotationValue + ) { + return ( + + } + /> + + ); } return ( - - } - /> - + <> + +

+ Or use a label selector query, which takes precedence over the previous + annotation. +

+ + ); }; diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/AwsKubernetesAuthProvider.ts b/plugins/kubernetes/src/kubernetes-auth-provider/AwsKubernetesAuthProvider.ts new file mode 100644 index 0000000000..cab81f3622 --- /dev/null +++ b/plugins/kubernetes/src/kubernetes-auth-provider/AwsKubernetesAuthProvider.ts @@ -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 { KubernetesAuthProvider } from './types'; +import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend'; + +export class AwsKubernetesAuthProvider implements KubernetesAuthProvider { + async decorateRequestBodyForAuth( + requestBody: KubernetesRequestBody, + ): Promise { + // No-op, with aws auth, server's AWS credentials are used for access + return requestBody; + } +} diff --git a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts index 9f64af9c2e..dc9d9b5e0d 100644 --- a/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts +++ b/plugins/kubernetes/src/kubernetes-auth-provider/KubernetesAuthProviders.ts @@ -19,6 +19,7 @@ import { KubernetesRequestBody } from '@backstage/plugin-kubernetes-backend'; import { KubernetesAuthProvider, KubernetesAuthProvidersApi } from './types'; import { GoogleKubernetesAuthProvider } from './GoogleKubernetesAuthProvider'; import { ServiceAccountKubernetesAuthProvider } from './ServiceAccountKubernetesAuthProvider'; +import { AwsKubernetesAuthProvider } from './AwsKubernetesAuthProvider'; export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { private readonly kubernetesAuthProviderMap: Map< @@ -36,6 +37,7 @@ export class KubernetesAuthProviders implements KubernetesAuthProvidersApi { 'serviceAccount', new ServiceAccountKubernetesAuthProvider(), ); + this.kubernetesAuthProviderMap.set('aws', new AwsKubernetesAuthProvider()); } async decorateRequestBodyForAuth( diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md index fab61283d9..20b1a5c1e1 100644 --- a/plugins/lighthouse/CHANGELOG.md +++ b/plugins/lighthouse/CHANGELOG.md @@ -1,5 +1,43 @@ # @backstage/plugin-lighthouse +## 0.2.12 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [a1f5e6545] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + - @backstage/config@0.1.3 + +## 0.2.11 + +### Patch Changes + +- f4c2bcf54: Use a more strict type for `variant` of cards. +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.2.10 ### Patch Changes diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 064e260389..a8f231cc1b 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-lighthouse", - "version": "0.2.10", + "version": "0.2.12", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,10 +31,10 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/config": "^0.1.2", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/config": "^0.1.3", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -46,9 +46,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx index d30b9a3312..693a8b6ec6 100644 --- a/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx +++ b/plugins/lighthouse/src/components/Cards/LastLighthouseAuditCard.tsx @@ -13,16 +13,17 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; -import { Audit, AuditCompleted, LighthouseCategoryId } from '../../api'; import { InfoCard, + InfoCardVariants, Progress, StatusError, StatusOK, StatusWarning, StructuredMetadataTable, } from '@backstage/core'; +import React from 'react'; +import { Audit, AuditCompleted, LighthouseCategoryId } from '../../api'; import { useWebsiteForEntity } from '../../hooks/useWebsiteForEntity'; import AuditStatusIcon from '../AuditStatusIcon'; @@ -96,7 +97,7 @@ export const LastLighthouseAuditCard = ({ variant, }: { dense?: boolean; - variant?: string; + variant?: InfoCardVariants; }) => { const { value: website, loading, error } = useWebsiteForEntity(); diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json index 6ab18a9cf5..d5aeaa0e5e 100644 --- a/plugins/newrelic/package.json +++ b/plugins/newrelic/package.json @@ -31,7 +31,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.1", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md index 898705cdc4..474d44a735 100644 --- a/plugins/org/CHANGELOG.md +++ b/plugins/org/CHANGELOG.md @@ -1,5 +1,49 @@ # @backstage/plugin-org +## 0.3.8 + +### Patch Changes + +- e3bc5aad7: Use the `pageTheme` to colour the OwnershipCard boxes with their respective theme colours. +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [b6c4f485d] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core-api@0.2.11 + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.3.7 + +### Patch Changes + +- f4c2bcf54: Use a more strict type for `variant` of cards. +- e8692df4a: - Fixes padding in `MembersListCard` + - Fixes email icon size in `GroupProfileCard` + - Uniform sizing across `GroupProfileCard` and `UserProfileCard` +- Updated dependencies [f10950bd2] +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core-api@0.2.10 + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.3.6 ### Patch Changes diff --git a/plugins/org/package.json b/plugins/org/package.json index 4a278a22f5..a9b5661c2e 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-org", - "version": "0.3.6", + "version": "0.3.8", "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.1", - "@backstage/core": "^0.6.1", - "@backstage/core-api": "^0.2.9", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/core-api": "^0.2.11", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -35,9 +35,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 1682192880..ce13f8391f 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -20,10 +20,10 @@ import { RELATION_CHILD_OF, RELATION_PARENT_OF, } from '@backstage/catalog-model'; -import { Avatar, InfoCard } from '@backstage/core'; +import { Avatar, InfoCard, InfoCardVariants } from '@backstage/core'; import { - getEntityRelations, entityRouteParams, + getEntityRelations, useEntity, } from '@backstage/plugin-catalog-react'; import { @@ -81,7 +81,7 @@ export const GroupProfileCard = ({ }: { /** @deprecated The entity is now grabbed from context instead */ entity?: GroupEntity; - variant: string; + variant?: InfoCardVariants; }) => { const group = useEntity().entity as GroupEntity; const { diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx index af42ef6759..bce1240434 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.stories.tsx @@ -13,10 +13,16 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Grid } from '@material-ui/core'; +import { Grid, ThemeProvider } from '@material-ui/core'; import React from 'react'; import { MemoryRouter } from 'react-router'; import { GroupEntity } from '@backstage/catalog-model'; +import { + createTheme, + genPageTheme, + shapes, + BackstageTheme, +} from '@backstage/theme'; import { EntityContext, CatalogApi, @@ -87,10 +93,44 @@ export const Default = () => ( - + ); + +const monochromeTheme = (outer: BackstageTheme) => + createTheme({ + ...outer, + defaultPageTheme: 'home', + pageTheme: { + home: genPageTheme(['#444'], shapes.wave2), + documentation: genPageTheme(['#474747'], shapes.wave2), + tool: genPageTheme(['#222'], shapes.wave2), + service: genPageTheme(['#aaa'], shapes.wave2), + website: genPageTheme(['#0e0e0e'], shapes.wave2), + library: genPageTheme(['#9d9d9d'], shapes.wave2), + other: genPageTheme(['#aaa'], shapes.wave2), + app: genPageTheme(['#666'], shapes.wave2), + }, + }); + +export const Themed = () => ( + + + + + + + + + + + + + +); diff --git a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx index 80e80bddcc..0c46b8e670 100644 --- a/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx +++ b/plugins/org/src/components/Cards/OwnershipCard/OwnershipCard.tsx @@ -15,19 +15,18 @@ */ import { Entity } from '@backstage/catalog-model'; -import { InfoCard, Progress, useApi } from '@backstage/core'; +import { InfoCard, InfoCardVariants, Progress, useApi } from '@backstage/core'; import { catalogApiRef, isOwnerOf, useEntity, } from '@backstage/plugin-catalog-react'; -import { pageTheme } from '@backstage/theme'; +import { BackstageTheme, genPageTheme } from '@backstage/theme'; import { Box, createStyles, Grid, makeStyles, - Theme, Typography, } from '@material-ui/core'; import Alert from '@material-ui/lab/Alert'; @@ -43,7 +42,17 @@ type EntitiesTypes = | 'api' | 'tool'; -const useStyles = makeStyles((theme: Theme) => +const createPageTheme = ( + theme: BackstageTheme, + shapeKey: string, + colorsKey: string, +) => { + const { colors } = theme.getPageTheme({ themeId: colorsKey }); + const { shape } = theme.getPageTheme({ themeId: shapeKey }); + return genPageTheme(colors, shape).backgroundImage; +}; + +const useStyles = makeStyles((theme: BackstageTheme) => createStyles({ card: { border: `1px solid ${theme.palette.divider}`, @@ -60,22 +69,22 @@ const useStyles = makeStyles((theme: Theme) => fontWeight: theme.typography.fontWeightBold, }, service: { - background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.service.colors})`, + background: createPageTheme(theme, 'home', 'service'), }, website: { - background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.website.colors})`, + background: createPageTheme(theme, 'home', 'website'), }, library: { - background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.library.colors})`, + background: createPageTheme(theme, 'home', 'library'), }, documentation: { - background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.documentation.colors})`, + background: createPageTheme(theme, 'home', 'documentation'), }, api: { - background: `${pageTheme.home.shape}, linear-gradient(90deg, #005B4B, #005B4B)`, + background: createPageTheme(theme, 'home', 'home'), }, tool: { - background: `${pageTheme.home.shape}, linear-gradient(90deg, ${pageTheme.tool.colors})`, + background: createPageTheme(theme, 'home', 'tool'), }, }), ); @@ -121,7 +130,7 @@ export const OwnershipCard = ({ }: { /** @deprecated The entity is now grabbed from context instead */ entity?: Entity; - variant: string; + variant?: InfoCardVariants; }) => { const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index 4dfa543d95..cf558fd55d 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -18,7 +18,7 @@ import { RELATION_MEMBER_OF, UserEntity, } from '@backstage/catalog-model'; -import { Avatar, InfoCard } from '@backstage/core'; +import { Avatar, InfoCard, InfoCardVariants } from '@backstage/core'; import { entityRouteParams, useEntity } from '@backstage/plugin-catalog-react'; import { Box, @@ -73,7 +73,7 @@ export const UserProfileCard = ({ }: { /** @deprecated The entity is now grabbed from context instead */ entity?: UserEntity; - variant: string; + variant?: InfoCardVariants; }) => { const user = useEntity().entity as UserEntity; const { diff --git a/plugins/pagerduty/CHANGELOG.md b/plugins/pagerduty/CHANGELOG.md index 3fe8e6db70..85547d08c7 100644 --- a/plugins/pagerduty/CHANGELOG.md +++ b/plugins/pagerduty/CHANGELOG.md @@ -1,5 +1,44 @@ # @backstage/plugin-pagerduty +## 0.3.1 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.3.0 + +### Minor Changes + +- 549a859ac: Improved the UI of the pagerduty plugin, and added a standalone TriggerButton + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.2.8 ### Patch Changes diff --git a/plugins/pagerduty/package.json b/plugins/pagerduty/package.json index c430075b95..3e10be41ea 100644 --- a/plugins/pagerduty/package.json +++ b/plugins/pagerduty/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-pagerduty", - "version": "0.2.8", + "version": "0.3.1", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,13 +30,14 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.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", + "@types/react": "^16.9", "classnames": "^2.2.6", "luxon": "1.25.0", "react": "^16.13.1", @@ -45,9 +46,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx index bf0d5d2587..3d2e4d0edd 100644 --- a/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx +++ b/plugins/pagerduty/src/components/Incident/IncidentListItem.tsx @@ -17,7 +17,6 @@ import React from 'react'; import { ListItem, - ListItemIcon, ListItemSecondaryAction, Tooltip, ListItemText, @@ -25,13 +24,16 @@ import { IconButton, Link, Typography, + Chip, } from '@material-ui/core'; -import { StatusError, StatusWarning } from '@backstage/core'; +import Done from '@material-ui/icons/Done'; +import Warning from '@material-ui/icons/Warning'; import { DateTime, Duration } from 'luxon'; import { Incident } from '../types'; import OpenInBrowserIcon from '@material-ui/icons/OpenInBrowser'; +import { BackstageTheme } from '@backstage/theme'; -const useStyles = makeStyles({ +const useStyles = makeStyles(theme => ({ denseListIcon: { marginRight: 0, display: 'flex', @@ -42,10 +44,21 @@ const useStyles = makeStyles({ listItemPrimary: { fontWeight: 'bold', }, - listItemIcon: { - minWidth: '1em', + warning: { + borderColor: theme.palette.status.warning, + color: theme.palette.status.warning, + '& *': { + color: theme.palette.status.warning, + }, }, -}); + error: { + borderColor: theme.palette.status.error, + color: theme.palette.status.error, + '& *': { + color: theme.palette.status.error, + }, + }, +})); type Props = { incident: Incident; @@ -62,19 +75,24 @@ export const IncidentListItem = ({ incident }: Props) => { return ( - - -
- {incident.status === 'triggered' ? ( - - ) : ( - - )} -
-
-
+ : } + className={ + incident.status === 'triggered' + ? classes.error + : classes.warning + } + /> + {incident.title} + + } primaryTypographyProps={{ variant: 'body1', className: classes.listItemPrimary, diff --git a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx index 9da9f9f11b..88500fbe9b 100644 --- a/plugins/pagerduty/src/components/Incident/Incidents.test.tsx +++ b/plugins/pagerduty/src/components/Incident/Incidents.test.tsx @@ -84,13 +84,7 @@ describe('Incidents', () => { }, ] as Incident[], ); - const { - getByText, - getByTitle, - getAllByTitle, - getByLabelText, - queryByTestId, - } = render( + const { getByText, getAllByTitle, queryByTestId } = render( wrapInTestApp( @@ -102,10 +96,10 @@ describe('Incidents', () => { expect(getByText('title2')).toBeInTheDocument(); expect(getByText('person1')).toBeInTheDocument(); expect(getByText('person2')).toBeInTheDocument(); - expect(getByTitle('triggered')).toBeInTheDocument(); - expect(getByTitle('acknowledged')).toBeInTheDocument(); - expect(getByLabelText('Status error')).toBeInTheDocument(); - expect(getByLabelText('Status warning')).toBeInTheDocument(); + expect(getByText('triggered')).toBeInTheDocument(); + expect(getByText('acknowledged')).toBeInTheDocument(); + expect(queryByTestId('chip-triggered')).toBeInTheDocument(); + expect(queryByTestId('chip-acknowledged')).toBeInTheDocument(); // assert links, mailto and hrefs, date calculation expect(getAllByTitle('View in PagerDuty').length).toEqual(2); diff --git a/plugins/pagerduty/src/components/PagerDutyCard.tsx b/plugins/pagerduty/src/components/PagerDutyCard.tsx index e03fb0fdbe..7ec425a062 100644 --- a/plugins/pagerduty/src/components/PagerDutyCard.tsx +++ b/plugins/pagerduty/src/components/PagerDutyCard.tsx @@ -17,68 +17,38 @@ 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, - Card, - CardHeader, - Divider, - CardContent, -} from '@material-ui/core'; +import { Card, CardHeader, Divider, CardContent } from '@material-ui/core'; import { Incidents } from './Incident'; import { EscalationPolicy } from './Escalation'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { pagerDutyApiRef, UnauthorizedError } from '../api'; import AlarmAddIcon from '@material-ui/icons/AlarmAdd'; -import { TriggerDialog } from './TriggerDialog'; import { MissingTokenError } from './Errors/MissingTokenError'; import WebIcon from '@material-ui/icons/Web'; - -const useStyles = makeStyles({ - triggerAlarm: { - paddingTop: 0, - paddingBottom: 0, - fontSize: '0.7rem', - textTransform: 'uppercase', - fontWeight: 600, - letterSpacing: 1.2, - lineHeight: 1.5, - '&:hover, &:focus, &.focus': { - backgroundColor: 'transparent', - textDecoration: 'none', - }, - }, -}); - -export const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; +import { PAGERDUTY_INTEGRATION_KEY } from './constants'; +import { TriggerButton, useShowDialog } from './TriggerButton'; export const isPluginApplicableToEntity = (entity: Entity) => Boolean(entity.metadata.annotations?.[PAGERDUTY_INTEGRATION_KEY]); -type Props = { - /** @deprecated The entity is now grabbed from context instead */ - entity?: Entity; -}; - -export const PagerDutyCard = (_props: Props) => { - const classes = useStyles(); +export const PagerDutyCard = () => { const { entity } = useEntity(); const api = useApi(pagerDutyApiRef); - const [showDialog, setShowDialog] = useState(false); const [refreshIncidents, setRefreshIncidents] = useState(false); const integrationKey = entity.metadata.annotations![ PAGERDUTY_INTEGRATION_KEY ]; + const setShowDialog = useShowDialog()[1]; + + const showDialog = useCallback(() => { + setShowDialog(true); + }, [setShowDialog]); const handleRefresh = useCallback(() => { setRefreshIncidents(x => !x); }, []); - const handleDialog = useCallback(() => { - setShowDialog(x => !x); - }, []); - const { value: service, loading, error } = useAsync(async () => { const services = await api.getServiceByIntegrationKey(integrationKey); @@ -114,17 +84,8 @@ export const PagerDutyCard = (_props: Props) => { const triggerLink = { label: 'Create Incident', - action: ( - - ), - icon: , + action: , + icon: , }; return ( @@ -140,13 +101,6 @@ export const PagerDutyCard = (_props: Props) => { refreshIncidents={refreshIncidents} /> - ); diff --git a/plugins/pagerduty/src/components/TriggerButton/index.tsx b/plugins/pagerduty/src/components/TriggerButton/index.tsx new file mode 100644 index 0000000000..833e01d8aa --- /dev/null +++ b/plugins/pagerduty/src/components/TriggerButton/index.tsx @@ -0,0 +1,94 @@ +/* + * 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 React, { useCallback, PropsWithChildren } from 'react'; +import { createGlobalState } from 'react-use'; +import { makeStyles, Button } from '@material-ui/core'; +import { useEntity } from '@backstage/plugin-catalog-react'; +import { BackstageTheme } from '@backstage/theme'; + +import { TriggerDialog } from '../TriggerDialog'; +import { PAGERDUTY_INTEGRATION_KEY } from '../constants'; + +export interface TriggerButtonProps { + design: 'link' | 'button'; + onIncidentCreated?: () => void; +} + +const useStyles = makeStyles(theme => ({ + buttonStyle: { + backgroundColor: theme.palette.error.main, + color: theme.palette.error.contrastText, + '&:hover': { + backgroundColor: theme.palette.error.dark, + }, + }, + triggerAlarm: { + paddingTop: 0, + paddingBottom: 0, + fontSize: '0.7rem', + textTransform: 'uppercase', + fontWeight: 600, + letterSpacing: 1.2, + lineHeight: 1.5, + '&:hover, &:focus, &.focus': { + backgroundColor: 'transparent', + textDecoration: 'none', + }, + }, +})); + +export const useShowDialog = createGlobalState(false); + +export function TriggerButton({ + design, + onIncidentCreated, + children, +}: PropsWithChildren) { + const { buttonStyle, triggerAlarm } = useStyles(); + const { entity } = useEntity(); + const [dialogShown = false, setDialogShown] = useShowDialog(); + + const showDialog = useCallback(() => { + setDialogShown(true); + }, [setDialogShown]); + const hideDialog = useCallback(() => { + setDialogShown(false); + }, [setDialogShown]); + + const integrationKey = entity.metadata.annotations![ + PAGERDUTY_INTEGRATION_KEY + ]; + + return ( + <> + + + + ); +} diff --git a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx index c390ed1f82..c2084a41ed 100644 --- a/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx +++ b/plugins/pagerduty/src/components/TriggerDialog/TriggerDialog.tsx @@ -35,7 +35,7 @@ type Props = { integrationKey: string; showDialog: boolean; handleDialog: () => void; - onIncidentCreated: () => void; + onIncidentCreated?: () => void; }; export const TriggerDialog = ({ @@ -69,11 +69,17 @@ export const TriggerDialog = ({ useEffect(() => { if (value) { - alertApi.post({ - message: `Alarm successfully triggered by ${userName}`, - }); - onIncidentCreated(); - handleDialog(); + (async () => { + alertApi.post({ + message: `Alarm successfully triggered by ${userName}`, + }); + + handleDialog(); + + // The pager duty API isn't always returning the newly created alarm immediately + await new Promise(resolve => setTimeout(resolve, 1000)); + onIncidentCreated?.(); + })(); } }, [value, alertApi, handleDialog, userName, onIncidentCreated]); diff --git a/plugins/pagerduty/src/components/constants.ts b/plugins/pagerduty/src/components/constants.ts new file mode 100644 index 0000000000..a7c32a362e --- /dev/null +++ b/plugins/pagerduty/src/components/constants.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 const PAGERDUTY_INTEGRATION_KEY = 'pagerduty.com/integration-key'; diff --git a/plugins/pagerduty/src/index.ts b/plugins/pagerduty/src/index.ts index 2dfedab164..92c3c8e73c 100644 --- a/plugins/pagerduty/src/index.ts +++ b/plugins/pagerduty/src/index.ts @@ -23,6 +23,7 @@ export { isPluginApplicableToEntity as isPagerDutyAvailable, PagerDutyCard, } from './components/PagerDutyCard'; +export { TriggerButton } from './components/TriggerButton'; export { PagerDutyClient, pagerDutyApiRef, diff --git a/plugins/register-component/CHANGELOG.md b/plugins/register-component/CHANGELOG.md index 30844b6fc6..c37820a2c7 100644 --- a/plugins/register-component/CHANGELOG.md +++ b/plugins/register-component/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-register-component +## 0.2.11 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.2.10 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.2.9 ### Patch Changes diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index ee95eae540..92ea4f0f6d 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.9", + "version": "0.2.11", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -45,9 +45,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx index e9da8ef84d..9dfed8f404 100644 --- a/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx +++ b/plugins/register-component/src/components/RegisterComponentPage/RegisterComponentPage.test.tsx @@ -21,11 +21,10 @@ import { errorApiRef, } from '@backstage/core'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; import { ThemeProvider } from '@material-ui/core'; -import { render, screen } from '@testing-library/react'; import React from 'react'; -import { MemoryRouter } from 'react-router-dom'; import { RegisterComponentPage } from './RegisterComponentPage'; const errorApi: jest.Mocked = { @@ -44,30 +43,29 @@ const catalogApi: jest.Mocked = { }; const Wrapper = ({ children }: { children?: React.ReactNode }) => ( - - - {children} - - + + {children} + ); describe('RegisterComponentPage', () => { - it('should render', () => { - render( - , - { wrapper: Wrapper }, + it('should render', async () => { + const { getByText } = await renderInTestApp( + + + , ); - expect(screen.getByText('Register existing component')).toBeInTheDocument(); + expect(getByText('Register existing component')).toBeInTheDocument(); }); }); diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md index 61ae7e2706..32995efab8 100644 --- a/plugins/rollbar/CHANGELOG.md +++ b/plugins/rollbar/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-rollbar +## 0.3.2 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.3.0 ### Minor Changes diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json index 39ecae186c..1ce8007683 100644 --- a/plugins/rollbar/package.json +++ b/plugins/rollbar/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-rollbar", - "version": "0.3.0", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md index 5a3553dc79..e7e3b0d718 100644 --- a/plugins/scaffolder-backend/CHANGELOG.md +++ b/plugins/scaffolder-backend/CHANGELOG.md @@ -1,5 +1,83 @@ # @backstage/plugin-scaffolder-backend +## 0.8.0 + +### Minor Changes + +- a5f42cf66: # Stateless scaffolding + + The scaffolder has been redesigned to be horizontally scalable and to persistently store task state and execution logs in the database. + + Each scaffolder task is given a unique task ID which is persisted in the database. + Tasks are then picked up by a `TaskWorker` which performs the scaffolding steps. + Execution logs are also persisted in the database meaning you can now refresh the scaffolder task status page without losing information. + + The task status page is now dynamically created based on the step information stored in the database. + This allows for custom steps to be displayed once the next version of the scaffolder template schema is available. + + The task page is updated to display links to both the git repository and to the newly created catalog entity. + + Component registration has moved from the frontend into a separate registration step executed by the `TaskWorker`. This requires that a `CatalogClient` is passed to the scaffolder backend instead of the old `CatalogEntityClient`. + + Make sure to update `plugins/scaffolder.ts` + + ```diff + import { + CookieCutter, + createRouter, + Preparers, + Publishers, + CreateReactAppTemplater, + Templaters, + - CatalogEntityClient, + } from '@backstage/plugin-scaffolder-backend'; + + +import { CatalogClient } from '@backstage/catalog-client'; + + const discovery = SingleHostDiscovery.fromConfig(config); + -const entityClient = new CatalogEntityClient({ discovery }); + +const catalogClient = new CatalogClient({ discoveryApi: discovery }) + + return await createRouter({ + preparers, + templaters, + publishers, + logger, + config, + dockerClient, + - entityClient, + database, + + catalogClient, + }); + ``` + + As well as adding the `@backstage/catalog-client` packages as a dependency of your backend package. + +### Patch Changes + +- Updated dependencies [bad21a085] +- Updated dependencies [a1f5e6545] + - @backstage/catalog-model@0.7.2 + - @backstage/config@0.1.3 + +## 0.7.1 + +### Patch Changes + +- edbc27bfd: Added githubApp authentication to the scaffolder-backend plugin +- fb28da212: Switched to using `'x-access-token'` for authenticating Git over HTTPS towards GitHub. +- 0ada34a0f: Minor typo in migration +- 29c8bcc53: Fixed the `prepare` step for when using local templates that were added to the catalog using the `file:` target configuration. + No more `EPERM: operation not permitted` error messages. +- a341a8716: Fix parsing of the path to default to empty string not undefined if git-url-parse throws something we don't expect. Fixes the error `The "path" argument must be of type string.` when preparing. +- Updated dependencies [16fb1d03a] +- Updated dependencies [491f3a0ec] +- Updated dependencies [491f3a0ec] +- Updated dependencies [434b4e81a] +- Updated dependencies [fb28da212] + - @backstage/backend-common@0.5.4 + - @backstage/integration@0.5.0 + ## 0.7.0 ### Minor Changes diff --git a/plugins/scaffolder-backend/migrations/20210120143715_init.js b/plugins/scaffolder-backend/migrations/20210120143715_init.js index 256ec7d423..5bde7a7c68 100644 --- a/plugins/scaffolder-backend/migrations/20210120143715_init.js +++ b/plugins/scaffolder-backend/migrations/20210120143715_init.js @@ -77,7 +77,7 @@ exports.up = async function up(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'); + table.dropIndex([], 'task_events_task_id_idx'); }); } await knex.schema.dropTable('task_events'); diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 3b25fb3881..4d9d5f5404 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder-backend", - "version": "0.7.0", + "version": "0.8.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,11 +29,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/backend-common": "^0.5.3", + "@backstage/backend-common": "^0.5.4", "@backstage/catalog-client": "^0.3.6", - "@backstage/catalog-model": "^0.7.1", - "@backstage/config": "^0.1.2", - "@backstage/integration": "^0.4.0", + "@backstage/catalog-model": "^0.7.2", + "@backstage/config": "^0.1.3", + "@backstage/integration": "^0.5.0", "@gitbeaker/core": "^28.0.2", "@gitbeaker/node": "^28.0.2", "@octokit/rest": "^18.0.12", @@ -51,25 +51,27 @@ "fs-extra": "^9.0.0", "git-url-parse": "^11.4.4", "globby": "^11.0.0", + "handlebars": "^4.7.6", "helmet": "^4.0.0", "isomorphic-git": "^1.8.0", "jsonschema": "^1.2.6", "knex": "^0.21.6", + "luxon": "^1.26.0", "morgan": "^1.10.0", "uuid": "^8.2.0", "winston": "^3.2.1", "yaml": "^1.10.0" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/test-utils": "^0.1.8", "@types/fs-extra": "^9.0.1", "@types/mock-fs": "^4.13.0", "@types/supertest": "^2.0.8", "mock-fs": "^4.13.0", + "msw": "^0.21.2", "supertest": "^4.0.2", - "yaml": "^1.10.0", - "msw": "^0.21.2" + "yaml": "^1.10.0" }, "files": [ "dist", diff --git a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts index 827ed0559a..cea0e85148 100644 --- a/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts +++ b/plugins/scaffolder-backend/src/lib/catalog/CatalogEntityClient.ts @@ -15,24 +15,14 @@ */ import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; -import { CatalogClient } from '@backstage/catalog-client'; -import { - ConflictError, - NotFoundError, - PluginEndpointDiscovery, -} from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import { ConflictError, NotFoundError } from '@backstage/backend-common'; /** * A catalog client tailored for reading out entity data from the catalog. */ export class CatalogEntityClient { - private readonly catalogClient: CatalogClient; - - constructor(options: { discovery: PluginEndpointDiscovery }) { - this.catalogClient = new CatalogClient({ - discoveryApi: options.discovery, - }); - } + constructor(private readonly catalogClient: CatalogApi) {} /** * Looks up a single template using a template name. diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts index f151780ba9..efba57a063 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/legacy.ts @@ -19,28 +19,37 @@ import { FilePreparer, PreparerBuilder } from './prepare'; import Docker from 'dockerode'; import { TemplaterBuilder, TemplaterValues } from './templater'; import { PublisherBuilder } from './publish'; +import { CatalogApi } from '@backstage/catalog-client'; +import { getEntityName } from '@backstage/catalog-model'; type Options = { dockerClient: Docker; preparers: PreparerBuilder; templaters: TemplaterBuilder; publishers: PublisherBuilder; + catalogClient: CatalogApi; }; export function registerLegacyActions( registry: TemplateActionRegistry, options: Options, ) { - const { dockerClient, preparers, templaters, publishers } = options; + const { + dockerClient, + preparers, + templaters, + publishers, + catalogClient, + } = options; registry.register({ id: 'legacy:prepare', async handler(ctx) { + ctx.logger.info('Preparing the skeleton'); 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, @@ -52,11 +61,8 @@ export function registerLegacyActions( registry.register({ id: 'legacy:template', async handler(ctx) { - const { logger } = ctx; - + ctx.logger.info('Running the templater'); const templater = templaters.get(ctx.parameters.templater as string); - - logger.info('Run the templater'); await templater.run({ workspacePath: ctx.workspacePath, dockerClient, @@ -107,4 +113,21 @@ export function registerLegacyActions( } }, }); + + registry.register({ + id: 'catalog:register', + async handler(ctx) { + const { catalogInfoUrl } = ctx.parameters; + ctx.logger.info(`Registering ${catalogInfoUrl} in the catalog`); + + const result = await catalogClient.addLocation({ + type: 'url', + target: catalogInfoUrl as string, + }); + if (result.entities.length >= 1) { + const { kind, name, namespace } = getEntityName(result.entities[0]); + ctx.output('entityRef', `${kind}:${namespace}/${name}`); + } + }, + }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts index 5b74ee81ca..81ccdeb026 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/prepare/github.ts @@ -18,14 +18,20 @@ import path from 'path'; import { Git } from '@backstage/backend-common'; import { PreparerBase, PreparerOptions } from './types'; import parseGitUrl from 'git-url-parse'; -import { GitHubIntegrationConfig } from '@backstage/integration'; +import { + GitHubIntegrationConfig, + GithubCredentialsProvider, +} from '@backstage/integration'; export class GithubPreparer implements PreparerBase { static fromConfig(config: GitHubIntegrationConfig) { - return new GithubPreparer({ token: config.token }); + const credentialsProvider = GithubCredentialsProvider.create(config); + return new GithubPreparer({ credentialsProvider }); } - constructor(private readonly config: { token?: string }) {} + constructor( + private readonly config: { credentialsProvider: GithubCredentialsProvider }, + ) {} async prepare({ url, workspacePath, logger }: PreparerOptions) { const parsedGitUrl = parseGitUrl(url); @@ -36,10 +42,14 @@ export class GithubPreparer implements PreparerBase { parsedGitUrl.filepath ?? '', ); - const git = this.config.token + const { token } = await this.config.credentialsProvider.getCredentials({ + url, + }); + + const git = token ? Git.fromAuth({ username: 'x-access-token', - password: this.config.token, + password: token, logger, }) : Git.fromAuth({ logger }); diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts index 373920c005..102a3a7d02 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/bitbucket.ts @@ -98,10 +98,6 @@ export class BitbucketPublisher implements PublisherBase { const { project, name, description } = opts; let response: Response; - const buffer = Buffer.from( - `${this.config.username}:${this.config.appPassword}`, - 'utf8', - ); const options: RequestInit = { method: 'POST', @@ -111,7 +107,7 @@ export class BitbucketPublisher implements PublisherBase { is_private: this.config.repoVisibility === 'private', }), headers: { - Authorization: `Basic ${buffer.toString('base64')}`, + Authorization: this.getAuthorizationHeader(), 'Content-Type': 'application/json', }, }; @@ -132,13 +128,32 @@ export class BitbucketPublisher implements PublisherBase { } } - // TODO use the urlReader to get the defautl branch + // TODO use the urlReader to get the default branch const catalogInfoUrl = `${r.links.html.href}/src/master/catalog-info.yaml`; return { remoteUrl, catalogInfoUrl }; } throw new Error(`Not a valid response code ${await response.text()}`); } + private getAuthorizationHeader(): string { + if (this.config.username && this.config.appPassword) { + const buffer = Buffer.from( + `${this.config.username}:${this.config.appPassword}`, + 'utf8', + ); + + return `Basic ${buffer.toString('base64')}`; + } + + if (this.config.token) { + return `Bearer ${this.config.token}`; + } + + throw new Error( + `Authorization has not been provided for Bitbucket. Please add either username + appPassword or token to the Integrations config`, + ); + } + private async createBitbucketServerRepository(opts: { project: string; name: string; @@ -155,10 +170,11 @@ export class BitbucketPublisher implements PublisherBase { is_private: this.config.repoVisibility === 'private', }), headers: { - Authorization: `Bearer ${this.config.token}`, + Authorization: this.getAuthorizationHeader(), 'Content-Type': 'application/json', }, }; + try { response = await fetch( `https://${this.config.host}/rest/api/1.0/projects/${project}/repos`, diff --git a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts index 407a1633c2..a449eddd5d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/stages/publish/github.ts @@ -16,7 +16,10 @@ import { PublisherBase, PublisherOptions, PublisherResult } from './types'; import { initRepoAndPush } from './helpers'; -import { GitHubIntegrationConfig } from '@backstage/integration'; +import { + GitHubIntegrationConfig, + GithubCredentialsProvider, +} from '@backstage/integration'; import parseGitUrl from 'git-url-parse'; import { Octokit } from '@octokit/rest'; import path from 'path'; @@ -28,27 +31,24 @@ export class GithubPublisher implements PublisherBase { config: GitHubIntegrationConfig, { repoVisibility }: { repoVisibility: RepoVisibilityOptions }, ) { - if (!config.token) { + if (!config.token && !config.apps) { return undefined; } - const githubClient = new Octokit({ - auth: config.token, - baseUrl: config.apiBaseUrl, - }); + const credentialsProvider = GithubCredentialsProvider.create(config); return new GithubPublisher({ - token: config.token, - client: githubClient, + credentialsProvider, repoVisibility, + apiBaseUrl: config.apiBaseUrl, }); } constructor( private readonly config: { - token: string; - client: Octokit; + credentialsProvider: GithubCredentialsProvider; repoVisibility: RepoVisibilityOptions; + apiBaseUrl: string | undefined; }, ) {} @@ -59,9 +59,25 @@ export class GithubPublisher implements PublisherBase { }: PublisherOptions): Promise { const { owner, name } = parseGitUrl(values.storePath); + const { token } = await this.config.credentialsProvider.getCredentials({ + url: values.storePath, + }); + + if (!token) { + throw new Error( + `No token could be acquired for URL: ${values.storePath}`, + ); + } + + const client = new Octokit({ + auth: token, + baseUrl: this.config.apiBaseUrl, + }); + const description = values.description as string; const access = values.access as string; const remoteUrl = await this.createRemote({ + client, description, access, name, @@ -73,7 +89,7 @@ export class GithubPublisher implements PublisherBase { remoteUrl, auth: { username: 'x-access-token', - password: this.config.token, + password: token, }, logger, }); @@ -86,27 +102,28 @@ export class GithubPublisher implements PublisherBase { } private async createRemote(opts: { + client: Octokit; access: string; name: string; owner: string; description: string; }) { - const { access, description, owner, name } = opts; + const { client, access, description, owner, name } = opts; - const user = await this.config.client.users.getByUsername({ + const user = await client.users.getByUsername({ username: owner, }); const repoCreationPromise = user.data.type === 'Organization' - ? this.config.client.repos.createInOrg({ + ? client.repos.createInOrg({ name, org: owner, private: this.config.repoVisibility !== 'public', visibility: this.config.repoVisibility, description, }) - : this.config.client.repos.createForAuthenticatedUser({ + : client.repos.createForAuthenticatedUser({ name, private: this.config.repoVisibility === 'private', description, @@ -116,7 +133,7 @@ export class GithubPublisher implements PublisherBase { if (access?.startsWith(`${owner}/`)) { const [, team] = access.split('/'); - await this.config.client.teams.addOrUpdateRepoPermissionsInOrg({ + await client.teams.addOrUpdateRepoPermissionsInOrg({ org: owner, team_slug: team, owner, @@ -125,7 +142,7 @@ export class GithubPublisher implements PublisherBase { }); // no need to add access if it's the person who own's the personal account } else if (access && access !== owner) { - await this.config.client.repos.addCollaborator({ + await client.repos.addCollaborator({ owner, repo: name, username: access, diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts index 5717f3c8c1..d069008282 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts @@ -32,6 +32,7 @@ import { TaskStoreEmitOptions, TaskStoreGetEventsOptions, } from './types'; +import { DateTime } from 'luxon'; const migrationsDir = resolvePackagePath( '@backstage/plugin-scaffolder-backend', @@ -64,7 +65,7 @@ export class DatabaseTaskStore implements TaskStore { constructor(private readonly db: Knex) {} - async get(taskId: string): Promise { + async getTask(taskId: string): Promise { const [result] = await this.db('tasks') .where({ id: taskId }) .select(); @@ -255,11 +256,14 @@ export class DatabaseTaskStore implements TaskStore { try { const body = JSON.parse(event.body) as JsonObject; return { - id: event.id, + id: Number(event.id), taskId, body, type: event.event_type, - createdAt: event.created_at, + createdAt: + typeof event.created_at === 'string' + ? DateTime.fromSQL(event.created_at, { zone: 'UTC' }).toISO() + : event.created_at, }; } catch (error) { throw new Error( diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts index c45fa46335..fd2de30332 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.test.ts @@ -47,7 +47,7 @@ describe('StorageTaskBroker', () => { const logger = getVoidLogger(); it('should claim a dispatched work item', async () => { const broker = new StorageTaskBroker(storage, logger); - await broker.dispatch({ steps: [] }); + await broker.dispatch({} as TaskSpec); await expect(broker.claim()).resolves.toEqual(expect.any(TaskAgent)); }); @@ -57,7 +57,7 @@ describe('StorageTaskBroker', () => { await expect(Promise.race([promise, 'waiting'])).resolves.toBe('waiting'); - await broker.dispatch({ steps: [] }); + await broker.dispatch({} as TaskSpec); await expect(promise).resolves.toEqual(expect.any(TaskAgent)); }); @@ -80,19 +80,19 @@ describe('StorageTaskBroker', () => { it('should complete a task', async () => { const broker = new StorageTaskBroker(storage, logger); - const dispatchResult = await broker.dispatch({ steps: [] }); + const dispatchResult = await broker.dispatch({} as TaskSpec); const task = await broker.claim(); await task.complete('completed'); - const taskRow = await storage.get(dispatchResult.taskId); + const taskRow = await storage.getTask(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 dispatchResult = await broker.dispatch({} as TaskSpec); const task = await broker.claim(); await task.complete('failed'); - const taskRow = await storage.get(dispatchResult.taskId); + const taskRow = await storage.getTask(dispatchResult.taskId); expect(taskRow.status).toBe('failed'); }); @@ -100,7 +100,7 @@ describe('StorageTaskBroker', () => { const broker1 = new StorageTaskBroker(storage, logger); const broker2 = new StorageTaskBroker(storage, logger); - const { taskId } = await broker1.dispatch({ steps: [] }); + const { taskId } = await broker1.dispatch({} as TaskSpec); const logPromise = new Promise(resolve => { const observedEvents = new Array(); @@ -139,13 +139,13 @@ describe('StorageTaskBroker', () => { it('should heartbeat', async () => { const broker = new StorageTaskBroker(storage, logger); - const { taskId } = await broker.dispatch({ steps: [] }); + const { taskId } = await broker.dispatch({} as TaskSpec); const task = await broker.claim(); - const initialTask = await storage.get(taskId); + const initialTask = await storage.getTask(taskId); for (;;) { - const maybeTask = await storage.get(taskId); + const maybeTask = await storage.getTask(taskId); if (maybeTask.lastHeartbeatAt !== initialTask.lastHeartbeatAt) { break; } @@ -157,7 +157,7 @@ describe('StorageTaskBroker', () => { 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 { taskId } = await broker.dispatch({} as TaskSpec); const task = await broker.claim(); jest @@ -169,7 +169,7 @@ describe('StorageTaskBroker', () => { }, 500); for (;;) { - const maybeTask = await storage.get(taskId); + const maybeTask = await storage.getTask(taskId); if (maybeTask.status === 'failed') { break; } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 48c0d5e5ba..fb1f4ab422 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { JsonObject } from '@backstage/config'; import { Logger } from 'winston'; import { CompletedTaskState, @@ -22,6 +23,7 @@ import { TaskBroker, DispatchResult, DbTaskEventRow, + DbTaskRow, } from './types'; export class TaskAgent implements Task { @@ -54,18 +56,24 @@ export class TaskAgent implements Task { return this.isDone; } - async emitLog(message: string): Promise { + async emitLog(message: string, metadata?: JsonObject): Promise { await this.storage.emitLogEvent({ taskId: this.state.taskId, - body: { message }, + body: { message, ...metadata }, }); } - async complete(result: CompletedTaskState): Promise { + async complete( + result: CompletedTaskState, + metadata?: JsonObject, + ): Promise { await this.storage.completeTask({ taskId: this.state.taskId, status: result === 'failed' ? 'failed' : 'completed', - eventBody: { message: `Run completed with status: ${result}` }, + eventBody: { + message: `Run completed with status: ${result}`, + ...metadata, + }, }); this.isDone = true; if (this.heartbeatTimeoutId) { @@ -136,6 +144,10 @@ export class StorageTaskBroker implements TaskBroker { }; } + async get(taskId: string): Promise { + return this.storage.getTask(taskId); + } + observe( options: { taskId: string; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index c62495ca4c..e717fcae1b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -22,6 +22,7 @@ import { TaskBroker, Task } from './types'; import fs from 'fs-extra'; import path from 'path'; import { TemplateActionRegistry } from './TemplateConverter'; +import * as handlebars from 'handlebars'; type Options = { logger: Logger; @@ -44,67 +45,116 @@ export class TaskWorker { async runOneTask(task: Task) { try { - const { actionRegistry, logger } = this.options; + const { actionRegistry } = this.options; const workspacePath = path.join( this.options.workingDirectory, await task.getWorkspaceName(), ); await fs.ensureDir(workspacePath); + await task.emitLog( + `Starting up task with ${task.spec.steps.length} steps`, + ); - 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 } = {}; + const templateCtx: { + steps: { + [stepName: string]: { output: { [outputName: string]: JsonValue } }; + }; + } = { steps: {} }; for (const step of task.spec.steps) { - task.emitLog(`Beginning step ${step.name}`); + const metadata = { stepId: step.id }; + try { + 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 action = actionRegistry.get(step.action); - if (!action) { - throw new Error(`Action '${step.action}' does not exist`); + const stream = new PassThrough(); + stream.on('data', async data => { + const message = data.toString().trim(); + if (message?.length > 1) { + await task.emitLog(message, metadata); + } + }); + + taskLogger.add(new winston.transports.Stream({ stream })); + await task.emitLog(`Beginning step ${step.name}`, { + ...metadata, + status: 'processing', + }); + + const action = actionRegistry.get(step.action); + if (!action) { + throw new Error(`Action '${step.action}' does not exist`); + } + + const parameters: { [name: string]: JsonValue } = {}; + for (const [name, maybeTemplateStr] of Object.entries( + step.parameters ?? {}, + )) { + if (typeof maybeTemplateStr === 'string') { + const value = handlebars.compile(maybeTemplateStr, { + noEscape: true, + strict: true, + data: false, + preventIndent: true, + })(templateCtx); + parameters[name] = value; + } else { + parameters[name] = maybeTemplateStr; + } + } + + const stepOutputs: { [name: string]: JsonValue } = {}; + + await action.handler({ + logger: taskLogger, + logStream: stream, + parameters, + workspacePath, + output(name: string, value: JsonValue) { + stepOutputs[name] = value; + }, + }); + + templateCtx.steps[step.id] = { output: stepOutputs }; + + await task.emitLog(`Finished step ${step.name}`, { + ...metadata, + status: 'completed', + }); + } catch (error) { + await task.emitLog(String(error.stack), { + ...metadata, + status: 'failed', + }); + throw error; } - - // 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'); + const output = Object.fromEntries( + Object.entries(task.spec.output).map(([name, templateStr]) => { + const value = handlebars.compile(templateStr, { + noEscape: true, + strict: true, + data: false, + preventIndent: true, + })(templateCtx); + return [name, value]; + }), + ); + + await task.complete('completed', { output }); } catch (error) { - task.emitLog(String(error.stack)); - await task.complete('failed'); + await task.complete('failed', { + error: { name: error.name, message: error.message }, + }); } } } diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts index 69788238cc..4139948513 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TemplateConverter.ts @@ -18,7 +18,7 @@ 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 { Writable } from 'stream'; import { TaskSpec } from './types'; import { ConflictError, NotFoundError } from '@backstage/backend-common'; @@ -69,14 +69,30 @@ export function templateEntityToSpec( steps.push({ id: 'publish', - name: 'Publishing', + name: 'Publish', action: 'legacy:publish', parameters: { values, }, }); - return { steps }; + steps.push({ + id: 'register', + name: 'Register', + action: 'catalog:register', + parameters: { + catalogInfoUrl: '{{ steps.publish.output.catalogInfoUrl }}', + }, + }); + + return { + steps, + output: { + remoteUrl: '{{ steps.publish.output.remoteUrl }}', + catalogInfoUrl: '{{ steps.publish.output.catalogInfoUrl }}', + entityRef: '{{ steps.register.output.entityRef }}', + }, + }; } type ActionContext = { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index 0c2592109b..ae18a59e41 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -49,6 +49,7 @@ export type TaskSpec = { action: string; parameters?: { [name: string]: JsonValue }; }>; + output: { [name: string]: string }; }; export type DispatchResult = { @@ -58,8 +59,8 @@ export type DispatchResult = { export interface Task { spec: TaskSpec; done: boolean; - emitLog(message: string): Promise; - complete(result: CompletedTaskState): Promise; + emitLog(message: string, metadata?: JsonValue): Promise; + complete(result: CompletedTaskState, metadata?: JsonValue): Promise; getWorkspaceName(): Promise; } @@ -90,6 +91,7 @@ export type TaskStoreGetEventsOptions = { }; export interface TaskStore { createTask(task: TaskSpec): Promise<{ taskId: string }>; + getTask(taskId: string): Promise; claimTask(): Promise; completeTask(options: { taskId: string; diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts index 8a0fa6cba6..8fd53e5fb6 100644 --- a/plugins/scaffolder-backend/src/service/router.test.ts +++ b/plugins/scaffolder-backend/src/service/router.test.ts @@ -39,12 +39,14 @@ import request from 'supertest'; import { createRouter } from './router'; import { Templaters, Preparers, Publishers } from '../scaffolder'; import Docker from 'dockerode'; +import { CatalogApi } from '@backstage/catalog-client'; jest.mock('dockerode'); -const generateEntityClient: any = (template: any) => ({ - findTemplate: () => Promise.resolve(template), -}); +const createCatalogClient = (templates: any[] = []) => + ({ + getEntities: async () => ({ items: templates }), + } as CatalogApi); function createDatabase(): PluginDatabaseManager { return SingleConnectionDatabaseManager.fromConfig( @@ -95,7 +97,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'); @@ -109,8 +110,8 @@ describe('createRouter - working directory', () => { publishers: new Publishers(), config: new ConfigReader(workDirConfig('/path')), dockerClient: new Docker(), - entityClient: mockedEntityClient, database: createDatabase(), + catalogClient: createCatalogClient([template]), }), ).rejects.toThrow('access error'); }); @@ -123,8 +124,8 @@ describe('createRouter - working directory', () => { publishers: new Publishers(), config: new ConfigReader(workDirConfig('/path')), dockerClient: new Docker(), - entityClient: mockedEntityClient, database: createDatabase(), + catalogClient: createCatalogClient([template]), }); const app = express().use(router); @@ -152,8 +153,8 @@ describe('createRouter - working directory', () => { publishers: new Publishers(), config: new ConfigReader({}), dockerClient: new Docker(), - entityClient: mockedEntityClient, database: createDatabase(), + catalogClient: createCatalogClient([template]), }); const app = express().use(router); @@ -184,6 +185,9 @@ describe('createRouter', () => { name: 'create-react-app-template', tags: ['experimental', 'react', 'cra'], title: 'Create React App Template', + annotations: { + 'backstage.io/managed-by-location': 'url:https://dev.azure.com', + }, }, spec: { owner: 'web@example.com', @@ -222,8 +226,8 @@ describe('createRouter', () => { publishers: new Publishers(), config: new ConfigReader({}), dockerClient: new Docker(), - entityClient: generateEntityClient(template), database: createDatabase(), + catalogClient: createCatalogClient([template]), }); app = express().use(router); }); @@ -246,4 +250,36 @@ describe('createRouter', () => { expect(response.status).toEqual(400); }); }); + + describe('POST /v2/tasks', () => { + it('rejects template values which do not match the template schema definition', async () => { + const response = await request(app) + .post('/v2/tasks') + .send({ + templateName: '', + values: { + storePath: 'https://github.com/backstage/backstage', + }, + }); + + expect(response.status).toEqual(400); + }); + + it('return the template id', async () => { + const response = await request(app) + .post('/v2/tasks') + .send({ + templateName: 'create-react-app-template', + values: { + storePath: 'https://github.com/backstage/backstage', + component_id: '123', + name: 'test', + use_typescript: false, + }, + }); + + expect(response.body.id).toBeDefined(); + expect(response.status).toEqual(201); + }); + }); }); diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index e8aa9e7c43..3d10bbd101 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -44,7 +44,11 @@ import { } from '../scaffolder/tasks/TemplateConverter'; import { registerLegacyActions } from '../scaffolder/stages/legacy'; import { getWorkingDirectory } from './helpers'; -import { PluginDatabaseManager } from '@backstage/backend-common'; +import { + NotFoundError, + PluginDatabaseManager, +} from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; export interface RouterOptions { preparers: PreparerBuilder; @@ -54,8 +58,8 @@ export interface RouterOptions { logger: Logger; config: Config; dockerClient: Docker; - entityClient: CatalogEntityClient; database: PluginDatabaseManager; + catalogClient: CatalogApi; } export async function createRouter( @@ -71,13 +75,14 @@ export async function createRouter( logger: parentLogger, config, dockerClient, - entityClient, database, + catalogClient, } = options; const logger = parentLogger.child({ plugin: 'scaffolder' }); const workingDirectory = await getWorkingDirectory(config, logger); const jobProcessor = await JobProcessor.fromConfig({ config, logger }); + const entityClient = new CatalogEntityClient(catalogClient); const databaseTaskStore = await DatabaseTaskStore.create( await database.getClient(), @@ -96,6 +101,7 @@ export async function createRouter( preparers, publishers, templaters, + catalogClient, }); worker.start(); @@ -249,6 +255,14 @@ export async function createRouter( res.status(201).json({ id: result.taskId }); }) + .get('/v2/tasks/:taskId', async (req, res) => { + const { taskId } = req.params; + const task = await taskBroker.get(taskId); + if (!task) { + throw new NotFoundError(`Task with id ${taskId} does not exist`); + } + res.status(200).json(task); + }) .get('/v2/tasks/:taskId/eventstream', async (req, res) => { const { taskId } = req.params; const after = Number(req.query.after) || undefined; diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md index 2e0676e3aa..5d5523f821 100644 --- a/plugins/scaffolder/CHANGELOG.md +++ b/plugins/scaffolder/CHANGELOG.md @@ -1,5 +1,134 @@ # @backstage/plugin-scaffolder +## 0.6.0 + +### Minor Changes + +- a5f42cf66: The Scaffolder and Catalog plugins have been migrated to partially require use of the [new composability API](https://backstage.io/docs/plugins/composability). The Scaffolder used to register its pages using the deprecated route registration plugin API, but those registrations have been removed. This means you now need to add the Scaffolder plugin page to the app directly. + + The page is imported from the Scaffolder plugin and added to the `` component: + + ```tsx + } /> + ``` + + The Catalog plugin has also been migrated to use an [external route reference](https://backstage.io/docs/plugins/composability#binding-external-routes-in-the-app) to dynamically link to the create component page. This means you need to migrate the catalog plugin to use the new extension components, as well as bind the external route. + + To use the new extension components, replace existing usage of the `CatalogRouter` with the following: + + ```tsx + } /> + }> + + + ``` + + And to bind the external route from the catalog plugin to the scaffolder template index page, make sure you have the appropriate imports and add the following to the `createApp` call: + + ```ts + import { catalogPlugin } from '@backstage/plugin-catalog'; + import { scaffolderPlugin } from '@backstage/plugin-scaffolder'; + + const app = createApp({ + // ... + bindRoutes({ bind }) { + bind(catalogPlugin.externalRoutes, { + createComponent: scaffolderPlugin.routes.root, + }); + }, + }); + ``` + +- d0760ecdf: Moved common useStarredEntities hook to plugin-catalog-react +- e8e35fb5f: Adding Search and Filter features to Scaffolder/Templates Grid + +### Patch Changes + +- a5f42cf66: # Stateless scaffolding + + The scaffolder has been redesigned to be horizontally scalable and to persistently store task state and execution logs in the database. + + Each scaffolder task is given a unique task ID which is persisted in the database. + Tasks are then picked up by a `TaskWorker` which performs the scaffolding steps. + Execution logs are also persisted in the database meaning you can now refresh the scaffolder task status page without losing information. + + The task status page is now dynamically created based on the step information stored in the database. + This allows for custom steps to be displayed once the next version of the scaffolder template schema is available. + + The task page is updated to display links to both the git repository and to the newly created catalog entity. + + Component registration has moved from the frontend into a separate registration step executed by the `TaskWorker`. This requires that a `CatalogClient` is passed to the scaffolder backend instead of the old `CatalogEntityClient`. + + Make sure to update `plugins/scaffolder.ts` + + ```diff + import { + CookieCutter, + createRouter, + Preparers, + Publishers, + CreateReactAppTemplater, + Templaters, + - CatalogEntityClient, + } from '@backstage/plugin-scaffolder-backend'; + + +import { CatalogClient } from '@backstage/catalog-client'; + + const discovery = SingleHostDiscovery.fromConfig(config); + -const entityClient = new CatalogEntityClient({ discovery }); + +const catalogClient = new CatalogClient({ discoveryApi: discovery }) + + return await createRouter({ + preparers, + templaters, + publishers, + logger, + config, + dockerClient, + - entityClient, + database, + + catalogClient, + }); + ``` + + As well as adding the `@backstage/catalog-client` packages as a dependency of your backend package. + +- e488f0502: Update messages that process during loading, error, and no templates found. + Remove unused dependencies. +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [a1f5e6545] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + - @backstage/config@0.1.3 + +## 0.5.1 + +### Patch Changes + +- 6c4a76c59: Make the `TemplateCard` conform to what material-ui recommends in their examples. This fixes the extra padding around the buttons. +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.5.0 ### Minor Changes diff --git a/plugins/scaffolder/dev/index.tsx b/plugins/scaffolder/dev/index.tsx index e75aadb03f..1501a1cd5e 100644 --- a/plugins/scaffolder/dev/index.tsx +++ b/plugins/scaffolder/dev/index.tsx @@ -19,8 +19,8 @@ import { createDevApp } from '@backstage/dev-utils'; import { discoveryApiRef, identityApiRef } from '@backstage/core'; import { CatalogClient } from '@backstage/catalog-client'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { TemplateIndexPage, TemplatePage } from '../src/plugin'; -import { ScaffolderApi, scaffolderApiRef } from '../src'; +import { ScaffolderPage } from '../src/plugin'; +import { ScaffolderClient, scaffolderApiRef } from '../src'; createDevApp() .registerApi({ @@ -32,15 +32,11 @@ createDevApp() api: scaffolderApiRef, deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, factory: ({ discoveryApi, identityApi }) => - new ScaffolderApi({ discoveryApi, identityApi }), + new ScaffolderClient({ discoveryApi, identityApi }), }) .addPage({ path: '/create', title: 'Create', - element: , - }) - .addPage({ - path: '/create/:templateName', - element: , + element: , }) .render(); diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 2b0e1a3771..7b288a25f1 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-scaffolder", - "version": "0.5.0", + "version": "0.6.0", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,11 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-client": "^0.3.6", + "@backstage/catalog-model": "^0.7.2", + "@backstage/config": "^0.1.3", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,23 +43,27 @@ "@rjsf/material-ui": "^2.4.0", "classnames": "^2.2.6", "git-url-parse": "^11.4.4", - "moment": "^2.26.0", + "humanize-duration": "^3.25.1", + "luxon": "^1.25.0", "react": "^16.13.1", "react-dom": "^16.13.1", "react-lazylog": "^4.5.2", "react-router": "6.0.0-beta.0", "react-router-dom": "6.0.0-beta.0", "react-use": "^15.3.3", - "swr": "^0.3.0" + "swr": "^0.3.0", + "use-immer": "^0.4.2", + "zen-observable": "^0.8.15" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", - "@backstage/catalog-client": "^0.3.6", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", + "@types/humanize-duration": "^3.18.1", + "@testing-library/react-hooks": "^3.3.0", "@types/jest": "^26.0.7", "@types/node": "^12.0.0", "cross-fetch": "^3.0.6", diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index 453b1b6a7e..20fca5b3f2 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -14,14 +14,53 @@ * limitations under the License. */ -import { createApiRef, DiscoveryApi, IdentityApi } from '@backstage/core'; +import { + createApiRef, + DiscoveryApi, + Observable, + IdentityApi, +} from '@backstage/core'; +import ObservableImpl from 'zen-observable'; +import { ScaffolderTask, Status } from './types'; export const scaffolderApiRef = createApiRef({ id: 'plugin.scaffolder.service', description: 'Used to make requests towards the scaffolder backend', }); -export class ScaffolderApi { +export type LogEvent = { + type: 'log' | 'completion'; + body: { + message: string; + stepId?: string; + status?: Status; + }; + createdAt: string; + id: string; + taskId: string; +}; + +export interface ScaffolderApi { + /** + * Executes the scaffolding of a component, given a template and its + * parameter values. + * + * @param templateName Name of the Template entity for the scaffolder to use. New project is going to be created out of this template. + * @param values Parameters for the template, e.g. name, description + */ + scaffold(templateName: string, values: Record): Promise; + + getTask(taskId: string): Promise; + + streamLogs({ + taskId, + after, + }: { + taskId: string; + after?: number; + }): Observable; +} +export class ScaffolderClient implements ScaffolderApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; @@ -40,9 +79,12 @@ export class ScaffolderApi { * @param templateName Template name for the scaffolder to use. New project is going to be created out of this template. * @param values Parameters for the template, e.g. name, description */ - async scaffold(templateName: string, values: Record) { + async scaffold( + templateName: string, + values: Record, + ): Promise { const token = await this.identityApi.getIdToken(); - const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v1/jobs`; + const url = `${await this.discoveryApi.getBaseUrl('scaffolder')}/v2/tasks`; const response = await fetch(url, { method: 'POST', headers: { @@ -58,16 +100,65 @@ export class ScaffolderApi { throw new Error(`Backend request failed, ${status} ${body.trim()}`); } - const { id } = await response.json(); + const { id } = (await response.json()) as { id: string }; return id; } - async getJob(jobId: string) { + async getTask(taskId: string) { const token = await this.identityApi.getIdToken(); const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder'); - const url = `${baseUrl}/v1/job/${encodeURIComponent(jobId)}`; + const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}`; return fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }).then(x => x.json()); } + + streamLogs({ + taskId, + after, + }: { + taskId: string; + after?: number; + }): Observable { + return new ObservableImpl(subscriber => { + const params = new URLSearchParams(); + if (after !== undefined) { + params.set('after', String(Number(after))); + } + + this.discoveryApi.getBaseUrl('scaffolder').then( + baseUrl => { + const url = `${baseUrl}/v2/tasks/${encodeURIComponent( + taskId, + )}/eventstream`; + const eventSource = new EventSource(url); + eventSource.addEventListener('log', (event: any) => { + if (event.data) { + try { + subscriber.next(JSON.parse(event.data)); + } catch (ex) { + subscriber.error(ex); + } + } + }); + eventSource.addEventListener('completion', (event: any) => { + if (event.data) { + try { + subscriber.next(JSON.parse(event.data)); + } catch (ex) { + subscriber.error(ex); + } + } + subscriber.complete(); + }); + eventSource.addEventListener('error', event => { + subscriber.error(event); + }); + }, + error => { + subscriber.error(error); + }, + ); + }); + } } diff --git a/plugins/scaffolder/src/components/JobStage/JobStage.tsx b/plugins/scaffolder/src/components/JobStage/JobStage.tsx deleted file mode 100644 index f285cb98c0..0000000000 --- a/plugins/scaffolder/src/components/JobStage/JobStage.tsx +++ /dev/null @@ -1,169 +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 { - Accordion, - AccordionDetails, - AccordionSummary, - AccordionActions, - Box, - CircularProgress, - LinearProgress, - Typography, - Button, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; -import ExpandLessIcon from '@material-ui/icons/ExpandLess'; -import cn from 'classnames'; -import moment from 'moment'; -import React, { Suspense, useEffect, useState } from 'react'; -import { LogModal } from './LogModal'; -import { Job } from '../../types'; - -const LazyLog = React.lazy(() => import('react-lazylog/build/LazyLog')); -moment.relativeTimeThreshold('ss', 0); - -const useStyles = makeStyles(theme => ({ - accordionDetails: { - padding: 0, - }, - button: { - order: -1, - margin: '0 1em 0 -20px', - }, - cardContent: { - backgroundColor: theme.palette.background.default, - }, - accordion: { - position: 'relative', - '&:after': { - pointerEvents: 'none', - content: '""', - position: 'absolute', - top: 0, - right: 0, - left: 0, - bottom: 0, - }, - }, - neutral: {}, - failed: { - '&:after': { - boxShadow: `inset 4px 0px 0px ${theme.palette.error.main}`, - }, - }, - started: { - '&:after': { - boxShadow: `inset 4px 0px 0px ${theme.palette.info.main}`, - }, - }, - completed: { - '&:after': { - boxShadow: `inset 4px 0px 0px ${theme.palette.success.main}`, - }, - }, - jobStatusTitle: { - display: 'flex', - width: '100%', - alignItems: 'center', - flexDirection: 'row', - justifyContent: 'space-between', - [theme.breakpoints.down('xs')]: { - flexDirection: 'column', - alignItems: 'flex-start', - justifyContent: 'flex-start', - }, - }, -})); - -type Props = { - name: string; - className?: string; - log: string[]; - startedAt: string; - endedAt?: string; - status: Job['status']; -}; - -export const JobStage = ({ endedAt, startedAt, name, log, status }: Props) => { - const classes = useStyles(); - - const [expanded, setExpanded] = useState(false); - useEffect(() => { - if (status === 'FAILED') setExpanded(true); - }, [status, setExpanded]); - - const timeElapsed = - status !== 'PENDING' - ? moment - .duration(moment(endedAt ?? moment()).diff(moment(startedAt))) - .humanize() - : null; - - const [logsFullScreen, setLogsFullScreen] = useState(false); - const toggleLogsFullScreen = () => setLogsFullScreen(!logsFullScreen); - - return ( - ] ?? - classes.neutral, - )} - expanded={expanded} - onChange={(_, newState) => setExpanded(newState)} - > - : } - aria-controls={`panel-${name}-content`} - id={`panel-${name}-header`} - IconButtonProps={{ - className: classes.button, - }} - > - - {name} {timeElapsed && `(${timeElapsed})`}{' '} - {startedAt && !endedAt && } - - - - {log.length === 0 ? ( - - No logs available for this step - - ) : ( - }> - -
- -
-
- )} -
- - - -
- ); -}; diff --git a/plugins/scaffolder/src/components/JobStage/LogModal.tsx b/plugins/scaffolder/src/components/JobStage/LogModal.tsx deleted file mode 100644 index 73b6253510..0000000000 --- a/plugins/scaffolder/src/components/JobStage/LogModal.tsx +++ /dev/null @@ -1,67 +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 { - Dialog, - DialogTitle, - DialogContent, - IconButton, -} from '@material-ui/core'; -import { makeStyles } from '@material-ui/core/styles'; -import Close from '@material-ui/icons/Close'; -import LazyLog from 'react-lazylog/build/LazyLog'; - -type Props = { - log: string[]; - open?: boolean; - onClose(): void; -}; - -const useStyles = makeStyles(theme => ({ - header: { - width: '100%', - padding: theme.spacing(1, 4), - }, - closeIcon: { - float: 'right', - padding: theme.spacing(0.5, 0), - }, - logs: { - boxShadow: '-3px -1px 7px 0px rgba(50, 50, 50, 0.59)', - height: '100%', - width: '100%', - }, -})); - -export const LogModal = ({ log, open = false, onClose }: Props) => { - const classes = useStyles(); - - return ( - - - Logs - - - - - -
- -
-
-
- ); -}; diff --git a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx b/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx deleted file mode 100644 index d35385ec3b..0000000000 --- a/plugins/scaffolder/src/components/JobStatusModal/JobStatusModal.tsx +++ /dev/null @@ -1,95 +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 { Button } from '@backstage/core'; -import { - Button as Action, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - LinearProgress, -} from '@material-ui/core'; - -import React, { useCallback } from 'react'; -import { Job } from '../../types'; -import { JobStage } from '../JobStage/JobStage'; - -type Props = { - job: Job | null; - toCatalogLink?: string; - open: boolean; - onModalClose: () => void; -}; - -export const JobStatusModal = ({ - job, - toCatalogLink, - open, - onModalClose, -}: Props) => { - const renderTitle = () => { - switch (job?.status) { - case 'COMPLETED': - return 'Successfully created component'; - case 'FAILED': - return 'Failed to create component'; - default: - return 'Create component'; - } - }; - - const onClose = useCallback(() => { - if (!job) { - return; - } - // Disallow closing modal if the job is in progress. - if (job.status === 'COMPLETED' || job.status === 'FAILED') { - onModalClose(); - } - }, [job, onModalClose]); - - return ( - - {renderTitle()} - - {!job ? ( - - ) : ( - (job?.stages ?? []).map(step => ( - - )) - )} - - {job?.status && toCatalogLink && ( - - - - )} - {job?.status === 'FAILED' && ( - - Close - - )} - - ); -}; diff --git a/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.test.tsx b/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.test.tsx new file mode 100644 index 0000000000..22de59b6ef --- /dev/null +++ b/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.test.tsx @@ -0,0 +1,107 @@ +/* + * 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 { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { + ApiProvider, + ApiRegistry, + IdentityApi, + identityApiRef, + storageApiRef, +} from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { render } from '@testing-library/react'; +import React from 'react'; +import { EntityFilterGroupsProvider } from '../../filter'; +import { ResultsFilter } from './ResultsFilter'; + +describe('Results Filter', () => { + const catalogApi: Partial = { + getEntities: () => + Promise.resolve({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity1', + tags: ['java'], + }, + spec: { + owner: 'tools@example.com', + type: 'service', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity2', + }, + spec: { + owner: 'not-tools@example.com', + type: 'service', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity3', + tags: ['java', 'test'], + }, + spec: { + owner: 'tools@example.com', + type: 'service', + }, + }, + ] as Entity[], + }), + }; + + const identityApi: Partial = { + getUserId: () => 'tools@example.com', + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children}, + , + ), + ); + + it('should render all available categories', async () => { + const categories = ['test', 'java']; + const { findByText } = renderWrapped( + , + ); + for (const category of categories) { + expect( + await findByText(category.charAt(0).toUpperCase() + category.slice(1)), + ).toBeInTheDocument(); + } + }); +}); diff --git a/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx b/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx new file mode 100644 index 0000000000..301c6b01d8 --- /dev/null +++ b/plugins/scaffolder/src/components/ResultsFilter/ResultsFilter.tsx @@ -0,0 +1,118 @@ +/* + * 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 { + Button, + Checkbox, + Divider, + List, + ListItem, + ListItemText, + makeStyles, + Theme, + Typography, +} from '@material-ui/core'; +import React, { useContext } from 'react'; +import { filterGroupsContext } from '../../filter/context'; + +const useStyles = makeStyles(theme => ({ + filterBox: { + display: 'flex', + margin: theme.spacing(2, 0, 0, 0), + }, + filterBoxTitle: { + margin: theme.spacing(1, 0, 0, 1), + fontWeight: 'bold', + flex: 1, + }, + title: { + margin: theme.spacing(1, 0, 0, 1), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + checkbox: { + padding: theme.spacing(0, 1, 0, 1), + }, +})); + +type Props = { + availableCategories: string[]; +}; + +/** + * The additional results filter in the sidebar. + */ +export const ResultsFilter = ({ availableCategories }: Props) => { + const classes = useStyles(); + + const context = useContext(filterGroupsContext); + if (!context) { + throw new Error(`Must be used inside an EntityFilterGroupsProvider`); + } + + const { selectedCategories, setSelectedCategories } = context; + + return ( + <> +
+ + Refine Results + {' '} + +
+ + + Categories + + + {availableCategories.map(category => { + const labelId = `checkbox-list-label-${category}`; + return ( + + setSelectedCategories( + selectedCategories.includes(category) + ? selectedCategories.filter( + selectedCategory => selectedCategory !== category, + ) + : [...selectedCategories, category], + ) + } + > + + + + ); + })} + + + ); +}; diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx new file mode 100644 index 0000000000..12d4ecce82 --- /dev/null +++ b/plugins/scaffolder/src/components/Router.tsx @@ -0,0 +1,29 @@ +/* + * 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 React from 'react'; +import { Routes, Route } from 'react-router'; +import { ScaffolderPage } from './ScaffolderPage'; +import { TemplatePage } from './TemplatePage'; +import { TaskPage } from './TaskPage'; + +export const Router = () => ( + + } /> + } /> + } /> + +); diff --git a/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.test.tsx b/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.test.tsx new file mode 100644 index 0000000000..b5ec45d8bb --- /dev/null +++ b/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.test.tsx @@ -0,0 +1,271 @@ +/* + * 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 { MockStorageApi, wrapInTestApp } from '@backstage/test-utils'; +import { fireEvent, render, waitFor } from '@testing-library/react'; +import { + ApiProvider, + ApiRegistry, + IdentityApi, + identityApiRef, + storageApiRef, +} from '@backstage/core'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { EntityFilterGroupsProvider } from '../../filter'; +import { ButtonGroup, ScaffolderFilter } from './ScaffolderFilter'; + +describe('Catalog Filter', () => { + const catalogApi: Partial = { + getEntities: () => + Promise.resolve({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity1', + }, + spec: { + owner: 'tools@example.com', + type: 'service', + }, + }, + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'Entity2', + }, + spec: { + owner: 'not-tools@example.com', + type: 'service', + }, + }, + ] as Entity[], + }), + }; + + const identityApi: Partial = { + getUserId: () => 'tools@example.com', + }; + + const renderWrapped = (children: React.ReactNode) => + render( + wrapInTestApp( + + {children}, + , + ), + ); + + describe('filter groups', () => { + it('should render the different groups', async () => { + const mockGroups: ButtonGroup[] = [ + { name: 'Test Group 1', items: [] }, + { name: 'Test Group 2', items: [] }, + ]; + const { findByText } = renderWrapped( + , + ); + for (const group of mockGroups) { + expect(await findByText(group.name)).toBeInTheDocument(); + } + }); + }); + + describe('filter items', () => { + it('should render the different items and their names', async () => { + const mockGroups: ButtonGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'all', + label: 'First Label', + filterFn: () => true, + }, + { + id: 'starred', + label: 'Second Label', + filterFn: () => false, + }, + ], + }, + ]; + + const { findByText } = renderWrapped( + , + ); + + for (const item of mockGroups[0].items) { + expect(await findByText(item.label)).toBeInTheDocument(); + } + }); + + it('selects the first item if no desired initial one is set', async () => { + const mockGroups: ButtonGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'all', + label: 'First Label', + filterFn: () => true, + }, + { + id: 'starred', + label: 'Second Label', + filterFn: () => false, + }, + ], + }, + ]; + + const onChange = jest.fn(); + + renderWrapped( + , + ); + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith({ + id: 'all', + label: 'First Label', + }); + }); + }); + + it('selects the initial item', async () => { + const mockGroups: ButtonGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'all', + label: 'First Label', + filterFn: () => true, + }, + { + id: 'starred', + label: 'Second Label', + filterFn: () => false, + }, + ], + }, + ]; + + const onChange = jest.fn(); + + renderWrapped( + , + ); + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith({ + id: 'starred', + label: 'Second Label', + }); + }); + }); + + it('can change the selected item', async () => { + const mockGroups: ButtonGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'all', + label: 'First Label', + filterFn: () => true, + }, + { + id: 'starred', + label: 'Second Label', + filterFn: () => false, + }, + ], + }, + ]; + + const onChange = jest.fn(); + + const { findByText } = renderWrapped( + , + ); + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith({ + id: 'all', + label: 'First Label', + }); + }); + + fireEvent.click(await findByText('Second Label')); + + await waitFor(() => { + expect(onChange).toHaveBeenLastCalledWith({ + id: 'starred', + label: 'Second Label', + }); + }); + }); + + it('displays match counts properly', async () => { + const mockGroups: ButtonGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'owned', + label: 'First Label', + filterFn: entity => entity.spec?.owner === 'tools@example.com', + }, + ], + }, + ]; + + const { findByText } = renderWrapped( + , + ); + + expect(await findByText('1')).toBeInTheDocument(); + }); + }); +}); diff --git a/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.tsx b/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.tsx new file mode 100644 index 0000000000..14093462b8 --- /dev/null +++ b/plugins/scaffolder/src/components/ScaffolderFilter/ScaffolderFilter.tsx @@ -0,0 +1,211 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; +import { IconComponent } from '@backstage/core'; +import { + Card, + List, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + makeStyles, + MenuItem, + Theme, + Typography, +} from '@material-ui/core'; +import React, { + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from 'react'; +import { FilterGroup, useEntityFilterGroup } from '../../filter'; + +export type ButtonGroup = { + name: string; + items: { + id: string; + label: string; + icon?: IconComponent; + filterFn: (entity: Entity) => boolean; + }[]; +}; + +const useStyles = makeStyles(theme => ({ + root: { + backgroundColor: 'rgba(0, 0, 0, .11)', + boxShadow: 'none', + }, + title: { + margin: theme.spacing(1, 0, 0, 1), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + listIcon: { + minWidth: 30, + color: theme.palette.text.primary, + }, + menuItem: { + minHeight: theme.spacing(6), + }, + groupWrapper: { + margin: theme.spacing(1, 1, 2, 1), + }, + menuTitle: { + fontWeight: 500, + }, +})); + +type OnChangeCallback = (item: { id: string; label: string }) => void; + +type Props = { + buttonGroups: ButtonGroup[]; + initiallySelected: string; + onChange?: OnChangeCallback; +}; + +/** + * The main filter group in the sidebar, toggling owned/starred/all. + */ +export const ScaffolderFilter = ({ + buttonGroups, + onChange, + initiallySelected, +}: Props) => { + const classes = useStyles(); + const { currentFilter, setCurrentFilter, getFilterCount } = useFilter( + buttonGroups, + initiallySelected, + ); + + const onChangeRef = useRef(); + useEffect(() => { + onChangeRef.current = onChange; + }, [onChange]); + + const setCurrent = useCallback( + (item: { id: string; label: string }) => { + setCurrentFilter(item.id); + onChangeRef.current?.({ id: item.id, label: item.label }); + }, + [setCurrentFilter], + ); + + // Make one initial onChange to inform the surroundings about the selected + // item + useEffect(() => { + const items = buttonGroups.flatMap(g => g.items); + const item = items.find(i => i.id === initiallySelected) || items[0]; + if (item) { + onChangeRef.current?.({ id: item.id, label: item.label }); + } + // intentionally only happens on startup + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + return ( + + {buttonGroups.map(group => ( + + + {group.name} + + + + {group.items.map(item => ( + setCurrent(item)} + selected={item.id === currentFilter} + className={classes.menuItem} + > + {item.icon && ( + + + + )} + + + {item.label} + + + + {getFilterCount(item.id) ?? '-'} + + + ))} + + + + ))} + + ); +}; + +function useFilter( + buttonGroups: ButtonGroup[], + initiallySelected: string, +): { + currentFilter: string; + setCurrentFilter: (filterId: string) => void; + getFilterCount: (filterId: string) => number | undefined; +} { + const [currentFilter, setCurrentFilter] = useState(initiallySelected); + + const filterGroup = useMemo( + () => ({ + filters: Object.fromEntries( + buttonGroups.flatMap(g => g.items).map(i => [i.id, i.filterFn]), + ), + }), + [buttonGroups], + ); + + const { setSelectedFilters, state } = useEntityFilterGroup( + 'primary-sidebar', + filterGroup, + [initiallySelected], + ); + + const setCurrent = useCallback( + (filterId: string) => { + setCurrentFilter(filterId); + setSelectedFilters([filterId]); + }, + [setCurrentFilter, setSelectedFilters], + ); + + const getFilterCount = useCallback( + (filterId: string) => { + if (state.type !== 'ready') { + return undefined; + } + return state.state.filters[filterId].matchCount; + }, + [state], + ); + + return { + currentFilter, + setCurrentFilter: setCurrent, + getFilterCount, + }; +} diff --git a/plugins/scaffolder/src/components/JobStatusModal/index.ts b/plugins/scaffolder/src/components/ScaffolderFilter/index.ts similarity index 91% rename from plugins/scaffolder/src/components/JobStatusModal/index.ts rename to plugins/scaffolder/src/components/ScaffolderFilter/index.ts index 5598999fe3..f53e4be89f 100644 --- a/plugins/scaffolder/src/components/JobStatusModal/index.ts +++ b/plugins/scaffolder/src/components/ScaffolderFilter/index.ts @@ -13,4 +13,5 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { JobStatusModal } from './JobStatusModal'; + +export { ScaffolderFilter } from './ScaffolderFilter'; diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index 75ee2d07df..3f6ceb64c7 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -14,11 +14,13 @@ * limitations under the License. */ -import { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import React, { useEffect, useMemo, useState } from 'react'; +import { Link as RouterLink } from 'react-router-dom'; +import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model'; import { + configApiRef, Content, ContentHeader, - errorApiRef, Header, Lifecycle, Page, @@ -27,12 +29,24 @@ import { useApi, WarningPanel, } from '@backstage/core'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { Button, Grid, Link, Typography } from '@material-ui/core'; -import React, { useEffect } from 'react'; -import { Link as RouterLink } from 'react-router-dom'; -import useStaleWhileRevalidate from 'swr'; +import { useStarredEntities } from '@backstage/plugin-catalog-react'; +import { Button, Grid, Link, makeStyles, Typography } from '@material-ui/core'; +import StarIcon from '@material-ui/icons/Star'; +import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter'; import { TemplateCard, TemplateCardProps } from '../TemplateCard'; +import { ResultsFilter } from '../ResultsFilter/ResultsFilter'; +import { ScaffolderFilter } from '../ScaffolderFilter'; +import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter'; +import SearchToolbar from '../SearchToolbar/SearchToolbar'; + +const useStyles = makeStyles(theme => ({ + contentWrapper: { + display: 'grid', + gridTemplateAreas: "'filters' 'grid'", + gridTemplateColumns: '250px 1fr', + gridColumnGap: theme.spacing(2), + }, +})); const getTemplateCardProps = ( template: TemplateEntityV1alpha1, @@ -47,24 +61,62 @@ const getTemplateCardProps = ( }; }; -export const ScaffolderPage = () => { - const catalogApi = useApi(catalogApiRef); - const errorApi = useApi(errorApiRef); - - const { data: templates, isValidating, error } = useStaleWhileRevalidate( - 'templates/all', - async () => { - const response = await catalogApi.getEntities({ - filter: { kind: 'Template' }, - }); - return response.items as TemplateEntityV1alpha1[]; - }, +export const ScaffolderPageContents = () => { + const styles = useStyles(); + const { + loading, + error, + filteredEntities, + availableCategories, + } = useFilteredEntities(); + const configApi = useApi(configApiRef); + const orgName = configApi.getOptionalString('organization.name') ?? 'Company'; + const { isStarredEntity } = useStarredEntities(); + const filterGroups = useMemo( + () => [ + { + name: orgName, + items: [ + { + id: 'all', + label: 'All', + filterFn: () => true, + }, + ], + }, + { + name: 'Personal', + items: [ + { + id: 'starred', + label: 'Starred', + icon: StarIcon, + filterFn: isStarredEntity, + }, + ], + }, + ], + [isStarredEntity, orgName], + ); + const [search, setSearch] = useState(''); + const [matchingEntities, setMatchingEntities] = useState( + [] as TemplateEntityV1alpha1[], ); + const matchesQuery = (metadata: EntityMeta, query: string) => + `${metadata.title}`.toUpperCase().includes(query) || + metadata.tags?.join('').toUpperCase().indexOf(query) !== -1; + useEffect(() => { - if (!error) return; - errorApi.post(error); - }, [error, errorApi]); + if (search.length === 0) { + return setMatchingEntities(filteredEntities); + } + return setMatchingEntities( + filteredEntities.filter(template => + matchesQuery(template.metadata, search.toUpperCase()), + ), + ); + }, [search, filteredEntities]); return ( @@ -93,33 +145,64 @@ export const ScaffolderPage = () => { documentation, ...). - {!templates && isValidating && } - {templates && !templates.length && ( - - Shoot! Looks like you don't have any templates. Check out the - documentation{' '} - - here! - - - )} - {error && ( - - Oops! Something went wrong loading the templates: {error.message} - - )} - - {templates && - templates?.length > 0 && - templates.map(template => { - return ( - - - - ); - })} - + +
+
+ + + +
+
+ {loading && } + + {error && ( + + {error.message} + + )} + + {!error && + !loading && + matchingEntities && + !matchingEntities.length && ( + + No templates found that match your filter. Learn more about{' '} + + adding templates + + . + + )} + + + {matchingEntities && + matchingEntities?.length > 0 && + matchingEntities.map(template => { + return ( + + + + ); + })} + +
+
); }; + +export const ScaffolderPage = () => ( + + + +); diff --git a/plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.test.tsx b/plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.test.tsx new file mode 100644 index 0000000000..7b9a64b935 --- /dev/null +++ b/plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.test.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 React from 'react'; +import { fireEvent, render } from '@testing-library/react'; +import SearchToolbar from './SearchToolbar'; + +describe('SearchToolbar', () => { + it('should display search value and execute set callback', async () => { + const setSearchSpy = jest.fn(); + const { getByDisplayValue } = render( + , + ); + + const searchInput = getByDisplayValue('hello'); + expect(searchInput).toBeInTheDocument(); + fireEvent.change(searchInput, { target: { value: 'world' } }); + expect(setSearchSpy).toHaveBeenCalled(); + }); +}); diff --git a/plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.tsx b/plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.tsx new file mode 100644 index 0000000000..4a50fcb257 --- /dev/null +++ b/plugins/scaffolder/src/components/SearchToolbar/SearchToolbar.tsx @@ -0,0 +1,74 @@ +/* + * 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 { + FormControl, + InputAdornment, + makeStyles, + Toolbar, + Input, + IconButton, +} from '@material-ui/core'; +import Search from '@material-ui/icons/Search'; +import Clear from '@material-ui/icons/Clear'; + +interface Props { + search: string; + setSearch: Function; +} + +const useStyles = makeStyles(_theme => ({ + searchToolbar: { + paddingLeft: 0, + paddingRight: 0, + }, +})); + +const SearchToolbar = ({ search, setSearch }: Props) => { + const styles = useStyles(); + return ( + + + setSearch(event.target.value)} + value={search} + startAdornment={ + + + + } + endAdornment={ + + setSearch('')} + edge="end" + disabled={search.length === 0} + > + + + + } + /> + + + ); +}; + +export default SearchToolbar; diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx new file mode 100644 index 0000000000..96a857dbb5 --- /dev/null +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -0,0 +1,338 @@ +/* + * 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 { Page, Header, Lifecycle, Content, ErrorPage } from '@backstage/core'; +import React, { useState, useEffect, memo, useMemo } from 'react'; +import { makeStyles, Theme, createStyles } from '@material-ui/core/styles'; +import Stepper from '@material-ui/core/Stepper'; +import Step from '@material-ui/core/Step'; +import StepLabel from '@material-ui/core/StepLabel'; +import Grid from '@material-ui/core/Grid'; +import Typography from '@material-ui/core/Typography'; +import { generatePath, useParams } from 'react-router'; +import { useTaskEventStream } from '../hooks/useEventStream'; +import LazyLog from 'react-lazylog/build/LazyLog'; +import { Link } from 'react-router-dom'; +import { + Box, + Button, + CircularProgress, + Paper, + StepButton, + StepIconProps, +} from '@material-ui/core'; +import { Status } from '../../types'; +import { DateTime, Interval } from 'luxon'; +import { useInterval } from 'react-use'; +import Check from '@material-ui/icons/Check'; +import Cancel from '@material-ui/icons/Cancel'; +import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; +import { entityRoute } from '@backstage/plugin-catalog-react'; +import { parseEntityName } from '@backstage/catalog-model'; +import classNames from 'classnames'; +import { BackstageTheme } from '@backstage/theme'; + +// typings are wrong for this library, so fallback to not parsing types. +const humanizeDuration = require('humanize-duration'); + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + width: '100%', + }, + button: { + marginTop: theme.spacing(1), + marginRight: theme.spacing(1), + }, + actionsContainer: { + marginBottom: theme.spacing(2), + }, + resetContainer: { + padding: theme.spacing(3), + }, + labelWrapper: { + display: 'flex', + flex: 1, + flexDirection: 'row', + justifyContent: 'space-between', + }, + stepWrapper: { + width: '100%', + }, + }), +); + +type TaskStep = { + id: string; + name: string; + status: Status; + startedAt?: string; + endedAt?: string; +}; + +const StepTimeTicker = ({ step }: { step: TaskStep }) => { + const [time, setTime] = useState(''); + + useInterval(() => { + if (!step.startedAt) { + setTime(''); + return; + } + + const end = step.endedAt + ? DateTime.fromISO(step.endedAt) + : DateTime.local(); + + const startedAt = DateTime.fromISO(step.startedAt); + const formatted = Interval.fromDateTimes(startedAt, end) + .toDuration() + .valueOf(); + + setTime(humanizeDuration(formatted, { round: true })); + }, 1000); + + return {time}; +}; + +const useStepIconStyles = makeStyles((theme: BackstageTheme) => + createStyles({ + root: { + color: theme.palette.text.disabled, + display: 'flex', + height: 22, + alignItems: 'center', + }, + completed: { + color: theme.palette.status.ok, + }, + error: { + color: theme.palette.status.error, + }, + }), +); + +function TaskStepIconComponent(props: StepIconProps) { + const classes = useStepIconStyles(); + const { active, completed, error } = props; + + const getMiddle = () => { + if (active) { + return ; + } + if (completed) { + return ; + } + if (error) { + return ; + } + return ; + }; + + return ( +
+ {getMiddle()} +
+ ); +} + +export const TaskStatusStepper = memo( + ({ + steps, + currentStepId, + onUserStepChange, + }: { + steps: TaskStep[]; + currentStepId: string | undefined; + onUserStepChange: (id: string) => void; + }) => { + const classes = useStyles(); + + return ( +
+ s.id === currentStepId)} + orientation="vertical" + nonLinear + > + {steps.map((step, index) => { + const isCompleted = step.status === 'completed'; + const isFailed = step.status === 'failed'; + const isActive = step.status === 'processing'; + return ( + + onUserStepChange(step.id)}> + +
+ {step.name} + +
+
+
+
+ ); + })} +
+
+ ); + }, +); + +const TaskLogger = memo(({ log }: { log: string }) => { + return ( +
+ +
+ ); +}); + +export const TaskPage = () => { + const [userSelectedStepId, setUserSelectedStepId] = useState< + string | undefined + >(undefined); + const [lastActiveStepId, setLastActiveStepId] = useState( + undefined, + ); + const { taskId } = useParams(); + const taskStream = useTaskEventStream(taskId); + const completed = taskStream.completed; + const steps = useMemo( + () => + taskStream.task?.spec.steps.map(step => ({ + ...step, + ...taskStream?.steps?.[step.id], + })) ?? [], + [taskStream], + ); + + useEffect(() => { + const mostRecentFailedOrActiveStep = steps.find(step => + ['failed', 'processing'].includes(step.status), + ); + if (completed && !mostRecentFailedOrActiveStep) { + setLastActiveStepId(steps[steps.length - 1]?.id); + return; + } + + setLastActiveStepId(mostRecentFailedOrActiveStep?.id); + }, [steps, completed]); + + const currentStepId = userSelectedStepId ?? lastActiveStepId; + + const logAsString = useMemo(() => { + if (!currentStepId) { + return 'Loading...'; + } + const log = taskStream.stepLogs[currentStepId]; + + if (!log?.length) { + return 'Waiting for logs...'; + } + return log.join('\n'); + }, [taskStream.stepLogs, currentStepId]); + + const taskNotFound = + taskStream.completed === true && + taskStream.loading === false && + !taskStream.task; + + const entityRef = taskStream.output?.entityRef; + const remoteUrl = taskStream.output?.remoteUrl; + return ( + +
+ Task Activity + + } + subtitle={`Activity for task: ${taskId}`} + /> + + {taskNotFound ? ( + + ) : ( +
+ + + + + {(entityRef || remoteUrl) && ( + + {entityRef && ( + + )} + {remoteUrl && ( + + )} + + )} + + + + + + +
+ )} +
+ + ); +}; diff --git a/plugins/scaffolder/src/components/TaskPage/index.ts b/plugins/scaffolder/src/components/TaskPage/index.ts new file mode 100644 index 0000000000..3695c2792e --- /dev/null +++ b/plugins/scaffolder/src/components/TaskPage/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 { TaskPage } from './TaskPage'; diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx index ef17335b26..c38f4e09a5 100644 --- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx +++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { Button } from '@backstage/core'; +import { Button, useRouteRef } from '@backstage/core'; import { BackstageTheme, pageTheme } from '@backstage/theme'; import { Card, @@ -25,8 +25,8 @@ import { useTheme, } from '@material-ui/core'; import React from 'react'; -import { generatePath } from 'react-router-dom'; -import { templateRoute } from '../../routes'; +import { generatePath } from 'react-router'; +import { rootRouteRef } from '../../routes'; const useStyles = makeStyles(theme => ({ header: { @@ -59,11 +59,14 @@ export const TemplateCard = ({ name, }: TemplateCardProps) => { const backstageTheme = useTheme(); + const rootLink = useRouteRef(rootRouteRef); const themeId = pageTheme[type] ? type : 'other'; const theme = backstageTheme.getPageTheme({ themeId }); const classes = useStyles({ backgroundImage: theme.backgroundImage }); - const href = generatePath(templateRoute.path, { templateName: name }); + const href = generatePath(`${rootLink()}/templates/:templateName`, { + templateName: name, + }); return ( diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx index 09df6dba1a..f0c77f2f03 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx @@ -22,7 +22,7 @@ import React from 'react'; import { act } from 'react-dom/test-utils'; import { MemoryRouter, Route } from 'react-router'; import { ScaffolderApi, scaffolderApiRef } from '../../api'; -import { rootRoute } from '../../routes'; +import { rootRouteRef } from '../../routes'; import { TemplatePage } from './TemplatePage'; const templateMock = { @@ -97,11 +97,15 @@ describe('TemplatePage', () => { , + { + mountedRoutes: { + '/create': rootRouteRef, + }, + }, ); expect(rendered.queryByText('Create a New Component')).toBeInTheDocument(); expect(rendered.queryByText('React SSR Template')).toBeInTheDocument(); - // await act(async () => await mutate('templates/test')); }); it('renders spinner while loading', async () => { @@ -114,13 +118,18 @@ describe('TemplatePage', () => { , + { + mountedRoutes: { + '/create': rootRouteRef, + }, + }, ); expect(rendered.queryByText('Create a New Component')).toBeInTheDocument(); expect(rendered.queryByTestId('loading-progress')).toBeInTheDocument(); - // Need to cleanup the promise or will timeout - act(() => { - resolve!({ items: [] }); + + await act(async () => { + resolve!({ items: [templateMock] }); }); }); @@ -134,7 +143,7 @@ describe('TemplatePage', () => { - This is root} /> + This is root} /> , diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index d779f8b42c..0fc1a3cf5d 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -22,23 +22,18 @@ import { Lifecycle, Page, useApi, + useRouteRef, } from '@backstage/core'; -import { - catalogApiRef, - entityRoute, - entityRouteParams, -} from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; import { LinearProgress } from '@material-ui/core'; import { IChangeEvent } from '@rjsf/core'; import parseGitUrl from 'git-url-parse'; import React, { useCallback, useState } from 'react'; -import { generatePath, Navigate } from 'react-router'; +import { generatePath, useNavigate, Navigate } from 'react-router'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { scaffolderApiRef } from '../../api'; -import { rootRoute } from '../../routes'; -import { useJobPolling } from '../hooks/useJobPolling'; -import { JobStatusModal } from '../JobStatusModal'; +import { rootRouteRef } from '../../routes'; import { MultistepJsonForm } from '../MultistepJsonForm'; const useTemplate = ( @@ -50,7 +45,7 @@ const useTemplate = ( filter: { kind: 'Template', 'metadata.name': templateName }, }); return response.items as TemplateEntityV1alpha1[]; - }); + }, [catalogApi, templateName]); return { template: value?.[0], loading, error }; }; @@ -76,57 +71,28 @@ const OWNER_REPO_SCHEMA = { }, }, }; + export const TemplatePage = () => { const errorApi = useApi(errorApiRef); const catalogApi = useApi(catalogApiRef); const scaffolderApi = useApi(scaffolderApiRef); const { templateName } = useParams(); - const [catalogLink, setCatalogLink] = useState(); + const navigate = useNavigate(); + const rootLink = useRouteRef(rootRouteRef); const { template, loading } = useTemplate(templateName, catalogApi); const [formState, setFormState] = useState({}); - const [modalOpen, setModalOpen] = useState(false); const handleFormReset = () => setFormState({}); + const handleChange = useCallback( (e: IChangeEvent) => setFormState({ ...formState, ...e.formData }), [setFormState, formState], ); - const [jobId, setJobId] = useState(null); - const job = useJobPolling(jobId, async jobItem => { - if (!jobItem.metadata.catalogInfoUrl) { - errorApi.post( - new Error(`No catalogInfoUrl returned from the scaffolder`), - ); - return; - } - - try { - const { - entities: [createdEntity], - } = await catalogApi.addLocation({ - target: jobItem.metadata.catalogInfoUrl, - }); - - const resolvedPath = generatePath( - `/catalog/${entityRoute.path}`, - entityRouteParams(createdEntity), - ); - - setCatalogLink(resolvedPath); - } catch (ex) { - errorApi.post( - new Error( - `Something went wrong trying to add the new 'catalog-info.yaml' to the catalog`, - ), - ); - } - }); - const handleCreate = async () => { try { const id = await scaffolderApi.scaffold(templateName, formState); - setJobId(id); - setModalOpen(true); + + navigate(generatePath(`${rootLink()}/tasks/:taskId`, { taskId: id })); } catch (e) { errorApi.post(e); } @@ -134,7 +100,7 @@ export const TemplatePage = () => { if (!loading && !template) { errorApi.post(new Error('Template was not found.')); - return ; + return ; } if (template && !template?.spec?.schema) { @@ -143,7 +109,7 @@ export const TemplatePage = () => { 'Template schema is corrupted, please check the template.yaml file.', ), ); - return ; + return ; } return ( @@ -159,12 +125,6 @@ export const TemplatePage = () => { /> {loading && } - setModalOpen(false)} - /> {template && ( { + current[next.id] = { status: 'open', id: next.id }; + return current; + }, {} as { [stepId in string]: Step }); + draft.stepLogs = action.data.spec.steps.reduce((current, next) => { + current[next.id] = []; + return current; + }, {} as { [stepId in string]: string[] }); + draft.loading = false; + draft.error = undefined; + draft.completed = false; + draft.task = action.data; + return; + } + + case 'LOGS': { + const entries = action.data; + const logLines = []; + + for (const entry of entries) { + const logLine = `${entry.createdAt} ${entry.body.message}`; + logLines.push(logLine); + + if (!entry.body.stepId || !draft.steps?.[entry.body.stepId]) { + continue; + } + + const currentStepLog = draft.stepLogs?.[entry.body.stepId]; + const currentStep = draft.steps?.[entry.body.stepId]; + + if (entry.body.status && entry.body.status !== currentStep.status) { + currentStep.status = entry.body.status; + + if (currentStep.status === 'processing') { + currentStep.startedAt = entry.createdAt; + } + + if ( + ['cancelled', 'failed', 'completed'].includes(currentStep.status) + ) { + currentStep.endedAt = entry.createdAt; + } + } + + currentStepLog?.push(logLine); + } + + return; + } + + case 'COMPLETED': { + draft.completed = true; + draft.output = action.data.body.output; + return; + } + + case 'ERROR': { + draft.error = action.data; + draft.loading = false; + draft.completed = true; + return; + } + + default: + return; + } +} + +export const useTaskEventStream = (taskId: string): TaskStream => { + const scaffolderApi = useApi(scaffolderApiRef); + const [state, dispatch] = useImmerReducer(reducer, { + loading: true, + completed: false, + stepLogs: {} as { [stepId in string]: string[] }, + steps: {} as { [stepId in string]: Step }, + }); + + useEffect(() => { + let didCancel = false; + let subscription: Subscription | undefined; + let logPusher: NodeJS.Timeout | undefined; + + scaffolderApi.getTask(taskId).then( + task => { + if (didCancel) { + return; + } + dispatch({ type: 'INIT', data: task }); + + // TODO(blam): Use a normal fetch to fetch the current log for the event stream + // and use that for an INIT_EVENTs dispatch event, and then + // use the last event ID to subscribe using after option to + // stream logs. Without this, if you have a lot of logs, it can look like the + // task is being rebuilt on load as it progresses through the steps at a slower + // rate whilst it builds the status from the event logs + const observable = scaffolderApi.streamLogs({ taskId }); + + const collectedLogEvents = new Array(); + + function emitLogs() { + if (collectedLogEvents.length) { + const logs = collectedLogEvents.splice( + 0, + collectedLogEvents.length, + ); + dispatch({ type: 'LOGS', data: logs }); + } + } + + logPusher = setInterval(emitLogs, 500); + + subscription = observable.subscribe({ + next: event => { + switch (event.type) { + case 'log': + return collectedLogEvents.push(event); + case 'completion': + emitLogs(); + dispatch({ type: 'COMPLETED', data: event }); + return undefined; + default: + throw new Error( + `Unhandled event type ${event.type} in observer`, + ); + } + }, + error: error => { + emitLogs(); + dispatch({ type: 'ERROR', data: error }); + }, + }); + }, + error => { + if (!didCancel) { + dispatch({ type: 'ERROR', data: error }); + } + }, + ); + + return () => { + didCancel = true; + if (subscription) { + subscription.unsubscribe(); + } + if (logPusher) { + clearInterval(logPusher); + } + }; + }, [scaffolderApi, dispatch, taskId]); + + return state; +}; diff --git a/plugins/scaffolder/src/components/hooks/useJobPolling.ts b/plugins/scaffolder/src/components/hooks/useJobPolling.ts deleted file mode 100644 index 2cd4fbdc9f..0000000000 --- a/plugins/scaffolder/src/components/hooks/useJobPolling.ts +++ /dev/null @@ -1,62 +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 { useEffect, useState } from 'react'; -import { Job } from '../../types'; -import { useApi } from '@backstage/core'; -import { scaffolderApiRef } from '../../api'; -import { useInterval } from 'react-use'; - -const DEFAULT_POLLING_INTERVAL = 1000; - -export const useJobPolling = ( - jobId: string | null, - onFinish?: (j: Job) => void, - pollingInterval = DEFAULT_POLLING_INTERVAL, -) => { - const scaffolderApi = useApi(scaffolderApiRef); - const [currentJob, setCurrentJob] = useState(null); - - useEffect(() => { - const resetCurrentJob = async () => { - if (jobId) { - const job = await scaffolderApi.getJob(jobId); - setCurrentJob(job); - } - }; - - resetCurrentJob(); - }, [jobId, scaffolderApi]); - - const shouldBeRunningInterval = - jobId && - currentJob?.status !== 'COMPLETED' && - currentJob?.status !== 'FAILED'; - - useInterval( - async () => { - if (jobId) { - const job = await scaffolderApi.getJob(jobId); - if (job?.status === 'COMPLETED' || job?.status === 'FAILED') { - onFinish?.(job); - } - setCurrentJob(job); - } - }, - shouldBeRunningInterval ? pollingInterval : null, - ); - - return currentJob; -}; diff --git a/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx b/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx new file mode 100644 index 0000000000..9e11525347 --- /dev/null +++ b/plugins/scaffolder/src/filter/EntityFilterGroupsProvider.tsx @@ -0,0 +1,263 @@ +/* + * 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 { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { useApi } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; +import { useAsyncFn } from 'react-use'; +import { filterGroupsContext, FilterGroupsContext } from './context'; +import { + EntityFilterFn, + FilterGroup, + FilterGroupState, + FilterGroupStates, +} from './types'; + +/** + * Implementation of the shared filter groups state. + */ +export const EntityFilterGroupsProvider = ({ + children, +}: { + children?: React.ReactNode; +}) => { + const state = useProvideEntityFilters(); + return ( + + {children} + + ); +}; + +// The hook that implements the actual context building +function useProvideEntityFilters(): FilterGroupsContext { + const catalogApi = useApi(catalogApiRef); + const [{ value: entities, error }, doReload] = useAsyncFn(async () => { + const response = await catalogApi.getEntities({ + filter: { kind: 'Template' }, + }); + return response.items as TemplateEntityV1alpha1[]; + }); + + const filterGroups = useRef<{ + [filterGroupId: string]: FilterGroup; + }>({}); + const selectedFilterKeys = useRef<{ + [filterGroupId: string]: Set; + }>({}); + const selectedCategories = useRef([]); + const [filterGroupStates, setFilterGroupStates] = useState<{ + [filterGroupId: string]: FilterGroupStates; + }>({}); + const [filteredEntities, setFilteredEntities] = useState< + TemplateEntityV1alpha1[] + >([]); + const [availableCategories, setAvailableCategories] = useState([]); + const [isCatalogEmpty, setCatalogEmpty] = useState(false); + + useEffect(() => { + doReload(); + }, [doReload]); + + const rebuild = useCallback(() => { + setFilterGroupStates( + buildStates( + filterGroups.current, + selectedFilterKeys.current, + selectedCategories.current, + entities, + error, + ), + ); + setFilteredEntities( + buildMatchingEntities( + filterGroups.current, + selectedFilterKeys.current, + selectedCategories.current, + entities, + ), + ); + setAvailableCategories(collectCategories(entities)); + setCatalogEmpty(entities !== undefined && entities.length === 0); + }, [entities, error]); + + const register = useCallback( + ( + filterGroupId: string, + filterGroup: FilterGroup, + initialSelectedFilterIds?: string[], + ) => { + filterGroups.current[filterGroupId] = filterGroup; + selectedFilterKeys.current[filterGroupId] = new Set( + initialSelectedFilterIds ?? [], + ); + rebuild(); + }, + [rebuild], + ); + + const unregister = useCallback( + (filterGroupId: string) => { + delete filterGroups.current[filterGroupId]; + delete selectedFilterKeys.current[filterGroupId]; + rebuild(); + }, + [rebuild], + ); + + const setGroupSelectedFilters = useCallback( + (filterGroupId: string, filters: string[]) => { + selectedFilterKeys.current[filterGroupId] = new Set(filters); + rebuild(); + }, + [rebuild], + ); + + const setSelectedCategories = useCallback( + (categories: string[]) => { + selectedCategories.current = categories; + rebuild(); + }, + [rebuild], + ); + + const reload = useCallback(async () => { + await doReload(); + }, [doReload]); + + return { + register, + unregister, + setGroupSelectedFilters, + setSelectedCategories, + reload, + selectedCategories: selectedCategories.current, + loading: !error && !entities, + error, + filterGroupStates, + filteredEntities, + availableCategories, + isCatalogEmpty, + }; +} + +// Given all filter groups and what filters are actually selected, along with +// the loading state for entities, generate the state of each individual filter +function buildStates( + filterGroups: { [filterGroupId: string]: FilterGroup }, + selectedFilterKeys: { [filterGroupId: string]: Set }, + selectedCategories: string[], + entities?: TemplateEntityV1alpha1[], + error?: Error, +): { [filterGroupId: string]: FilterGroupStates } { + // On error - all entries are an error state + if (error) { + return Object.fromEntries( + Object.keys(filterGroups).map(filterGroupId => [ + filterGroupId, + { type: 'error', error }, + ]), + ); + } + + // On startup - all entries are a loading state + if (!entities) { + return Object.fromEntries( + Object.keys(filterGroups).map(filterGroupId => [ + filterGroupId, + { type: 'loading' }, + ]), + ); + } + + const result: { [filterGroupId: string]: FilterGroupStates } = {}; + for (const [filterGroupId, filterGroup] of Object.entries(filterGroups)) { + const otherMatchingEntities = buildMatchingEntities( + filterGroups, + selectedFilterKeys, + selectedCategories, + entities, + filterGroupId, + ); + const groupState: FilterGroupState = { filters: {} }; + for (const [filterId, filterFn] of Object.entries(filterGroup.filters)) { + const isSelected = !!selectedFilterKeys[filterGroupId]?.has(filterId); + const matchCount = otherMatchingEntities.filter(entity => + filterFn(entity), + ).length; + groupState.filters[filterId] = { isSelected, matchCount }; + } + result[filterGroupId] = { type: 'ready', state: groupState }; + } + + return result; +} + +// Given all entites, find all possible categories and provide them in a sorted list. +function collectCategories(entities?: TemplateEntityV1alpha1[]): string[] { + const categories = new Set(); + (entities || []).forEach(e => { + if (e.spec?.type) { + categories.add(e.spec.type as string); + } + }); + return Array.from(categories).sort(); +} + +// Given all filter groups and what filters are actually selected, extract all +// entities that match all those filter groups. +function buildMatchingEntities( + filterGroups: { [filterGroupId: string]: FilterGroup }, + selectedFilterKeys: { [filterGroupId: string]: Set }, + selectedCategories: string[], + entities?: TemplateEntityV1alpha1[], + excludeFilterGroupId?: string, +): TemplateEntityV1alpha1[] { + // Build one filter fn per filter group + const allFilters: EntityFilterFn[] = []; + for (const [filterGroupId, filterGroup] of Object.entries(filterGroups)) { + if (excludeFilterGroupId === filterGroupId) { + continue; + } + + // Pick out all of the filter functions in the group that are actually + // selected + const groupFilters: EntityFilterFn[] = []; + for (const [filterId, filterFn] of Object.entries(filterGroup.filters)) { + if (!!selectedFilterKeys[filterGroupId]?.has(filterId)) { + groupFilters.push(filterFn); + } + } + + // Need to match any of the selected filters in the group - if there is + // any at all + if (groupFilters.length) { + allFilters.push(entity => groupFilters.some(fn => fn(entity))); + } + } + + // Filter by categories, if at least one category is selected. + if (selectedCategories.length > 0) { + allFilters.push(entity => + selectedCategories.some(c => entity.spec?.type === c), + ); + } + + // All filter groups that had any checked filters need to match. Note that + // every() always returns true for an empty array. + return entities?.filter(entity => allFilters.every(fn => fn(entity))) ?? []; +} diff --git a/plugins/scaffolder/src/filter/context.ts b/plugins/scaffolder/src/filter/context.ts new file mode 100644 index 0000000000..a7819be752 --- /dev/null +++ b/plugins/scaffolder/src/filter/context.ts @@ -0,0 +1,45 @@ +/* + * 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 { TemplateEntityV1alpha1 } from '@backstage/catalog-model'; +import { createContext } from 'react'; +import { FilterGroup, FilterGroupStates } from './types'; + +export type FilterGroupsContext = { + register: ( + filterGroupId: string, + filterGroup: FilterGroup, + initialSelectedFilterIds?: string[], + ) => void; + unregister: (filterGroupId: string) => void; + setGroupSelectedFilters: (filterGroupId: string, filterIds: string[]) => void; + setSelectedCategories: (categories: string[]) => void; + reload: () => Promise; + selectedCategories: string[]; + loading: boolean; + error?: Error; + filterGroupStates: { [filterGroupId: string]: FilterGroupStates }; + filteredEntities: TemplateEntityV1alpha1[]; + availableCategories: string[]; + isCatalogEmpty: boolean; +}; + +/** + * The context that maintains shared state for all visible filter groups. + */ +export const filterGroupsContext = createContext< + FilterGroupsContext | undefined +>(undefined); diff --git a/plugins/scaffolder/src/filter/index.ts b/plugins/scaffolder/src/filter/index.ts new file mode 100644 index 0000000000..da73147ef9 --- /dev/null +++ b/plugins/scaffolder/src/filter/index.ts @@ -0,0 +1,28 @@ +/* + * 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 { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider'; +export type { + EntityFilterFn, + FilterGroup, + FilterGroupState, + FilterGroupStates, + FilterGroupStatesError, + FilterGroupStatesLoading, + FilterGroupStatesReady, +} from './types'; +export { useEntityFilterGroup } from './useEntityFilterGroup'; +export { useFilteredEntities } from './useFilteredEntities'; diff --git a/plugins/scaffolder/src/filter/types.ts b/plugins/scaffolder/src/filter/types.ts new file mode 100644 index 0000000000..ed08b131bf --- /dev/null +++ b/plugins/scaffolder/src/filter/types.ts @@ -0,0 +1,53 @@ +/* + * 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 { Entity } from '@backstage/catalog-model'; + +export type EntityFilterFn = (entity: Entity) => boolean; + +export type FilterGroup = { + filters: { + [filterId: string]: EntityFilterFn; + }; +}; + +export type FilterGroupState = { + filters: { + [filterId: string]: { + isSelected: boolean; + matchCount: number; + }; + }; +}; + +export type FilterGroupStatesReady = { + type: 'ready'; + state: FilterGroupState; +}; + +export type FilterGroupStatesError = { + type: 'error'; + error: Error; +}; + +export type FilterGroupStatesLoading = { + type: 'loading'; +}; + +export type FilterGroupStates = + | FilterGroupStatesReady + | FilterGroupStatesError + | FilterGroupStatesLoading; diff --git a/plugins/scaffolder/src/filter/useEntityFilterGroup.test.tsx b/plugins/scaffolder/src/filter/useEntityFilterGroup.test.tsx new file mode 100644 index 0000000000..685fc751d2 --- /dev/null +++ b/plugins/scaffolder/src/filter/useEntityFilterGroup.test.tsx @@ -0,0 +1,120 @@ +/* + * 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, storageApiRef } from '@backstage/core'; +import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { MockStorageApi } from '@backstage/test-utils'; +import { act, renderHook } from '@testing-library/react-hooks'; +import React from 'react'; +import { EntityFilterGroupsProvider } from './EntityFilterGroupsProvider'; +import { FilterGroup, FilterGroupStatesReady } from './types'; +import { useEntityFilterGroup } from './useEntityFilterGroup'; + +describe('useEntityFilterGroup', () => { + let catalogApi: jest.Mocked; + let wrapper: ({ children }: { children?: React.ReactNode }) => JSX.Element; + + beforeEach(() => { + catalogApi = { + /* eslint-disable-next-line @typescript-eslint/no-unused-vars */ + addLocation: jest.fn(_a => new Promise(() => {})), + getEntities: jest.fn(), + getLocationByEntity: jest.fn(), + getLocationById: jest.fn(), + removeEntityByUid: jest.fn(), + getEntityByName: jest.fn(), + }; + const apis = ApiRegistry.with(catalogApiRef, catalogApi).with( + storageApiRef, + MockStorageApi.create(), + ); + wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + }); + + it('works for an empty set of filters', async () => { + catalogApi.getEntities.mockResolvedValue({ items: [] }); + const group: FilterGroup = { filters: {} }; + const { result, waitFor } = renderHook( + () => useEntityFilterGroup('g1', group), + { wrapper }, + ); + + await waitFor(() => expect(result.current.state.type).toBe('ready')); + }); + + it('works for a single group', async () => { + catalogApi.getEntities.mockResolvedValue({ + items: [ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'n' }, + }, + ], + }); + const group: FilterGroup = { + filters: { + f1: e => e.metadata.name === 'n', + f2: e => e.metadata.name !== 'n', + }, + }; + const { result, waitFor } = renderHook( + () => useEntityFilterGroup('g1', group), + { wrapper }, + ); + + await waitFor(() => expect(result.current.state.type).toEqual('ready')); + let state = result.current.state as FilterGroupStatesReady; + expect(state.state.filters.f1).toEqual({ + isSelected: false, + matchCount: 1, + }); + expect(state.state.filters.f2).toEqual({ + isSelected: false, + matchCount: 0, + }); + + act(() => result.current.setSelectedFilters(['f1'])); + + await waitFor(() => expect(result.current.state.type).toEqual('ready')); + state = result.current.state as FilterGroupStatesReady; + expect(state.state.filters.f1).toEqual({ + isSelected: true, + matchCount: 1, + }); + expect(state.state.filters.f2).toEqual({ + isSelected: false, + matchCount: 0, + }); + + act(() => result.current.setSelectedFilters(['f2'])); + + await waitFor(() => expect(result.current.state.type).toEqual('ready')); + state = result.current.state as FilterGroupStatesReady; + expect(state.state.filters.f1).toEqual({ + isSelected: false, + matchCount: 1, + }); + expect(state.state.filters.f2).toEqual({ + isSelected: true, + matchCount: 0, + }); + }); +}); diff --git a/plugins/scaffolder/src/filter/useEntityFilterGroup.ts b/plugins/scaffolder/src/filter/useEntityFilterGroup.ts new file mode 100644 index 0000000000..242238e4f4 --- /dev/null +++ b/plugins/scaffolder/src/filter/useEntityFilterGroup.ts @@ -0,0 +1,69 @@ +/* + * 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 { useCallback, useContext, useEffect, useMemo } from 'react'; +import { filterGroupsContext } from './context'; +import { FilterGroup, FilterGroupStates } from './types'; + +export type EntityFilterGroupOutput = { + state: FilterGroupStates; + setSelectedFilters: (filterIds: string[]) => void; +}; + +/** + * Hook that exposes the relevant data and operations for a single filter + * group. + */ +export const useEntityFilterGroup = ( + filterGroupId: string, + filterGroup: FilterGroup, + initialSelectedFilters?: string[], +): EntityFilterGroupOutput => { + const context = useContext(filterGroupsContext); + if (!context) { + throw new Error(`Must be used inside an EntityFilterGroupsProvider`); + } + const { + register, + unregister, + setGroupSelectedFilters, + filterGroupStates, + } = context; + + // Intentionally consider initial set only at mount time + // eslint-disable-next-line react-hooks/exhaustive-deps + const initialMemo = useMemo(() => initialSelectedFilters?.slice(), []); + + // Register the group on mount, and unregister on unmount + useEffect(() => { + register(filterGroupId, filterGroup, initialMemo); + return () => unregister(filterGroupId); + }, [register, unregister, filterGroupId, filterGroup, initialMemo]); + + const setSelectedFilters = useCallback( + (filters: string[]) => { + setGroupSelectedFilters(filterGroupId, filters); + }, + [setGroupSelectedFilters, filterGroupId], + ); + + let state = filterGroupStates[filterGroupId]; + if (!state) { + state = { type: 'loading' }; + } + + return { state, setSelectedFilters }; +}; diff --git a/plugins/scaffolder/src/filter/useFilteredEntities.ts b/plugins/scaffolder/src/filter/useFilteredEntities.ts new file mode 100644 index 0000000000..d3eb553687 --- /dev/null +++ b/plugins/scaffolder/src/filter/useFilteredEntities.ts @@ -0,0 +1,37 @@ +/* + * 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 { useContext } from 'react'; +import { filterGroupsContext } from './context'; + +/** + * Hook that exposes the result of applying a set of filter groups. + */ +export function useFilteredEntities() { + const context = useContext(filterGroupsContext); + if (!context) { + throw new Error(`Must be used inside an EntityFilterGroupsProvider`); + } + + return { + loading: context.loading, + error: context.error, + filteredEntities: context.filteredEntities, + availableCategories: context.availableCategories, + isCatalogEmpty: context.isCatalogEmpty, + reload: context.reload, + }; +} diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 5f102e7853..e0b574e6c6 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -17,8 +17,7 @@ export { scaffolderPlugin, scaffolderPlugin as plugin, - TemplateIndexPage, - TemplatePage, + ScaffolderPage, } from './plugin'; -export { ScaffolderApi, scaffolderApiRef } from './api'; -export { rootRoute, templateRoute } from './routes'; +export type { ScaffolderApi } from './api'; +export { ScaffolderClient, scaffolderApiRef } from './api'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index 3e884d7683..a6ba1a9899 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -21,10 +21,8 @@ import { identityApiRef, createRoutableExtension, } from '@backstage/core'; -import { ScaffolderPage as ScaffolderPageComponent } from './components/ScaffolderPage'; -import { TemplatePage as TemplatePageComponent } from './components/TemplatePage'; -import { rootRoute, templateRoute } from './routes'; -import { scaffolderApiRef, ScaffolderApi } from './api'; +import { rootRouteRef } from './routes'; +import { scaffolderApiRef, ScaffolderClient } from './api'; export const scaffolderPlugin = createPlugin({ id: 'scaffolder', @@ -33,31 +31,17 @@ export const scaffolderPlugin = createPlugin({ api: scaffolderApiRef, deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef }, factory: ({ discoveryApi, identityApi }) => - new ScaffolderApi({ discoveryApi, identityApi }), + new ScaffolderClient({ discoveryApi, identityApi }), }), ], - register({ router }) { - router.addRoute(rootRoute, ScaffolderPageComponent); - router.addRoute(templateRoute, TemplatePageComponent); - }, routes: { - templateIndex: rootRoute, - template: templateRoute, + root: rootRouteRef, }, }); -export const TemplateIndexPage = scaffolderPlugin.provide( +export const ScaffolderPage = scaffolderPlugin.provide( createRoutableExtension({ - component: () => - import('./components/ScaffolderPage').then(m => m.ScaffolderPage), - mountPoint: rootRoute, - }), -); - -export const TemplatePage = scaffolderPlugin.provide( - createRoutableExtension({ - component: () => - import('./components/TemplatePage').then(m => m.TemplatePage), - mountPoint: templateRoute, + component: () => import('./components/Router').then(m => m.Router), + mountPoint: rootRouteRef, }), ); diff --git a/plugins/scaffolder/src/routes.ts b/plugins/scaffolder/src/routes.ts index 8efd1d3aaf..413e8f4194 100644 --- a/plugins/scaffolder/src/routes.ts +++ b/plugins/scaffolder/src/routes.ts @@ -15,12 +15,6 @@ */ import { createRouteRef } from '@backstage/core'; -export const rootRoute = createRouteRef({ - path: '/create', +export const rootRouteRef = createRouteRef({ title: 'Create new entity', }); - -export const templateRoute = createRouteRef({ - path: '/create/:templateName', - title: 'Entity creation', -}); diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 45672c603d..0499f4aca2 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -13,7 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { JsonValue } from '@backstage/config'; +export type Status = 'open' | 'processing' | 'failed' | 'completed'; export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; export type Job = { id: string; @@ -35,3 +37,20 @@ export type Stage = { startedAt: string; endedAt?: string; }; + +export type ScaffolderStep = { + id: string; + name: string; + action: string; + parameters?: { [name: string]: JsonValue }; +}; + +export type ScaffolderTask = { + id: string; + spec: { + steps: ScaffolderStep[]; + }; + status: 'failed' | 'completed' | 'processing' | 'open' | 'cancelled'; + lastHeartbeatAt: string; + createdAt: string; +}; diff --git a/plugins/search/CHANGELOG.md b/plugins/search/CHANGELOG.md index 38cbf202e1..a4715a9ce1 100644 --- a/plugins/search/CHANGELOG.md +++ b/plugins/search/CHANGELOG.md @@ -1,5 +1,40 @@ # @backstage/plugin-search +## 0.3.2 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.3.1 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.3.0 ### Minor Changes diff --git a/plugins/search/package.json b/plugins/search/package.json index 56f863a2a6..0f407bc22e 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-search", - "version": "0.3.0", + "version": "0.3.2", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -29,9 +29,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.1", - "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/core": "^0.6.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,9 +43,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/sentry/CHANGELOG.md b/plugins/sentry/CHANGELOG.md index 4c4d68c67b..2902faab77 100644 --- a/plugins/sentry/CHANGELOG.md +++ b/plugins/sentry/CHANGELOG.md @@ -1,5 +1,41 @@ # @backstage/plugin-sentry +## 0.3.7 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.3.6 + +### Patch Changes + +- f4c2bcf54: Use a more strict type for `variant` of cards. +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.3.5 ### Patch Changes diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index b79b7613fe..f010bd3e47 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sentry", - "version": "0.3.5", + "version": "0.3.7", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,9 +31,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -46,9 +46,9 @@ "timeago.js": "^4.0.2" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx index c52b76c4bc..b1ba8c7cc2 100644 --- a/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx +++ b/plugins/sentry/src/components/SentryIssuesWidget/SentryIssuesWidget.tsx @@ -14,24 +14,25 @@ * limitations under the License. */ -import React, { useEffect } from 'react'; +import { Entity } from '@backstage/catalog-model'; import { EmptyState, ErrorApi, errorApiRef, InfoCard, + InfoCardVariants, MissingAnnotationEmptyState, Progress, useApi, } from '@backstage/core'; -import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable'; +import React, { useEffect } from 'react'; import { useAsync } from 'react-use'; import { sentryApiRef } from '../../api'; +import SentryIssuesTable from '../SentryIssuesTable/SentryIssuesTable'; import { SENTRY_PROJECT_SLUG_ANNOTATION, useProjectSlug, } from '../useProjectSlug'; -import { Entity } from '@backstage/catalog-model'; export const SentryIssuesWidget = ({ entity, @@ -40,7 +41,7 @@ export const SentryIssuesWidget = ({ }: { entity: Entity; statsFor?: '24h' | '12h'; - variant?: string; + variant?: InfoCardVariants; }) => { const errorApi = useApi(errorApiRef); const sentryApi = useApi(sentryApiRef); diff --git a/plugins/sonarqube/CHANGELOG.md b/plugins/sonarqube/CHANGELOG.md index 11306bb5cd..677cdcbb32 100644 --- a/plugins/sonarqube/CHANGELOG.md +++ b/plugins/sonarqube/CHANGELOG.md @@ -1,5 +1,42 @@ # @backstage/plugin-sonarqube +## 0.1.13 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.1.12 + +### Patch Changes + +- 3a82293da: Fix bug retrieving current theme +- f4c2bcf54: Use a more strict type for `variant` of cards. +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.1.11 ### Patch Changes diff --git a/plugins/sonarqube/package.json b/plugins/sonarqube/package.json index e8236fbfd6..ba6c173a59 100644 --- a/plugins/sonarqube/package.json +++ b/plugins/sonarqube/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-sonarqube", - "version": "0.1.11", + "version": "0.1.13", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -32,9 +32,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/plugin-catalog-react": "^0.0.3", - "@backstage/core": "^0.6.1", + "@backstage/catalog-model": "^0.7.2", + "@backstage/plugin-catalog-react": "^0.1.0", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -47,9 +47,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx index 5577048fa9..12ed5b3bf4 100644 --- a/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx +++ b/plugins/sonarqube/src/components/SonarQubeCard/SonarQubeCard.tsx @@ -18,6 +18,7 @@ import { Entity } from '@backstage/catalog-model'; import { EmptyState, InfoCard, + InfoCardVariants, MissingAnnotationEmptyState, Progress, useApi, @@ -88,7 +89,7 @@ export const SonarQubeCard = ({ duplicationRatings = defaultDuplicationRatings, }: { entity?: Entity; - variant?: string; + variant?: InfoCardVariants; duplicationRatings?: DuplicationRating[]; }) => { const { entity } = useEntity(); diff --git a/plugins/splunk-on-call/CHANGELOG.md b/plugins/splunk-on-call/CHANGELOG.md new file mode 100644 index 0000000000..bff3bcf8bf --- /dev/null +++ b/plugins/splunk-on-call/CHANGELOG.md @@ -0,0 +1,37 @@ +# @backstage/plugin-splunk-on-call + +## 0.1.3 + +### Patch Changes + +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + +## 0.1.2 + +### Patch Changes + +- 70e2ba9cf: Added splunk-on-call plugin. +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/plugin-catalog-react@0.0.4 diff --git a/plugins/splunk-on-call/package.json b/plugins/splunk-on-call/package.json index c0542bd64c..8600afb105 100644 --- a/plugins/splunk-on-call/package.json +++ b/plugins/splunk-on-call/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-splunk-on-call", - "version": "0.1.1", + "version": "0.1.3", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,9 +30,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.2", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -45,9 +45,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/tech-radar/CHANGELOG.md b/plugins/tech-radar/CHANGELOG.md index 3c4e3a0706..697469bf7a 100644 --- a/plugins/tech-radar/CHANGELOG.md +++ b/plugins/tech-radar/CHANGELOG.md @@ -1,5 +1,19 @@ # @backstage/plugin-tech-radar +## 0.3.6 + +### Patch Changes + +- 9f2b3a26e: Added a dialog box that will show up when a you click on link on the radar and display the description if provided. +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [1407b34c6] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [3a58084b6] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + ## 0.3.5 ### Patch Changes diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 7a7c886372..223d5c1762 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.5", + "version": "0.3.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.6.1", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -43,9 +43,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/tech-radar/src/components/RadarComponent.tsx b/plugins/tech-radar/src/components/RadarComponent.tsx index 759fec62c3..bed45e63fc 100644 --- a/plugins/tech-radar/src/components/RadarComponent.tsx +++ b/plugins/tech-radar/src/components/RadarComponent.tsx @@ -68,6 +68,7 @@ const RadarComponent = (props: TechRadarComponentProps): JSX.Element => { }; }), moved: entry.timeline[0].moved, + description: entry.timeline[0].description, url: entry.url, }; }); diff --git a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx new file mode 100644 index 0000000000..b19794042e --- /dev/null +++ b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.test.tsx @@ -0,0 +1,43 @@ +/* + * 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 { render, screen } from '@testing-library/react'; +import { ThemeProvider } from '@material-ui/core'; +import { lightTheme } from '@backstage/theme'; + +import { Props, RadarDescription } from './RadarDescription'; + +const minProps: Props = { + open: true, + title: 'example-title', + description: 'example-description', + onClose: () => {}, +}; + +describe('RadarDescription', () => { + it('should render', () => { + render( + + + , + ); + + const radarDescription = screen.getByTestId('radar-description'); + expect(radarDescription).toBeInTheDocument(); + expect(screen.getByText(String(minProps.description))).toBeInTheDocument(); + }); +}); diff --git a/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx new file mode 100644 index 0000000000..20c489f630 --- /dev/null +++ b/plugins/tech-radar/src/components/RadarDescription/RadarDescription.tsx @@ -0,0 +1,63 @@ +/* + * 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 Dialog from '@material-ui/core/Dialog'; +import DialogTitle from '@material-ui/core/DialogTitle'; +import { Button, DialogActions, DialogContent } from '@material-ui/core'; +import LinkIcon from '@material-ui/icons/Link'; + +export type Props = { + open: boolean; + onClose: () => void; + title: string; + description: string; + url?: string; +}; + +const RadarDescription = (props: Props): JSX.Element => { + const { open, onClose, title, description, url } = props; + + const handleClick = () => { + onClose(); + if (url) { + window.location.href = url; + } + }; + + return ( + + + {title} + + {description} + {url && ( + + + + )} + + ); +}; + +export { RadarDescription }; diff --git a/plugins/tech-radar/src/components/RadarDescription/index.ts b/plugins/tech-radar/src/components/RadarDescription/index.ts new file mode 100644 index 0000000000..027407718e --- /dev/null +++ b/plugins/tech-radar/src/components/RadarDescription/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 { RadarDescription } from './RadarDescription'; +export type { Props } from './RadarDescription'; diff --git a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.test.tsx b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.test.tsx index 2d24f301d1..0424624007 100644 --- a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.test.tsx +++ b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.test.tsx @@ -15,7 +15,8 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; import { ThemeProvider } from '@material-ui/core'; import { lightTheme } from '@backstage/theme'; import GetBBoxPolyfill from '../../utils/polyfills/getBBox'; @@ -29,6 +30,12 @@ const minProps: Props = { color: 'red', }; +const optionalProps: Props = { + ...minProps, + title: 'example-title', + description: 'example-description', +}; + describe('RadarEntry', () => { beforeAll(() => { GetBBoxPolyfill.create(0, 0, 1000, 500); @@ -38,8 +45,8 @@ describe('RadarEntry', () => { GetBBoxPolyfill.remove(); }); - it('should render', () => { - const rendered = render( + it('should render link only', () => { + render( @@ -47,10 +54,29 @@ describe('RadarEntry', () => { , ); - const radarEntry = rendered.getByTestId('radar-entry'); + const radarEntry = screen.getByTestId('radar-entry'); const { x, y } = minProps; expect(radarEntry).toBeInTheDocument(); expect(radarEntry.getAttribute('transform')).toBe(`translate(${x}, ${y})`); - expect(rendered.getByText(String(minProps.value))).toBeInTheDocument(); + expect(screen.getByText(String(minProps.value))).toBeInTheDocument(); + }); + + it('should render with description', () => { + render( + + + + + , + ); + + userEvent.click(screen.getByRole('button')); + + const radarEntry = screen.getByTestId('radar-entry'); + expect(radarEntry).toBeInTheDocument(); + + const radarDescription = screen.getByTestId('radar-description'); + expect(radarDescription).toBeInTheDocument(); + expect(screen.getByText(String(minProps.value))).toBeInTheDocument(); }); }); diff --git a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx index 29d35e01e2..fb47a75c71 100644 --- a/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx +++ b/plugins/tech-radar/src/components/RadarEntry/RadarEntry.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { makeStyles, Theme } from '@material-ui/core'; import { WithLink } from '../../utils/components'; +import { RadarDescription } from '../RadarDescription'; export type Props = { x: number; @@ -25,6 +26,8 @@ export type Props = { color: string; url?: string; moved?: number; + description?: string; + title?: string; onMouseEnter?: (event: React.MouseEvent) => void; onMouseLeave?: (event: React.MouseEvent) => void; onClick?: (event: React.MouseEvent) => void; @@ -59,9 +62,12 @@ const makeBlip = (color: string, moved?: number) => { const RadarEntry = (props: Props): JSX.Element => { const classes = useStyles(props); + const [open, setOpen] = React.useState(false); const { moved, + description, + title, color, url, value, @@ -74,6 +80,18 @@ const RadarEntry = (props: Props): JSX.Element => { const blip = makeBlip(color, moved); + const handleClickOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const toggle = () => { + setOpen(!open); + }; + return ( { onClick={onClick} data-testid="radar-entry" > - - {blip} - + {' '} + {open && ( + + )} + {description ? ( + + {blip} + + ) : ( + + {blip} + + )} {value} diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx index e10993b010..d4ef38d5ee 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.tsx @@ -17,6 +17,7 @@ import React from 'react'; import { makeStyles, Theme } from '@material-ui/core'; import type { Quadrant, Ring, Entry } from '../../utils/types'; import { WithLink } from '../../utils/components'; +import { RadarDescription } from '../RadarDescription'; type Segments = { [k: number]: { [k: number]: Entry[] }; @@ -105,6 +106,62 @@ const RadarLegend = (props: Props): JSX.Element => { onEntryMouseLeave?: Props['onEntryMouseEnter']; }; + type RadarLegendLinkProps = { + url?: string; + description?: string; + title?: string; + }; + + const RadarLegendLink = ({ + url, + description, + title, + }: RadarLegendLinkProps) => { + const [open, setOpen] = React.useState(false); + + const handleClickOpen = () => { + setOpen(true); + }; + + const handleClose = () => { + setOpen(false); + }; + + const toggle = () => { + setOpen(!open); + }; + + if (description) { + return ( + <> + + {title} + + {open && ( + + )} + + ); + } + return ( + + {title} + + ); + }; + const RadarLegendRing = ({ ring, entries, @@ -129,9 +186,11 @@ const RadarLegend = (props: Props): JSX.Element => { onEntryMouseLeave && (() => onEntryMouseLeave(entry)) } > - - {entry.title} - + ))} diff --git a/plugins/tech-radar/src/components/RadarPage.test.tsx b/plugins/tech-radar/src/components/RadarPage.test.tsx index ee5cf8e131..918015001b 100644 --- a/plugins/tech-radar/src/components/RadarPage.test.tsx +++ b/plugins/tech-radar/src/components/RadarPage.test.tsx @@ -14,16 +14,19 @@ * limitations under the License. */ -import React from 'react'; -import { render, waitForElement } from '@testing-library/react'; -import { ThemeProvider } from '@material-ui/core'; +import { ApiProvider, ApiRegistry, errorApiRef } from '@backstage/core'; +import { + MockErrorApi, + renderInTestApp, + wrapInTestApp, +} from '@backstage/test-utils'; import { lightTheme } from '@backstage/theme'; -import { ApiRegistry, ApiProvider, errorApiRef } from '@backstage/core'; - +import { ThemeProvider } from '@material-ui/core'; +import { render, waitForElement } from '@testing-library/react'; +import React from 'react'; +import { act } from 'react-dom/test-utils'; import GetBBoxPolyfill from '../utils/polyfills/getBBox'; import { RadarPage } from './RadarPage'; -import { act } from 'react-dom/test-utils'; -import { MockErrorApi, wrapInTestApp } from '@backstage/test-utils'; describe('RadarPage', () => { beforeAll(() => { @@ -67,12 +70,10 @@ describe('RadarPage', () => { svgProps: { 'data-testid': 'tech-radar-svg' }, }; - const { getByText, getByTestId } = render( - wrapInTestApp( - - - , - ), + const { getByText, getByTestId } = await renderInTestApp( + + + , ); await waitForElement(() => getByTestId('tech-radar-svg')); @@ -94,7 +95,7 @@ describe('RadarPage', () => { svgProps: { 'data-testid': 'tech-radar-svg' }, }; - const { queryByTestId } = render( + const { queryByTestId } = await renderInTestApp( diff --git a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx index 7f68ffe021..4dd11c6ccf 100644 --- a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx +++ b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.tsx @@ -73,7 +73,9 @@ const RadarPlot = (props: Props): JSX.Element => { color={entry.color || ''} value={(entry?.index || 0) + 1} url={entry.url} + description={entry.description} moved={entry.moved} + title={entry.title} onMouseEnter={onEntryMouseEnter && (() => onEntryMouseEnter(entry))} onMouseLeave={onEntryMouseLeave && (() => onEntryMouseLeave(entry))} /> diff --git a/plugins/tech-radar/src/utils/types.ts b/plugins/tech-radar/src/utils/types.ts index 3322a67dcb..e4b2ef6fbe 100644 --- a/plugins/tech-radar/src/utils/types.ts +++ b/plugins/tech-radar/src/utils/types.ts @@ -68,6 +68,8 @@ export type Entry = { url?: string; // How this entry has recently moved; -1 for "down", +1 for "up", 0 for not moved moved?: MovedState; + // Most recent description to display in the UI + description?: string; active?: boolean; timeline?: Array; }; diff --git a/plugins/techdocs-backend/CHANGELOG.md b/plugins/techdocs-backend/CHANGELOG.md index 50925cec5f..49e3fdbc44 100644 --- a/plugins/techdocs-backend/CHANGELOG.md +++ b/plugins/techdocs-backend/CHANGELOG.md @@ -1,5 +1,34 @@ # @backstage/plugin-techdocs-backend +## 0.6.2 + +### Patch Changes + +- f37992797: Got rid of some `attr` and cleaned up a bit in the TechDocs config schema. +- Updated dependencies [bad21a085] +- Updated dependencies [2499f6cde] +- Updated dependencies [a1f5e6545] +- Updated dependencies [1e4ddd71d] + - @backstage/catalog-model@0.7.2 + - @backstage/techdocs-common@0.4.2 + - @backstage/config@0.1.3 + +## 0.6.1 + +### Patch Changes + +- b0a41c707: Add etag of the prepared file tree to techdocs_metadata.json in the storage +- Updated dependencies [16fb1d03a] +- Updated dependencies [491f3a0ec] +- Updated dependencies [434b4e81a] +- Updated dependencies [fb28da212] +- Updated dependencies [26e143e60] +- Updated dependencies [c6655413d] +- Updated dependencies [44414239f] +- Updated dependencies [b0a41c707] + - @backstage/backend-common@0.5.4 + - @backstage/techdocs-common@0.4.1 + ## 0.6.0 ### Minor Changes diff --git a/plugins/techdocs-backend/config.d.ts b/plugins/techdocs-backend/config.d.ts index 7ec1162d03..5a323f244a 100644 --- a/plugins/techdocs-backend/config.d.ts +++ b/plugins/techdocs-backend/config.d.ts @@ -13,36 +13,31 @@ * See the License for the specific language governing permissions and * 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 TechDocs schema please refer: plugins/techdocs/config.d.ts - * */ + */ export interface Config { - /** Configuration options for the techdocs-backend plugin */ + /** + * Configuration options for the techdocs-backend plugin + * @see http://backstage.io/docs/features/techdocs/configuration + */ techdocs: { /** - * documentation building process depends on the builder attr - * attr: 'builder' - accepts a string value - * e.g. builder: 'local' - * alternative: 'external' etc. - * @see http://backstage.io/docs/features/techdocs/configuration + * Documentation building process depends on the builder attr */ builder: 'local' | 'external'; + /** - * techdocs publisher information + * Techdocs publisher information */ publisher: { - /** - * attr: 'type' - accepts a string value - * e.g. type: 'local' - * aleternatives: 'googleGcs' etc. - * @see http://backstage.io/docs/features/techdocs/configuration - */ type: 'local' | 'googleGcs' | 'awsS3'; }; + /** - * attr: 'storageUrl' - accepts a string value - * e.g. storageUrl: http://localhost:7000/api/techdocs/static/docs + * @example 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 2d4d1309c0..03bea5233d 100644 --- a/plugins/techdocs-backend/package.json +++ b/plugins/techdocs-backend/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs-backend", - "version": "0.6.0", + "version": "0.6.2", "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.3", - "@backstage/catalog-model": "^0.7.1", - "@backstage/config": "^0.1.2", - "@backstage/techdocs-common": "^0.4.0", + "@backstage/backend-common": "^0.5.4", + "@backstage/catalog-model": "^0.7.2", + "@backstage/config": "^0.1.3", + "@backstage/techdocs-common": "^0.4.2", "@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.6.0", + "@backstage/cli": "^0.6.2", "supertest": "^4.0.2" }, "files": [ diff --git a/plugins/techdocs/CHANGELOG.md b/plugins/techdocs/CHANGELOG.md index f782dcb3ad..a1e7e456d4 100644 --- a/plugins/techdocs/CHANGELOG.md +++ b/plugins/techdocs/CHANGELOG.md @@ -1,5 +1,55 @@ # @backstage/plugin-techdocs +## 0.5.8 + +### Patch Changes + +- f37992797: Got rid of some `attr` and cleaned up a bit in the TechDocs config schema. +- 2499f6cde: Add support for assuming role in AWS integrations +- Updated dependencies [3a58084b6] +- Updated dependencies [e799e74d4] +- Updated dependencies [dc12852c9] +- Updated dependencies [d0760ecdf] +- Updated dependencies [1407b34c6] +- Updated dependencies [88f1f1b60] +- Updated dependencies [bad21a085] +- Updated dependencies [9615e68fb] +- Updated dependencies [49f9b7346] +- Updated dependencies [5c2e2863f] +- Updated dependencies [3a58084b6] +- Updated dependencies [2499f6cde] +- Updated dependencies [a1f5e6545] +- Updated dependencies [1e4ddd71d] +- Updated dependencies [2c1f2a7c2] + - @backstage/core@0.6.3 + - @backstage/test-utils@0.1.8 + - @backstage/plugin-catalog-react@0.1.0 + - @backstage/catalog-model@0.7.2 + - @backstage/techdocs-common@0.4.2 + - @backstage/config@0.1.3 + +## 0.5.7 + +### Patch Changes + +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [fb28da212] +- Updated dependencies [d34d26125] +- Updated dependencies [0af242b6d] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [10a0124e0] +- Updated dependencies [07e226872] +- Updated dependencies [26e143e60] +- Updated dependencies [c6655413d] +- Updated dependencies [44414239f] +- Updated dependencies [b0a41c707] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + - @backstage/techdocs-common@0.4.1 + - @backstage/plugin-catalog-react@0.0.4 + ## 0.5.6 ### Patch Changes diff --git a/plugins/techdocs/config.d.ts b/plugins/techdocs/config.d.ts index 1104d75237..1c49833d68 100644 --- a/plugins/techdocs/config.d.ts +++ b/plugins/techdocs/config.d.ts @@ -15,105 +15,81 @@ */ export interface Config { - /** Configuration options for the techdocs plugin */ + /** + * Configuration options for the techdocs plugin + * @see http://backstage.io/docs/features/techdocs/configuration + */ techdocs: { /** - * documentation building process depends on the builder attr - * attr: 'builder' - accepts a string value - * e.g. builder: 'local' - * alternative: 'external' etc. - * @see http://backstage.io/docs/features/techdocs/configuration + * Documentation building process depends on the builder attr * @visibility frontend */ builder: 'local' | 'external'; /** - * techdocs publisher information + * Techdocs generator information */ generators?: { - /** - * attr: 'techdocs' - accepts a string value - * e.g. type: 'docker' - * alternatives: 'local' etc. - * @see http://backstage.io/docs/features/techdocs/configuration - */ techdocs: 'local' | 'docker'; }; /** - * techdocs publisher information + * Techdocs publisher information */ publisher?: | { - /** - * attr: 'type' - accepts a string value - * e.g. type: 'local' - * alternatives: 'googleGcs' etc. - * @see http://backstage.io/docs/features/techdocs/configuration - */ type: 'local'; } | { - /** - * attr: 'type' - accepts a string value - * e.g. type: 'awsS3' - * alternatives: 'googleGcs' etc. - * @see http://backstage.io/docs/features/techdocs/configuration - */ type: 'awsS3'; /** - * awsS3 required when 'type' is set to awsS3 + * Required when 'type' is set to awsS3 */ awsS3?: { /** * (Optional) Credentials used to access a storage bucket. * If not set, environment variables or aws config file will be used to authenticate. - * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html - * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html + * @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-environment.html + * @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/loading-node-credentials-shared.html * @visibility secret */ credentials?: { /** * User access key id - * attr: 'accessKeyId' - accepts a string value * @visibility secret */ accessKeyId: string; /** * User secret access key - * attr: 'secretAccessKey' - accepts a string value * @visibility secret */ secretAccessKey: string; + /** + * ARN of role to be assumed + * @visibility backend + */ + roleArn?: string; }; /** * (Required) Cloud Storage Bucket Name - * attr: 'bucketName' - accepts a string value * @visibility backend */ bucketName: string; /** * (Optional) AWS Region. * If not set, AWS_REGION environment variable or aws config file will be used. - * https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html - * attr: 'region' - accepts a string value + * @see https://docs.aws.amazon.com/sdk-for-javascript/v3/developer-guide/setting-region.html * @visibility secret */ region?: string; }; } | { - /** - * attr: 'type' - accepts a string value - * e.g. type: 'azureBlobStorage' - * alternatives: 'azureBlobStorage' etc. - * @see http://backstage.io/docs/features/techdocs/configuration - */ type: 'azureBlobStorage'; /** - * azureBlobStorage required when 'type' is set to azureBlobStorage + * Required when 'type' is set to azureBlobStorage */ azureBlobStorage?: { /** @@ -123,66 +99,55 @@ export interface Config { credentials: { /** * Account access name - * attr: 'account' - accepts a string value * @visibility secret */ accountName: string; /** * (Optional) Account secret primary key * If not set, environment variables will be used to authenticate. - * https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json - * attr: 'accountKey' - accepts a string value + * @see https://docs.microsoft.com/en-us/azure/storage/common/storage-auth?toc=/azure/storage/blobs/toc.json * @visibility secret */ accountKey?: string; }; /** * (Required) Cloud Storage Container Name - * attr: 'containerName' - accepts a string value * @visibility backend */ containerName: string; }; } | { - /** - * attr: 'type' - accepts a string value - * e.g. type: 'googleGcs' - * alternatives: 'googleGcs' etc. - * @see http://backstage.io/docs/features/techdocs/configuration - */ type: 'googleGcs'; /** - * googleGcs required when 'type' is set to googleGcs + * Required when 'type' is set to googleGcs */ googleGcs?: { /** * (Required) Cloud Storage Bucket Name - * attr: 'bucketName' - accepts a string value * @visibility backend */ bucketName: string; /** * (Optional) API key used to write to a storage bucket. * If not set, environment variables will be used to authenticate. - * Read more: https://cloud.google.com/docs/authentication/production - * attr: 'credentials' - accepts a string value + * @see https://cloud.google.com/docs/authentication/production * @visibility secret */ credentials?: string; }; }; + /** - * attr: 'requestUrl' - accepts a string value - * e.g. requestUrl: http://localhost:7000/api/techdocs + * @example 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 + * @example http://localhost:7000/api/techdocs/static/docs * @deprecated */ storageUrl?: string; diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 32983270ad..360441945d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/plugin-techdocs", - "version": "0.5.6", + "version": "0.5.8", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -31,13 +31,13 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/config": "^0.1.2", - "@backstage/catalog-model": "^0.7.1", - "@backstage/core": "^0.6.1", - "@backstage/plugin-catalog-react": "^0.0.3", - "@backstage/test-utils": "^0.1.7", + "@backstage/config": "^0.1.3", + "@backstage/catalog-model": "^0.7.2", + "@backstage/core": "^0.6.3", + "@backstage/plugin-catalog-react": "^0.1.0", + "@backstage/test-utils": "^0.1.8", "@backstage/theme": "^0.2.3", - "@backstage/techdocs-common": "^0.4.0", + "@backstage/techdocs-common": "^0.4.2", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", @@ -50,9 +50,9 @@ "sanitize-html": "^2.3.2" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/user-settings/CHANGELOG.md b/plugins/user-settings/CHANGELOG.md index 467f207754..d35b347e16 100644 --- a/plugins/user-settings/CHANGELOG.md +++ b/plugins/user-settings/CHANGELOG.md @@ -1,5 +1,18 @@ # @backstage/plugin-user-settings +## 0.2.6 + +### Patch Changes + +- d872f662d: Use routed tabs to link to every settings page. +- Updated dependencies [fd3f2a8c0] +- Updated dependencies [f4c2bcf54] +- Updated dependencies [07e226872] +- Updated dependencies [f62e7abe5] +- Updated dependencies [96f378d10] +- Updated dependencies [688b73110] + - @backstage/core@0.6.2 + ## 0.2.5 ### Patch Changes diff --git a/plugins/user-settings/package.json b/plugins/user-settings/package.json index 493fb16a28..0e5d9a2859 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.5", + "version": "0.2.6", "main": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", @@ -30,7 +30,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.6.1", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index b6744216f3..cb8452e8f8 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -30,7 +30,7 @@ "start": "backstage-cli plugin:serve" }, "dependencies": { - "@backstage/core": "^0.6.1", + "@backstage/core": "^0.6.3", "@backstage/theme": "^0.2.3", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -41,9 +41,9 @@ "react-use": "^15.3.3" }, "devDependencies": { - "@backstage/cli": "^0.6.0", - "@backstage/dev-utils": "^0.1.10", - "@backstage/test-utils": "^0.1.7", + "@backstage/cli": "^0.6.2", + "@backstage/dev-utils": "^0.1.12", + "@backstage/test-utils": "^0.1.8", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^10.4.1", "@testing-library/user-event": "^12.0.7", diff --git a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx index b639da9d50..210cdcf921 100644 --- a/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx +++ b/plugins/welcome/src/components/WelcomePage/WelcomePage.test.tsx @@ -14,23 +14,22 @@ * limitations under the License. */ -import React from 'react'; -import { render } from '@testing-library/react'; -import WelcomePage from './WelcomePage'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; import { ApiProvider, ApiRegistry, - errorApiRef, configApiRef, ConfigReader, + errorApiRef, } from '@backstage/core'; +import { renderInTestApp } from '@backstage/test-utils'; +import { lightTheme } from '@backstage/theme'; +import { ThemeProvider } from '@material-ui/core'; +import React from 'react'; +import WelcomePage from './WelcomePage'; describe('WelcomePage', () => { - it('should render', () => { - // TODO: use common test app with mock implementations of all core APIs - const rendered = render( + it('should render', async () => { + const { baseElement } = await renderInTestApp( { , ); - expect(rendered.baseElement).toBeInTheDocument(); + expect(baseElement).toBeInTheDocument(); }); }); diff --git a/yarn.lock b/yarn.lock index ac3a9e4a28..818d8d9c9e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -44,6 +44,19 @@ dependencies: xss "^1.0.6" +"@apollographql/graphql-upload-8-fork@^8.1.3": + version "8.1.3" + resolved "https://registry.npmjs.org/@apollographql/graphql-upload-8-fork/-/graphql-upload-8-fork-8.1.3.tgz#a0d4e0d5cec8e126d78bd915c264d6b90f5784bc" + integrity sha512-ssOPUT7euLqDXcdVv3Qs4LoL4BPtfermW1IOouaqEmj36TpHYDmYDIbKoSQxikd9vtMumFnP87OybH7sC9fJ6g== + dependencies: + "@types/express" "*" + "@types/fs-capacitor" "*" + "@types/koa" "*" + busboy "^0.3.1" + fs-capacitor "^2.0.4" + http-errors "^1.7.3" + object-path "^0.11.4" + "@ardatan/aggregate-error@0.0.1": version "0.0.1" resolved "https://registry.npmjs.org/@ardatan/aggregate-error/-/aggregate-error-0.0.1.tgz#1403ac5de10d8ca689fc1f65844c27179ae1d44f" @@ -68,13 +81,13 @@ dependencies: "@openapi-contrib/openapi-schema-to-json-schema" "^3.0.0" -"@asyncapi/parser@^1.3.2": - version "1.3.2" - resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.3.2.tgz#6ff22680ff0e9aaf732121040e01e1f5836a305d" - integrity sha512-YDGdxoVA3+gQ3jvWDp+J3IV5XsvAe+nSHSytPg9m5hfJ48R+mBPMnO7Bnh//gR17LjbmyebPj27h4Ceyaa4d3Q== +"@asyncapi/parser@^1.4.0": + version "1.4.0" + resolved "https://registry.npmjs.org/@asyncapi/parser/-/parser-1.4.0.tgz#a0b0e3cb1803400d93b15f7c354fe95824a36e2c" + integrity sha512-rI0UF9niaqUxZqFS2+iBvkekxhI3cXOv/O+i+w/1EhpPppLicPVnazQbBqQ1iO37sio+CPejJbeoBgj8m+ylPw== dependencies: "@apidevtools/json-schema-ref-parser" "^9.0.6" - "@asyncapi/specs" "^2.7.5" + "@asyncapi/specs" "^2.7.6" "@fmvilas/pseudo-yaml-ast" "^0.3.1" ajv "^6.10.1" js-yaml "^3.13.1" @@ -82,14 +95,14 @@ node-fetch "^2.6.0" tiny-merge-patch "^0.1.2" -"@asyncapi/react-component@^0.18.2": - version "0.18.2" - resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-0.18.2.tgz#74882b4c2a5ecd00c156898f38d0a5c9e8c4384d" - integrity sha512-vsD3acSM0hTxxSsTJoU79kpI9FXVA64OqNCu8xrawfaU4wdsSQWWn0y7SD8pUfwe4gohwmms0xNcAaMzYIhGLw== +"@asyncapi/react-component@^0.19.2": + version "0.19.2" + resolved "https://registry.npmjs.org/@asyncapi/react-component/-/react-component-0.19.2.tgz#364a1d839e6f49a454fcb44b544f9496d1501b93" + integrity sha512-NXLBVaJdXqsIX98tF8fKxjTDf6cE+ym3Fkp3IQhMQh/6o1X2y/YVp+XkjfLO2C/Ccb/Cys1C5OnqipfwEY1oqA== dependencies: "@asyncapi/avro-schema-parser" "^0.2.0" "@asyncapi/openapi-schema-parser" "^2.0.0" - "@asyncapi/parser" "^1.3.2" + "@asyncapi/parser" "^1.4.0" constate "^1.2.0" dompurify "^2.1.1" markdown-it "^11.0.1" @@ -97,10 +110,10 @@ openapi-sampler "^1.0.0-beta.15" react-use "^12.2.0" -"@asyncapi/specs@^2.7.5": - version "2.7.5" - resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.5.tgz#3a516d198fc41a1103695bd889fdd4fbbebe7f5d" - integrity sha512-T1Ham9sqZKCtSowXRPaBCRy2oz3KHglqqrKiaO7lEudpP6lwH5SwXaq4qliyKzWaqd22srJHE4szdsorbFZKlw== +"@asyncapi/specs@^2.7.6": + version "2.7.6" + resolved "https://registry.npmjs.org/@asyncapi/specs/-/specs-2.7.6.tgz#d04ad015a148cd222f7939b9561360ab903de632" + integrity sha512-2IYlLA02beYQKVJVCAqV+G5tXRI4s8yeKYiszuoxS178psdAMKxgXtng8hUYPOqSvU6X4lc+Jabk1zSQFHifVg== "@aws-sdk/credential-provider-env@3.3.0": version "3.3.0" @@ -1803,9 +1816,9 @@ to-fast-properties "^2.0.0" "@backstage/catalog-model@^0.2.0": - version "0.7.1" + version "0.7.2" dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.3" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" ajv "^7.0.3" @@ -1815,9 +1828,9 @@ yup "^0.29.3" "@backstage/catalog-model@^0.3.0": - version "0.7.1" + version "0.7.2" dependencies: - "@backstage/config" "^0.1.2" + "@backstage/config" "^0.1.3" "@types/json-schema" "^7.0.5" "@types/yup" "^0.29.8" ajv "^7.0.3" @@ -1827,10 +1840,10 @@ yup "^0.29.3" "@backstage/core@^0.3.0": - version "0.6.1" + version "0.6.3" dependencies: - "@backstage/config" "^0.1.2" - "@backstage/core-api" "^0.2.8" + "@backstage/config" "^0.1.3" + "@backstage/core-api" "^0.2.11" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -1840,6 +1853,7 @@ "@types/prop-types" "^15.7.3" "@types/react" "^16.9" "@types/react-sparklines" "^1.7.0" + "@types/react-text-truncate" "^0.14.0" classnames "^2.2.6" clsx "^1.1.0" d3-selection "^2.0.0" @@ -1861,33 +1875,18 @@ react-router-dom "6.0.0-beta.0" react-sparklines "^1.7.0" react-syntax-highlighter "^13.5.1" + react-text-truncate "^0.16.0" react-use "^15.3.3" remark-gfm "^1.0.0" zen-observable "^0.8.15" -"@backstage/plugin-catalog-react@^0.0.2": - version "0.0.2" - resolved "https://registry.npmjs.org/@backstage/plugin-catalog-react/-/plugin-catalog-react-0.0.2.tgz#e50da2dac9fab3a0d5973f8d1083ee2c368e5e52" - integrity sha512-O6aujFPRaEFTk4XlwOoswbnoHIOqMtj6ycUj6R1mNKOM4plUgGDKKhO3be69FHMJEMbiSvVe6AW+1kXaK+1LqA== - dependencies: - "@backstage/catalog-client" "^0.3.5" - "@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" - react-router "6.0.0-beta.0" - react-router-dom "6.0.0-beta.0" - react-use "^15.3.3" - -"@backstage/plugin-catalog@^0.2.0": - version "0.3.1" +"@backstage/plugin-catalog@^0.2.1": + version "0.4.0" dependencies: "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.1" - "@backstage/plugin-catalog-react" "^0.0.3" - "@backstage/plugin-scaffolder" "^0.5.0" + "@backstage/catalog-model" "^0.7.2" + "@backstage/core" "^0.6.3" + "@backstage/plugin-catalog-react" "^0.1.0" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -1903,14 +1902,13 @@ react-use "^15.3.3" swr "^0.3.0" -"@backstage/plugin-catalog@^0.2.1": - version "0.3.1" +"@backstage/plugin-catalog@^0.3.1": + version "0.4.0" dependencies: "@backstage/catalog-client" "^0.3.6" - "@backstage/catalog-model" "^0.7.1" - "@backstage/core" "^0.6.1" - "@backstage/plugin-catalog-react" "^0.0.3" - "@backstage/plugin-scaffolder" "^0.5.0" + "@backstage/catalog-model" "^0.7.2" + "@backstage/core" "^0.6.3" + "@backstage/plugin-catalog-react" "^0.1.0" "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" @@ -1968,9 +1966,9 @@ semver "^5.4.1" "@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== + version "2.14.1" + resolved "https://registry.npmjs.org/@changesets/cli/-/cli-2.14.1.tgz#ef6ba9f69e4dd5f6cde4220e032200e3039a52f8" + integrity sha512-ydU2ZUP/s7nHQmz8TrliT+kE2dJUpZYKh8MBHrkV6suchCBhT4DmoP4VDF9M4215r18iPbMpE1TjskzXGXB8eQ== dependencies: "@babel/runtime" "^7.10.4" "@changesets/apply-release-plan" "^4.2.0" @@ -3882,9 +3880,9 @@ react-transition-group "^4.4.0" "@material-ui/icons@^4.9.1": - version "4.9.1" - resolved "https://registry.npmjs.org/@material-ui/icons/-/icons-4.9.1.tgz#fdeadf8cb3d89208945b33dbc50c7c616d0bd665" - integrity sha512-GBitL3oBWO0hzBhvA9KxqcowRUsA0qzwKkURyC8nppnC3fw54KPKZ+d4V1Eeg/UnDRSzDaI9nGCdel/eh9AQMg== + version "4.11.2" + resolved "https://registry.npmjs.org/@material-ui/icons/-/icons-4.11.2.tgz#b3a7353266519cd743b6461ae9fdfcb1b25eb4c5" + integrity sha512-fQNsKX2TxBmqIGJCSi3tGTO/gZ+eJgWmMJkgDiOfyNaunNaxcklJQFaFogYcFl0qFuaEz1qaXYXboa/bUXVSOQ== dependencies: "@babel/runtime" "^7.4.4" @@ -3899,17 +3897,6 @@ prop-types "^15.7.2" react-is "^16.8.0" -"@material-ui/lab@^4.0.0-alpha.56": - version "4.0.0-alpha.56" - resolved "https://registry.npmjs.org/@material-ui/lab/-/lab-4.0.0-alpha.56.tgz#ff63080949b55b40625e056bbda05e130d216d34" - integrity sha512-xPlkK+z/6y/24ka4gVJgwPfoCF4RCh8dXb1BNE7MtF9bXEBLN/lBxNTK8VAa0qm3V2oinA6xtUIdcRh0aeRtVw== - dependencies: - "@babel/runtime" "^7.4.4" - "@material-ui/utils" "^4.10.2" - clsx "^1.0.4" - prop-types "^15.7.2" - react-is "^16.8.0" - "@material-ui/pickers@^3.2.2": version "3.2.10" resolved "https://registry.npmjs.org/@material-ui/pickers/-/pickers-3.2.10.tgz#19df024895876eb0ec7cd239bbaea595f703f0ae" @@ -4164,14 +4151,6 @@ "@octokit/types" "^6.8.3" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@4.4.1": - version "4.4.1" - resolved "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.4.1.tgz#105cf93255432155de078c9efc33bd4e14d1cd63" - integrity sha512-+v5PcvrUcDeFXf8hv1gnNvNLdm4C0+2EiuWt9EatjjUmfriM1pTMM+r4j1lLHxeBQ9bVDmbywb11e3KjuavieA== - dependencies: - "@octokit/types" "^6.1.0" - deprecation "^2.3.1" - "@octokit/request-error@^2.0.0": version "2.0.2" resolved "https://registry.npmjs.org/@octokit/request-error/-/request-error-2.0.2.tgz#0e76b83f5d8fdda1db99027ea5f617c2e6ba9ed0" @@ -4195,17 +4174,7 @@ once "^1.4.0" universal-user-agent "^6.0.0" -"@octokit/rest@^18.0.0", "@octokit/rest@^18.0.12": - version "18.0.12" - resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.0.12.tgz#278bd41358c56d87c201e787e8adc0cac132503a" - integrity sha512-hNRCZfKPpeaIjOVuNJzkEL6zacfZlBPV8vw8ReNeyUkVvbuCvvrrx8K8Gw2eyHHsmd4dPlAxIXIZ9oHhJfkJpw== - dependencies: - "@octokit/core" "^3.2.3" - "@octokit/plugin-paginate-rest" "^2.6.2" - "@octokit/plugin-request-log" "^1.0.2" - "@octokit/plugin-rest-endpoint-methods" "4.4.1" - -"@octokit/rest@^18.1.0": +"@octokit/rest@^18.0.0", "@octokit/rest@^18.0.12", "@octokit/rest@^18.1.0": version "18.1.1" resolved "https://registry.npmjs.org/@octokit/rest/-/rest-18.1.1.tgz#bd7053c28db3577c936029e9da6bfbd046474a2f" integrity sha512-ZcCHMyfGT1qtJD72usigAfUQ6jU89ZUPFb2AOubR6WZ7/RRFVZUENVm1I2yvJBUicqTujezPW9cY1+o3Mb4rNA== @@ -4222,15 +4191,7 @@ dependencies: "@types/node" ">= 8" -"@octokit/types@^6.0.0", "@octokit/types@^6.0.1", "@octokit/types@^6.0.3", "@octokit/types@^6.1.0": - version "6.2.1" - resolved "https://registry.npmjs.org/@octokit/types/-/types-6.2.1.tgz#7f881fe44475ab1825776a4a59ca1ae082ed1043" - integrity sha512-jHs9OECOiZxuEzxMZcXmqrEO8GYraHF+UzNVH2ACYh8e/Y7YoT+hUf9ldvVd6zIvWv4p3NdxbQ0xx3ku5BnSiA== - dependencies: - "@octokit/openapi-types" "^2.2.0" - "@types/node" ">= 8" - -"@octokit/types@^6.8.3": +"@octokit/types@^6.0.0", "@octokit/types@^6.0.1", "@octokit/types@^6.0.3", "@octokit/types@^6.8.3": version "6.8.5" resolved "https://registry.npmjs.org/@octokit/types/-/types-6.8.5.tgz#797dfdad8c75718e97dc687d4c9fc49200ca8d17" integrity sha512-ZsQawftZoi0kSF2pCsdgLURbOjtVcHnBOXiSxBKSNF56CRjARt5rb/g8WJgqB8vv4lgUEHrv06EdDKYQ22vA9Q== @@ -4412,27 +4373,27 @@ react-router "^6.0.0-beta.0" react-use "^15.3.3" -"@roadiehq/backstage-plugin-github-pull-requests@^0.6.3": - version "0.6.3" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.6.3.tgz#46b63e90f3f5412a4b8f0df96ada62cff7046478" - integrity sha512-ofWH9k4WVVwTbK/XAVqmtH03QW3rsT822p4neOse0Wy0dAKlqlSU4nwl5jBKMJXsf8lfc0c7gbc50R7VumzoOQ== +"@roadiehq/backstage-plugin-github-pull-requests@^0.7.6": + version "0.7.6" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-0.7.6.tgz#06a7c22ddf26ab504dd8f95c96ef0319c915ed80" + integrity sha512-UMNniDR+0MGhVX6f4Bojm4BOmv6u323ycWkzXNLoQpWSm03EpZGeT8FL1Q0CY0Cd4DgdoV+L0seFqt8jEbOr0g== dependencies: - "@backstage/catalog-model" "^0.2.0" - "@backstage/core" "^0.3.0" - "@backstage/plugin-catalog" "^0.2.0" - "@backstage/theme" "^0.2.0" + "@backstage/catalog-model" "^0.7.1" + "@backstage/core" "^0.6.1" + "@backstage/plugin-catalog" "^0.3.1" + "@backstage/theme" "^0.2.3" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" - "@material-ui/lab" "^4.0.0-alpha.56" "@octokit/rest" "^18.0.0" "@octokit/types" "^5.0.1" - "@types/react-dom" "^16.9.8" + "@types/node-fetch" "^2.5.7" history "^5.0.0" moment "^2.27.0" + node-fetch "^2.6.1" react "^16.13.1" react-dom "^16.13.1" react-router "6.0.0-beta.0" - react-use "^15.3.3" + react-use "^15.3.6" "@roadiehq/backstage-plugin-travis-ci@^0.2.8": version "0.2.8" @@ -5857,7 +5818,14 @@ resolved "https://registry.npmjs.org/@types/core-js/-/core-js-2.5.4.tgz#fc42ebde7d9cfa7c5f2668f117449b02348e41fd" integrity sha512-Xwy8o12ak+iYgFr/KCVaVK5Sy+jFMiiPAID3+ObvMlBzy26XQJw5xu+a6rlHsrJENXj/AwJOGsJpVohUjAzSKQ== -"@types/cors@^2.8.4", "@types/cors@^2.8.6": +"@types/cors@2.8.8": + version "2.8.8" + resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.8.tgz#317a8d8561995c60e35b9e0fcaa8d36660c98092" + integrity sha512-fO3gf3DxU2Trcbr75O7obVndW/X5k8rJNZkLXlQWStTHhP71PkRqjwPIEI0yMnJdg9R9OasjU+Bsr+Hr1xy/0w== + dependencies: + "@types/express" "*" + +"@types/cors@^2.8.6": version "2.8.9" resolved "https://registry.npmjs.org/@types/cors/-/cors-2.8.9.tgz#4bd1fcac72eca8d5bec93e76c7fdcbdc1bc2cd4a" integrity sha512-zurD1ibz21BRlAOIKP8yhrxlqKx6L9VCwkB5kMiP6nZAhoF5MvC7qS1qPA7nRcr1GJolfkQC7/EAL4hdYejLtg== @@ -5958,7 +5926,7 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== -"@types/express-serve-static-core@*", "@types/express-serve-static-core@^4.17.5": +"@types/express-serve-static-core@*", "@types/express-serve-static-core@4.17.18", "@types/express-serve-static-core@^4.17.5": version "4.17.18" resolved "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.18.tgz#8371e260f40e0e1ca0c116a9afcd9426fa094c40" integrity sha512-m4JTwx5RUBNZvky/JJ8swEJPKFd8si08pPF2PfizYjGZOKr/svUWPcoUmLow6MmPzhasphB7gSTINY67xn3JNA== @@ -6047,16 +6015,6 @@ dependencies: "@types/node" "*" -"@types/graphql-upload@^8.0.0": - version "8.0.3" - resolved "https://registry.npmjs.org/@types/graphql-upload/-/graphql-upload-8.0.3.tgz#b371edb5f305a2a1f7b7843a890a2a7adc55c3ec" - integrity sha512-hmLg9pCU/GmxBscg8GCr1vmSoEmbItNNxdD5YH2TJkXm//8atjwuprB+xJBK714JG1dkxbbhp5RHX+Pz1KsCMA== - dependencies: - "@types/express" "*" - "@types/fs-capacitor" "*" - "@types/koa" "*" - graphql "^14.5.3" - "@types/hast@^2.0.0": version "2.3.1" resolved "https://registry.npmjs.org/@types/hast/-/hast-2.3.1.tgz#b16872f2a6144c7025f296fb9636a667ebb79cd9" @@ -6130,6 +6088,11 @@ dependencies: "@types/node" "*" +"@types/humanize-duration@^3.18.1": + version "3.18.1" + resolved "https://registry.npmjs.org/@types/humanize-duration/-/humanize-duration-3.18.1.tgz#10090d596053703e7de0ac43a37b96cd9fc78309" + integrity sha512-MUgbY3CF7hg/a/jogixmAufLjJBQT7WEf8Q+kYJkOc47ytngg1IuZobCngdTjAgY83JWEogippge5O5fplaQlw== + "@types/inquirer@^7.3.1": version "7.3.1" resolved "https://registry.npmjs.org/@types/inquirer/-/inquirer-7.3.1.tgz#1f231224e7df11ccfaf4cf9acbcc3b935fea292d" @@ -6373,7 +6336,7 @@ dependencies: "@types/node" "*" -"@types/node-fetch@2.5.7", "@types/node-fetch@^2.5.4": +"@types/node-fetch@2.5.7": version "2.5.7" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz#20a2afffa882ab04d44ca786449a276f9f6bbf3c" integrity sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw== @@ -6381,7 +6344,7 @@ "@types/node" "*" form-data "^3.0.0" -"@types/node-fetch@^2.5.0": +"@types/node-fetch@^2.5.0", "@types/node-fetch@^2.5.4", "@types/node-fetch@^2.5.7": version "2.5.8" resolved "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.8.tgz#e199c835d234c7eb0846f6618012e558544ee2fb" integrity sha512-fbjI6ja0N5ZA8TV53RUqzsKNkl9fv8Oj3T7zxW7FGv1GSH7gwJaNF8dzCjrqKaxKeUpTz4yT1DaJFq/omNpGfw== @@ -6605,6 +6568,13 @@ dependencies: "@types/react" "*" +"@types/react-text-truncate@^0.14.0": + version "0.14.0" + resolved "https://registry.npmjs.org/@types/react-text-truncate/-/react-text-truncate-0.14.0.tgz#588bbabbc7f2a13815e805f3a48942db73fe65fe" + integrity sha512-XZNmx8mMPFjRLFjFqIF5uLPXEZ4THKxoEv1yU63JzInV3ES42PD+DaQOK6+Rd+cRolzrNoY4YIY9rrW8kh4ikw== + dependencies: + "@types/react" "*" + "@types/react-transition-group@^4.2.0": version "4.2.4" resolved "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.2.4.tgz#c7416225987ccdb719262766c1483da8f826838d" @@ -6955,9 +6925,9 @@ "@types/node" "*" "@types/xml2js@^0.4.7": - version "0.4.7" - resolved "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.7.tgz#cd5b6c67bbec741ac625718a76e6cb99bc34365e" - integrity sha512-f5VOKSMEE0O+/L54FHwA/a7vcx9mHeSDM71844yHCOhh8Cin2xQa0UFw0b7Vc5hoZ3Ih6ZHaDobjfLih4tWPNw== + version "0.4.8" + resolved "https://registry.npmjs.org/@types/xml2js/-/xml2js-0.4.8.tgz#84c120c864a5976d0b5cf2f930a75d850fc2b03a" + integrity sha512-EyvT83ezOdec7BhDaEcsklWy7RSIdi6CNe95tmOAK0yx/Lm30C9K75snT3fYayK59ApC2oyW+rcHErdG05FHJA== dependencies: "@types/node" "*" @@ -7533,43 +7503,21 @@ anymatch@^3.0.3, anymatch@~3.1.1: normalize-path "^3.0.0" picomatch "^2.0.4" -apollo-cache-control@^0.11.1: - version "0.11.1" - resolved "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.11.1.tgz#3bce0924ae7322a8b9f7ca1e2fb036d1fc9f1df5" - integrity sha512-6iHa8TkcKt4rx5SKRzDNjUIpCQX+7/FlZwD7vRh9JDnM4VH8SWhpj8fUR3CiEY8Kuc4ChXnOY8bCcMju5KPnIQ== +apollo-cache-control@^0.11.6: + version "0.11.6" + resolved "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.11.6.tgz#f7bdf924272af47ac474cf3f3f35cfc038cc9485" + integrity sha512-YZ+uuIG+fPy+mkpBS2qKF0v1qlzZ3PW6xZVaDukeK3ed3iAs4L/2YnkTqau3OmoF/VPzX2FmSkocX/OVd59YSw== dependencies: - apollo-server-env "^2.4.5" - apollo-server-plugin-base "^0.9.1" + apollo-server-env "^3.0.0" + apollo-server-plugin-base "^0.10.4" -apollo-datasource@^0.7.2: - version "0.7.2" - resolved "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.2.tgz#1662ee93453a9b89af6f73ce561bde46b41ebf31" - integrity sha512-ibnW+s4BMp4K2AgzLEtvzkjg7dJgCaw9M5b5N0YKNmeRZRnl/I/qBTQae648FsRKgMwTbRQIvBhQ0URUFAqFOw== +apollo-datasource@^0.7.3: + version "0.7.3" + resolved "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.7.3.tgz#c824eb1457bdee5a3173ced0e35e594547e687a0" + integrity sha512-PE0ucdZYjHjUyXrFWRwT02yLcx2DACsZ0jm1Mp/0m/I9nZu/fEkvJxfsryXB6JndpmQO77gQHixf/xGCN976kA== dependencies: - apollo-server-caching "^0.5.2" - apollo-server-env "^2.4.5" - -apollo-engine-reporting-protobuf@^0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/apollo-engine-reporting-protobuf/-/apollo-engine-reporting-protobuf-0.5.2.tgz#b01812508a1c583328a8dc603769bc63b8895e7e" - integrity sha512-4wm9FR3B7UvJxcK/69rOiS5CAJPEYKufeRWb257ZLfX7NGFTMqvbc1hu4q8Ch7swB26rTpkzfsftLED9DqH9qg== - dependencies: - "@apollo/protobufjs" "^1.0.3" - -apollo-engine-reporting@^2.3.0: - version "2.3.0" - resolved "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-2.3.0.tgz#3bb59f81aaf6b967ed098896a4a60a053b4eed5a" - integrity sha512-SbcPLFuUZcRqDEZ6mSs8uHM9Ftr8yyt2IEu0JA8c3LNBmYXSLM7MHqFe80SVcosYSTBgtMz8mLJO8orhYoSYZw== - dependencies: - apollo-engine-reporting-protobuf "^0.5.2" - apollo-graphql "^0.5.0" - apollo-server-caching "^0.5.2" - apollo-server-env "^2.4.5" - apollo-server-errors "^2.4.2" - apollo-server-plugin-base "^0.9.1" - apollo-server-types "^0.5.1" - async-retry "^1.2.1" - uuid "^8.0.0" + apollo-server-caching "^0.5.3" + apollo-server-env "^3.0.0" apollo-env@^0.6.5: version "0.6.5" @@ -7581,12 +7529,22 @@ apollo-env@^0.6.5: node-fetch "^2.2.0" sha.js "^2.4.11" -apollo-graphql@^0.5.0: - version "0.5.0" - resolved "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.5.0.tgz#7e9152093211b58352aa6504d8d39ec7241d6872" - integrity sha512-YSdF/BKPbsnQpxWpmCE53pBJX44aaoif31Y22I/qKpB6ZSGzYijV5YBoCL5Q15H2oA/v/02Oazh9lbp4ek3eig== +apollo-env@^0.6.6: + version "0.6.6" + resolved "https://registry.npmjs.org/apollo-env/-/apollo-env-0.6.6.tgz#d7880805c4e96ee3d4142900a405176a04779438" + integrity sha512-hXI9PjJtzmD34XviBU+4sPMOxnifYrHVmxpjykqI/dUD2G3yTiuRaiQqwRwB2RCdwC1Ug/jBfoQ/NHDTnnjndQ== dependencies: - apollo-env "^0.6.5" + "@types/node-fetch" "2.5.7" + core-js "^3.0.1" + node-fetch "^2.2.0" + sha.js "^2.4.11" + +apollo-graphql@^0.6.0: + version "0.6.1" + resolved "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.6.1.tgz#d0bf0aff76f445de3da10e08f6974f1bf65f5753" + integrity sha512-ZRXAV+k+hboCVS+FW86FW/QgnDR7gm/xMUwJPGXEbV53OLGuQQdIT0NCYK7AzzVkCfsbb7NJ3mmEclkZY9uuxQ== + dependencies: + apollo-env "^0.6.6" lodash.sortby "^4.7.0" apollo-link-http-common@^0.2.14: @@ -7608,6 +7566,13 @@ apollo-link@^1.2.12, apollo-link@^1.2.14: tslib "^1.9.3" zen-observable-ts "^0.8.21" +apollo-reporting-protobuf@^0.6.2: + version "0.6.2" + resolved "https://registry.npmjs.org/apollo-reporting-protobuf/-/apollo-reporting-protobuf-0.6.2.tgz#5572866be9b77f133916532b10e15fbaa4158304" + integrity sha512-WJTJxLM+MRHNUxt1RTl4zD0HrLdH44F2mDzMweBj1yHL0kSt8I1WwoiF/wiGVSpnG48LZrBegCaOJeuVbJTbtw== + dependencies: + "@apollo/protobufjs" "^1.0.3" + apollo-server-caching@0.5.1: version "0.5.1" resolved "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.1.tgz#5cd0536ad5473abb667cc82b59bc56b96fb35db6" @@ -7615,45 +7580,48 @@ apollo-server-caching@0.5.1: dependencies: lru-cache "^5.0.0" -apollo-server-caching@^0.5.2: - version "0.5.2" - resolved "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.2.tgz#bef5d5e0d48473a454927a66b7bb947a0b6eb13e" - integrity sha512-HUcP3TlgRsuGgeTOn8QMbkdx0hLPXyEJehZIPrcof0ATz7j7aTPA4at7gaiFHCo8gk07DaWYGB3PFgjboXRcWQ== +apollo-server-caching@^0.5.3: + version "0.5.3" + resolved "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.3.tgz#cf42a77ad09a46290a246810075eaa029b5305e1" + integrity sha512-iMi3087iphDAI0U2iSBE9qtx9kQoMMEWr6w+LwXruBD95ek9DWyj7OeC2U/ngLjRsXM43DoBDXlu7R+uMjahrQ== dependencies: - lru-cache "^5.0.0" + lru-cache "^6.0.0" -apollo-server-core@^2.16.1: - version "2.16.1" - resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.16.1.tgz#5b5b8245ab9c0cb6c2367ec19ab855dea6ccea3a" - integrity sha512-nuwn5ZBbmzPwDetb3FgiFFJlNK7ZBFg8kis/raymrjd3eBGdNcOyMTJDl6J9673X9Xqp+dXQmFYDW/G3G8S1YA== +apollo-server-core@^2.16.1, apollo-server-core@^2.21.0: + version "2.21.0" + resolved "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.21.0.tgz#12ee11aee61fa124f11b1d73cae2e068112a3a53" + integrity sha512-GtIiq2F0dVDLzzIuO5+dK/pGq/sGxYlKCqAuQQqzYg0fvZ7fukyluXtcTe0tMI+FJZjU0j0WnKgiLsboCoAlPQ== dependencies: "@apollographql/apollo-tools" "^0.4.3" "@apollographql/graphql-playground-html" "1.6.26" - "@types/graphql-upload" "^8.0.0" + "@apollographql/graphql-upload-8-fork" "^8.1.3" "@types/ws" "^7.0.0" - apollo-cache-control "^0.11.1" - apollo-datasource "^0.7.2" - apollo-engine-reporting "^2.3.0" - apollo-server-caching "^0.5.2" - apollo-server-env "^2.4.5" + apollo-cache-control "^0.11.6" + apollo-datasource "^0.7.3" + apollo-graphql "^0.6.0" + apollo-reporting-protobuf "^0.6.2" + apollo-server-caching "^0.5.3" + apollo-server-env "^3.0.0" apollo-server-errors "^2.4.2" - apollo-server-plugin-base "^0.9.1" - apollo-server-types "^0.5.1" - apollo-tracing "^0.11.1" + apollo-server-plugin-base "^0.10.4" + apollo-server-types "^0.6.3" + apollo-tracing "^0.12.2" + async-retry "^1.2.1" fast-json-stable-stringify "^2.0.0" - graphql-extensions "^0.12.4" - graphql-tag "^2.9.2" - graphql-tools "^4.0.0" - graphql-upload "^8.0.2" + graphql-extensions "^0.12.8" + graphql-tag "^2.11.0" + graphql-tools "^4.0.8" loglevel "^1.6.7" + lru-cache "^6.0.0" sha.js "^2.4.11" subscriptions-transport-ws "^0.9.11" + uuid "^8.0.0" ws "^6.0.0" -apollo-server-env@^2.4.5: - version "2.4.5" - resolved "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-2.4.5.tgz#73730b4f0439094a2272a9d0caa4079d4b661d5f" - integrity sha512-nfNhmGPzbq3xCEWT8eRpoHXIPNcNy3QcEoBlzVMjeglrBGryLG2LXwBSPnVmTRRrzUYugX0ULBtgE3rBFNoUgA== +apollo-server-env@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/apollo-server-env/-/apollo-server-env-3.0.0.tgz#0157c51f52b63aee39af190760acf789ffc744d9" + integrity sha512-tPSN+VttnPsoQAl/SBVUpGbLA97MXG990XIwq6YUnJyAixrrsjW1xYG7RlaOqetxm80y5mBZKLrRDiiSsW/vog== dependencies: node-fetch "^2.1.2" util.promisify "^1.0.0" @@ -7664,42 +7632,43 @@ apollo-server-errors@^2.4.2: integrity sha512-FeGxW3Batn6sUtX3OVVUm7o56EgjxDlmgpTLNyWcLb0j6P8mw9oLNyAm3B+deHA4KNdNHO5BmHS2g1SJYjqPCQ== apollo-server-express@^2.16.1: - version "2.16.1" - resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.16.1.tgz#7438bca590ef8577d24d20ba0b6d582c120c0146" - integrity sha512-Oq5YNcaMYnRk6jDmA9LWf8oSd2KHDVe7jQ4wtooAvG9FVUD+FaFBgSkytXHMvtifQh2wdF07Ri8uDLMz6IQjTw== + version "2.21.0" + resolved "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.21.0.tgz#29bd4ec728e1992da240c5956c3ce6d95c1d252e" + integrity sha512-zbOSNGuxUjlOFZnRrbMpga3pKDEroitF4NAqoVxgBivx7v2hGsE7rljct3PucTx2cMN90AyYe3cU4oA8jBxZIQ== dependencies: "@apollographql/graphql-playground-html" "1.6.26" "@types/accepts" "^1.3.5" "@types/body-parser" "1.19.0" - "@types/cors" "^2.8.4" + "@types/cors" "2.8.8" "@types/express" "4.17.7" + "@types/express-serve-static-core" "4.17.18" accepts "^1.3.5" - apollo-server-core "^2.16.1" - apollo-server-types "^0.5.1" + apollo-server-core "^2.21.0" + apollo-server-types "^0.6.3" body-parser "^1.18.3" cors "^2.8.4" express "^4.17.1" graphql-subscriptions "^1.0.0" - graphql-tools "^4.0.0" + graphql-tools "^4.0.8" parseurl "^1.3.2" subscriptions-transport-ws "^0.9.16" type-is "^1.6.16" -apollo-server-plugin-base@^0.9.1: - version "0.9.1" - resolved "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.9.1.tgz#a62ae9ab4e89790fd4cc5d123bb616da34e8e5fb" - integrity sha512-kvrX4Z3FdpjrZdHkyl5iY2A1Wvp4b6KQp00DeZqss7GyyKNUBKr80/7RQgBLEw7EWM7WB19j459xM/TjvW0FKQ== +apollo-server-plugin-base@^0.10.4: + version "0.10.4" + resolved "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.10.4.tgz#fbf73f64f95537ca9f9639dd7c535eb5eeb95dcd" + integrity sha512-HRhbyHgHFTLP0ImubQObYhSgpmVH4Rk1BinnceZmwudIVLKrqayIVOELdyext/QnSmmzg5W7vF3NLGBcVGMqDg== dependencies: - apollo-server-types "^0.5.1" + apollo-server-types "^0.6.3" -apollo-server-types@^0.5.1: - version "0.5.1" - resolved "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.5.1.tgz#091c09652894d6532db9ba873574443adabf85b9" - integrity sha512-my2cPw+DAb2qVnIuBcsRKGyS28uIc2vjFxa1NpRoJZe9gK0BWUBk7wzXnIzWy3HZ5Er11e/40MPTUesNfMYNVA== +apollo-server-types@^0.6.3: + version "0.6.3" + resolved "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.6.3.tgz#f7aa25ff7157863264d01a77d7934aa6e13399e8" + integrity sha512-aVR7SlSGGY41E1f11YYz5bvwA89uGmkVUtzMiklDhZ7IgRJhysT5Dflt5IuwDxp+NdQkIhVCErUXakopocFLAg== dependencies: - apollo-engine-reporting-protobuf "^0.5.2" - apollo-server-caching "^0.5.2" - apollo-server-env "^2.4.5" + apollo-reporting-protobuf "^0.6.2" + apollo-server-caching "^0.5.3" + apollo-server-env "^3.0.0" apollo-server@^2.16.1: version "2.16.1" @@ -7712,13 +7681,13 @@ apollo-server@^2.16.1: graphql-subscriptions "^1.0.0" graphql-tools "^4.0.0" -apollo-tracing@^0.11.1: - version "0.11.2" - resolved "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.11.2.tgz#14308b176e021f5e6ec3ee670f8f96e9fbfdb50c" - integrity sha512-QjmRd2ozGD+PfmF6U9w/w6jrclYSBNczN6Bzppr8qA5somEGl5pqdprIZYL28H0IapZiutA3x6p6ZVF/cVX8wA== +apollo-tracing@^0.12.2: + version "0.12.2" + resolved "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.12.2.tgz#a261c3970bb421b6dadf50cd85d75b2567a7e52c" + integrity sha512-SYN4o0C0wR1fyS3+P0FthyvsQVHFopdmN3IU64IaspR/RZScPxZ3Ae8uu++fTvkQflAkglnFM0aX6DkZERBp6w== dependencies: - apollo-server-env "^2.4.5" - apollo-server-plugin-base "^0.9.1" + apollo-server-env "^3.0.0" + apollo-server-plugin-base "^0.10.4" apollo-upload-client@^13.0.0: version "13.0.0" @@ -7777,9 +7746,9 @@ archiver-utils@^2.1.0: readable-stream "^2.0.0" archiver@^5.0.2: - version "5.1.0" - resolved "https://registry.npmjs.org/archiver/-/archiver-5.1.0.tgz#05b0f6f7836f3e6356a0532763d2bb91017a7e37" - integrity sha512-iKuQUP1nuKzBC2PFlGet5twENzCfyODmvkxwDV0cEFXavwcLrIW5ssTuHi9dyTPvpWr6Faweo2eQaQiLIwyXTA== + version "5.2.0" + resolved "https://registry.npmjs.org/archiver/-/archiver-5.2.0.tgz#25aa1b3d9febf7aec5b0f296e77e69960c26db94" + integrity sha512-QEAKlgQuAtUxKeZB9w5/ggKXh21bZS+dzzuQ0RPBC20qtDCbTyzqmisoeJP46MP39fg4B4IcyvR+yeyEBdblsQ== dependencies: archiver-utils "^2.1.0" async "^3.2.0" @@ -10381,6 +10350,11 @@ create-react-context@0.3.0: gud "^1.0.0" warning "^4.0.3" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-env@^7.0.0: version "7.0.3" resolved "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz#865264b29677dc015ba8418918965dd232fc54cf" @@ -12986,6 +12960,14 @@ file-loader@^6.0.0: loader-utils "^2.0.0" schema-utils "^2.7.1" +file-loader@^6.2.0: + version "6.2.0" + resolved "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz#baef7cf8e1840df325e4390b4484879480eebe4d" + integrity sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw== + dependencies: + loader-utils "^2.0.0" + schema-utils "^3.0.0" + file-system-cache@^1.0.5: version "1.0.5" resolved "https://registry.npmjs.org/file-system-cache/-/file-system-cache-1.0.5.tgz#84259b36a2bbb8d3d6eb1021d3132ffe64cfff4f" @@ -13335,7 +13317,7 @@ fs-extra@8.1.0, fs-extra@^8.1.0: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@9.0.1, fs-extra@^9.0.0, fs-extra@^9.0.1: +fs-extra@9.0.1: version "9.0.1" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== @@ -13365,7 +13347,7 @@ fs-extra@^7.0.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.1.0: +fs-extra@^9.0.0, fs-extra@^9.0.1, fs-extra@^9.1.0: version "9.1.0" resolved "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== @@ -14054,14 +14036,14 @@ graphql-config@^3.0.2: string-env-interpolation "1.0.1" tslib "^2.0.0" -graphql-extensions@^0.12.4: - version "0.12.4" - resolved "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.12.4.tgz#c0aa49a20f983a2da641526d1e505996bd2b4188" - integrity sha512-GnR4LiWk3s2bGOqIh6V1JgnSXw2RCH4NOgbCFEWvB6JqWHXTlXnLZ8bRSkCiD4pltv7RHUPWqN/sGh8R6Ae/ag== +graphql-extensions@^0.12.8: + version "0.12.8" + resolved "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.12.8.tgz#9cdc2c43d8fe5e0f6c3177a004ac011da2a8aa0f" + integrity sha512-xjsSaB6yKt9jarFNNdivl2VOx52WySYhxPgf8Y16g6GKZyAzBoIFiwyGw5PJDlOSUa6cpmzn6o7z8fVMbSAbkg== dependencies: "@apollographql/apollo-tools" "^0.4.3" - apollo-server-env "^2.4.5" - apollo-server-types "^0.5.1" + apollo-server-env "^3.0.0" + apollo-server-types "^0.6.3" graphql-language-service-interface@^2.4.0: version "2.4.0" @@ -14119,11 +14101,6 @@ graphql-tag@^2.11.0: resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.11.0.tgz#1deb53a01c46a7eb401d6cb59dec86fa1cccbffd" integrity sha512-VmsD5pJqWJnQZMUeRwrDhfgoyqcfwEkvtpANqcoUG8/tOLkwNgU9mzub/Mc78OJMhHjx7gfAMTxzdG43VGg3bA== -graphql-tag@^2.9.2: - version "2.10.4" - resolved "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.4.tgz#2f301a98219be8b178a6453bb7e33b79b66d8f83" - integrity sha512-O7vG5BT3w6Sotc26ybcvLKNTdfr4GfsIVMD+LdYqXCeJIYPRyp8BIsDOUtxw7S1PYvRw5vH3278J2EDezR6mfA== - graphql-tools@5.0.0: version "5.0.0" resolved "https://registry.npmjs.org/graphql-tools/-/graphql-tools-5.0.0.tgz#67281c834a0e29f458adba8018f424816fa627e9" @@ -14138,7 +14115,7 @@ graphql-tools@5.0.0: tslib "^1.11.1" uuid "^7.0.3" -graphql-tools@^4.0.0: +graphql-tools@^4.0.0, graphql-tools@^4.0.8: version "4.0.8" resolved "https://registry.npmjs.org/graphql-tools/-/graphql-tools-4.0.8.tgz#e7fb9f0d43408fb0878ba66b522ce871bafe9d30" integrity sha512-MW+ioleBrwhRjalKjYaLQbr+920pHBgy9vM/n47sswtns8+96sRn5M/G+J1eu7IMeKWiN/9p6tmwCHU7552VJg== @@ -14154,27 +14131,10 @@ graphql-type-json@^0.3.2: resolved "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115" integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg== -graphql-upload@^8.0.2: - version "8.1.0" - resolved "https://registry.npmjs.org/graphql-upload/-/graphql-upload-8.1.0.tgz#6d0ab662db5677a68bfb1f2c870ab2544c14939a" - integrity sha512-U2OiDI5VxYmzRKw0Z2dmfk0zkqMRaecH9Smh1U277gVgVe9Qn+18xqf4skwr4YJszGIh7iQDZ57+5ygOK9sM/Q== - dependencies: - busboy "^0.3.1" - fs-capacitor "^2.0.4" - http-errors "^1.7.3" - object-path "^0.11.4" - -graphql@15.4.0, graphql@^15.3.0: - version "15.4.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-15.4.0.tgz#e459dea1150da5a106486ba7276518b5295a4347" - integrity sha512-EB3zgGchcabbsU9cFe1j+yxdzKQKAbGUWRb13DsrsMN1yyfmmIq+2+L5MqVWcDCE4V89R5AyUOi7sMOGxdsYtA== - -graphql@^14.5.3: - version "14.7.0" - resolved "https://registry.npmjs.org/graphql/-/graphql-14.7.0.tgz#7fa79a80a69be4a31c27dda824dc04dac2035a72" - integrity sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA== - dependencies: - iterall "^1.2.2" +graphql@15.5.0, graphql@^15.3.0: + version "15.5.0" + resolved "https://registry.npmjs.org/graphql/-/graphql-15.5.0.tgz#39d19494dbe69d1ea719915b578bf920344a69d5" + integrity sha512-OmaM7y0kaK31NKG31q4YbD2beNYa6jBBKtMFT6gLYJljHLJr42IqJ8KX08u3Li/0ifzTU5HjmoOOrwa5BRLeDA== growly@^1.3.0: version "1.3.0" @@ -14722,6 +14682,11 @@ human-signals@^2.1.0: resolved "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz#dc91fcba42e4d06e4abaed33b3e7a3c02f514ea0" integrity sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw== +humanize-duration@^3.25.1: + version "3.25.1" + resolved "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.25.1.tgz#50e12bf4b3f515ec91106107ee981e8cfe955d6f" + integrity sha512-P+dRo48gpLgc2R9tMRgiDRNULPKCmqFYgguwqOO2C0fjO35TgdURDQDANSR1Nt92iHlbHGMxOTnsB8H8xnMa2Q== + humanize-ms@^1.2.1: version "1.2.1" resolved "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" @@ -15739,7 +15704,7 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -iterall@^1.1.3, iterall@^1.2.1, iterall@^1.2.2, iterall@^1.3.0: +iterall@^1.1.3, iterall@^1.2.1, iterall@^1.3.0: version "1.3.0" resolved "https://registry.npmjs.org/iterall/-/iterall-1.3.0.tgz#afcb08492e2915cbd8a0884eb93a8c94d0d72fea" integrity sha512-QZ9qOMdF+QLHxy1QIpUHUU1D5pS2CG2P69LF6L6CPjPYA/XMOmKV3PZpawHoAjHNyB0swdVTRxdYT4tbBbxqwg== @@ -15906,14 +15871,6 @@ jest-environment-node@^26.6.2: jest-mock "^26.6.2" jest-util "^26.6.2" -jest-esm-transformer@^1.0.0: - version "1.0.0" - resolved "https://registry.npmjs.org/jest-esm-transformer/-/jest-esm-transformer-1.0.0.tgz#b6c58f496aa48194f96361a52f5c578fd2209726" - integrity sha512-FoPgeMMwy1/CEsc8tBI41i83CEO3x85RJuZi5iAMmWoARXhfgk6Jd7y+4d+z+HCkTKNVDvSWKGRhwjzU9PUbrw== - dependencies: - "@babel/core" "^7.4.4" - "@babel/plugin-transform-modules-commonjs" "^7.4.4" - jest-get-type@^24.9.0: version "24.9.0" resolved "https://registry.npmjs.org/jest-get-type/-/jest-get-type-24.9.0.tgz#1684a0c8a50f2e4901b6644ae861f579eed2ef0e" @@ -17530,6 +17487,11 @@ luxon@1.25.0, luxon@^1.25.0: resolved "https://registry.npmjs.org/luxon/-/luxon-1.25.0.tgz#d86219e90bc0102c0eb299d65b2f5e95efe1fe72" integrity sha512-hEgLurSH8kQRjY6i4YLey+mcKVAWXbDNlZRmM6AgWDJ1cY3atl8Ztf5wEY7VBReFbmGnwQPz7KYJblL8B2k0jQ== +luxon@^1.26.0: + version "1.26.0" + resolved "https://registry.npmjs.org/luxon/-/luxon-1.26.0.tgz#d3692361fda51473948252061d0f8561df02b578" + integrity sha512-+V5QIQ5f6CDXQpWNICELwjwuHdqeJM1UenlZWx5ujcRMc9venvluCjFb4t5NYLhb6IhkbMVOxzVuOqkgMxee2A== + macos-release@^2.2.0: version "2.3.0" resolved "https://registry.npmjs.org/macos-release/-/macos-release-2.3.0.tgz#eb1930b036c0800adebccd5f17bc4c12de8bb71f" @@ -18021,7 +17983,7 @@ mime-db@1.44.0, "mime-db@>= 1.43.0 < 2": resolved "https://registry.npmjs.org/mime-db/-/mime-db-1.44.0.tgz#fa11c5eb0aca1334b4233cb4d52f10c5a6272f92" integrity sha512-/NOTfLrsPBVeH7YtFPgsVWveuL+4SjjYxaQ1xtM1KMFj7HdxlBlxeyNLzhyJVx7r4rZGJAZ/6lkKCitSc/Nmpg== -mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.26, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: +mime-types@^2.0.8, mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.17, mime-types@~2.1.19, mime-types@~2.1.24: version "2.1.27" resolved "https://registry.npmjs.org/mime-types/-/mime-types-2.1.27.tgz#47949f98e279ea53119f5722e0f34e529bec009f" integrity sha512-JIhqnCasI9yD+SsmkquHBxTSEuZdQX5BuQnS2Vc7puQQQ+8yiP5AY5uWhpdv4YL4VM5c6iliiYWPgJ/nJQLp7w== @@ -18297,7 +18259,7 @@ modify-values@^1.0.0: resolved "https://registry.npmjs.org/modify-values/-/modify-values-1.0.1.tgz#b3939fa605546474e3e3e3c63d64bd43b4ee6022" integrity sha512-xV2bxeN6F7oYjZWTe/YPAy6MN2M+sL4u/Rlm2AHCIVGfo2p1yGmBHQ6vHehl4bRTZBdHu3TSkWdYgkwpYzAGSw== -moment@^2.19.3, moment@^2.25.3, moment@^2.26.0, moment@^2.27.0: +moment@^2.19.3, moment@^2.25.3, moment@^2.27.0: version "2.29.1" resolved "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz#b2be769fa31940be9eeea6469c075e35006fa3d3" integrity sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== @@ -20939,7 +20901,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.5.10, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.5.7, prop-types@^15.5.8, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -21673,6 +21635,13 @@ react-test-renderer@^16.13.1: react-is "^16.8.6" scheduler "^0.19.1" +react-text-truncate@^0.16.0: + version "0.16.0" + resolved "https://registry.npmjs.org/react-text-truncate/-/react-text-truncate-0.16.0.tgz#03bbb942437dfba5cc0faf614f1339f888926f80" + integrity sha512-hMFXhUHgIBCCDaOfOsZAFeO4DGfG/paLyaS/F+X11CXseuScpxmMBUW6Luwjk9/FlGrVJWNGy1FSfK6b4yyiIg== + dependencies: + prop-types "^15.5.7" + react-textarea-autosize@^8.1.1: version "8.2.0" resolved "https://registry.npmjs.org/react-textarea-autosize/-/react-textarea-autosize-8.2.0.tgz#fae38653f5ec172a855fd5fffb39e466d56aebdb" @@ -21724,10 +21693,10 @@ react-use@^12.2.0: ts-easing "^0.2.0" tslib "^1.10.0" -react-use@^15.3.3: - version "15.3.4" - resolved "https://registry.npmjs.org/react-use/-/react-use-15.3.4.tgz#f853d310bd71f75b38900a8caa3db93f6dc6e872" - integrity sha512-cHq1dELW6122oi1+xX7lwNyE/ugZs5L902BuO8eFJCfn2api1KeuPVG1M/GJouVARoUf54S2dYFMKo5nQXdTag== +react-use@^15.3.3, react-use@^15.3.6: + version "15.3.8" + resolved "https://registry.npmjs.org/react-use/-/react-use-15.3.8.tgz#ca839ac7fb3d696e5ccbeabbc8dadc2698969d30" + integrity sha512-GeGcrmGuUvZrY5wER3Lnph9DSYhZt5nEjped4eKDq8BRGr2CnLf9bDQWG9RFc7oCPphnscUUdOovzq0E5F2c6Q== dependencies: "@types/js-cookie" "2.2.6" "@xobotyi/scrollbar-width" "1.9.5" @@ -22805,6 +22774,15 @@ schema-utils@^2.6.5, schema-utils@^2.6.6, schema-utils@^2.7.0, schema-utils@^2.7 ajv "^6.12.4" ajv-keywords "^3.5.2" +schema-utils@^3.0.0: + version "3.0.0" + resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz#67502f6aa2b66a2d4032b4279a2944978a0913ef" + integrity sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA== + dependencies: + "@types/json-schema" "^7.0.6" + ajv "^6.12.5" + ajv-keywords "^3.5.2" + screenfull@^5.0.0: version "5.0.2" resolved "https://registry.npmjs.org/screenfull/-/screenfull-5.0.2.tgz#b9acdcf1ec676a948674df5cd0ff66b902b0bed7" @@ -23996,7 +23974,7 @@ stylis@3.5.0: resolved "https://registry.npmjs.org/stylis/-/stylis-3.5.0.tgz#016fa239663d77f868fef5b67cf201c4b7c701e1" integrity sha512-pP7yXN6dwMzAR29Q0mBrabPCe0/mNO1MSr93bhay+hcZondvMMTpeGyd8nbhYJdyperNT2DRxONQuUGcJr5iPw== -subscriptions-transport-ws@0.9.18: +subscriptions-transport-ws@0.9.18, subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.16: version "0.9.18" resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.18.tgz#bcf02320c911fbadb054f7f928e51c6041a37b97" integrity sha512-tztzcBTNoEbuErsVQpTN2xUNN/efAZXyCyL5m3x4t6SKrEiTL2N8SaKWBFWM4u56pL79ULif3zjyeq+oV+nOaA== @@ -24007,17 +23985,6 @@ subscriptions-transport-ws@0.9.18: symbol-observable "^1.0.4" ws "^5.2.0" -subscriptions-transport-ws@^0.9.11, subscriptions-transport-ws@^0.9.16: - version "0.9.17" - resolved "https://registry.npmjs.org/subscriptions-transport-ws/-/subscriptions-transport-ws-0.9.17.tgz#e30e40f0caae0d2781903c01a8cb51b6e2682098" - integrity sha512-hNHi2N80PBz4T0V0QhnnsMGvG3XDFDS9mS6BhZ3R12T6EBywC8d/uJscsga0cVO4DKtXCkCRrWm2sOYrbOdhEA== - dependencies: - backo2 "^1.0.2" - eventemitter3 "^3.1.0" - iterall "^1.2.1" - symbol-observable "^1.0.4" - ws "^5.2.0" - sucrase@^3.16.0: version "3.17.0" resolved "https://registry.npmjs.org/sucrase/-/sucrase-3.17.0.tgz#d9fe5d7e359d884cdb31130358fbdfc18bfb4c24" @@ -24821,6 +24788,18 @@ ts-node@^8.10.2, ts-node@^8.6.2: source-map-support "^0.5.17" yn "3.1.1" +ts-node@^9.1.1: + version "9.1.1" + resolved "https://registry.npmjs.org/ts-node/-/ts-node-9.1.1.tgz#51a9a450a3e959401bda5f004a72d54b936d376d" + integrity sha512-hPlt7ZACERQGf03M253ytLY3dHbGNGrAq9qIHWUY9XHYl1z7wYngSr3OQ5xmui8o2AaxsONxIzjafLUiWBo1Fg== + dependencies: + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + source-map-support "^0.5.17" + yn "3.1.1" + ts-pnp@^1.1.6: version "1.2.0" resolved "https://registry.npmjs.org/ts-pnp/-/ts-pnp-1.2.0.tgz#a500ad084b0798f1c3071af391e65912c86bca92" @@ -24995,14 +24974,15 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript-json-schema@^0.47.0: - version "0.47.0" - resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.47.0.tgz#84dde5460b127c6774da81bf70b23c7e04857b13" - integrity sha512-A6NVwSOTSsNDHfaqDcDeKwwyXEeKqBHoAr20jcetnYj4e8C6zVFofAVhAuwsBXCRYiWEE/lyHrcxpsSpbIk0Mg== +typescript-json-schema@^0.49.0: + version "0.49.0" + resolved "https://registry.npmjs.org/typescript-json-schema/-/typescript-json-schema-0.49.0.tgz#442f6347ca85fb0d9811f217fb0d6537b68734b3" + integrity sha512-PumZkTmEE3T8TVyoJU6ZCp3K6VCmCb3Ei6fUaRIuDsIzYtmdJc6jV1D0RyBe5sd5mJ1iB6Zckm4KAKbqXs9oDw== dependencies: "@types/json-schema" "^7.0.6" glob "^7.1.6" json-stable-stringify "^1.0.1" + ts-node "^9.1.1" typescript "^4.1.3" yargs "^16.2.0" @@ -25318,13 +25298,13 @@ urix@^0.1.0: integrity sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI= url-loader@^4.0.0, url-loader@^4.1.0: - version "4.1.0" - resolved "https://registry.npmjs.org/url-loader/-/url-loader-4.1.0.tgz#c7d6b0d6b0fccd51ab3ffc58a78d32b8d89a7be2" - integrity sha512-IzgAAIC8wRrg6NYkFIJY09vtktQcsvU8V6HhtQj9PTefbYImzLB1hufqo4m+RyM5N3mLx5BqJKccgxJS+W3kqw== + version "4.1.1" + resolved "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz#28505e905cae158cf07c92ca622d7f237e70a4e2" + integrity sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA== dependencies: loader-utils "^2.0.0" - mime-types "^2.1.26" - schema-utils "^2.6.5" + mime-types "^2.1.27" + schema-utils "^3.0.0" url-parse-lax@^3.0.0: version "3.0.0" @@ -25364,6 +25344,11 @@ use-composed-ref@^1.0.0: dependencies: ts-essentials "^2.0.3" +use-immer@^0.4.2: + version "0.4.2" + resolved "https://registry.npmjs.org/use-immer/-/use-immer-0.4.2.tgz#7d7e7d3386cc834d45b2279f37d8974023550f4f" + integrity sha512-ONfZHEv/gzt/jyYxrJD3ZFUllKJED8F1mds9Fr9CYj54LmsiuGDg3vkx+R7WHS72p7DbSS1QbM7xNYrVWX+KcA== + use-isomorphic-layout-effect@^1.0.0: version "1.0.0" resolved "https://registry.npmjs.org/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.0.0.tgz#f56b4ed633e1c21cd9fc76fe249002a1c28989fb"