-
-);
+
+ );
+};
export default Plugins;
diff --git a/microsite/src/pages/plugins/plugins.module.scss b/microsite/src/pages/plugins/plugins.module.scss
index bb08c2fd33..d5ea5de6e7 100644
--- a/microsite/src/pages/plugins/plugins.module.scss
+++ b/microsite/src/pages/plugins/plugins.module.scss
@@ -28,10 +28,60 @@
:global(.pluginsContainer) {
gap: 1rem;
display: grid;
- grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ grid-template-columns: repeat(auto-fit, minmax(250px, 300px));
+ }
+
+ @media (max-width: 768px) {
+ :global(.pluginsContainer) {
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
+ }
+ }
+
+ :global(.hidePluginsHeader) {
+ display: none;
}
:global(.fit-content) {
width: fit-content;
}
+
+ :global(.pluginsFilterBox) {
+ width: 100%;
+ margin-bottom: 1rem;
+ height: 1rem;
+ }
+
+ :global(.dropdown) {
+ float: right;
+ margin-bottom: 1rem;
+ }
+
+ :global(.button--info) {
+ background-color: transparent;
+ border-color: var(--ifm-color-primary);
+ color: var(--ifm-color-primary);
+ min-width: 250px;
+ }
+
+ :global(label.dropdown__item) {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ margin-top: 5px;
+
+ &:hover {
+ background-color: var(--ifm-color-primary);
+ color: black;
+ }
+ }
+
+ :global(.dropdown__label) {
+ font-size: 15px;
+ cursor: pointer;
+ }
+
+ :global(.dropdown__checkbox) {
+ margin-right: 10px;
+ cursor: pointer;
+ }
}
diff --git a/microsite/src/util/types.ts b/microsite/src/util/types.ts
new file mode 100644
index 0000000000..fba7c0ba46
--- /dev/null
+++ b/microsite/src/util/types.ts
@@ -0,0 +1,4 @@
+export type ChipCategory = {
+ name: string;
+ isSelected: boolean;
+};
diff --git a/microsite/yarn.lock b/microsite/yarn.lock
index 10028fc4b4..7097fb337e 100644
--- a/microsite/yarn.lock
+++ b/microsite/yarn.lock
@@ -1620,9 +1620,9 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/core@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/core@npm:0.0.0-5566"
+"@docusaurus/core@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/core@npm:0.0.0-5580"
dependencies:
"@babel/core": ^7.20.12
"@babel/generator": ^7.21.1
@@ -1634,13 +1634,13 @@ __metadata:
"@babel/runtime": ^7.21.0
"@babel/runtime-corejs3": ^7.21.0
"@babel/traverse": ^7.21.2
- "@docusaurus/cssnano-preset": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
+ "@docusaurus/cssnano-preset": 0.0.0-5580
+ "@docusaurus/logger": 0.0.0-5580
+ "@docusaurus/mdx-loader": 0.0.0-5580
"@docusaurus/react-loadable": 5.5.2
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-common": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
"@slorber/static-site-generator-webpack-plugin": ^4.0.7
"@svgr/webpack": ^6.5.1
autoprefixer: ^10.4.13
@@ -1700,41 +1700,41 @@ __metadata:
react-dom: ^16.8.4 || ^17.0.0
bin:
docusaurus: bin/docusaurus.mjs
- checksum: c85ea103cd3a14dc340533d1cad0627736d87e4336b5919adabd23627958eed108fddb2dd45066944185ebe52a8648a4463a9587804f3a537f0a8deaea3e835e
+ checksum: 1160559109aa834bcd3dc0210ca824ad6cbdea363951c5cba690c1882c78a7da6e1e52b8a5c35fef3bfc23f9f66e84880c354727b7987d371cac8ec6fb77d1bb
languageName: node
linkType: hard
-"@docusaurus/cssnano-preset@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/cssnano-preset@npm:0.0.0-5566"
+"@docusaurus/cssnano-preset@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/cssnano-preset@npm:0.0.0-5580"
dependencies:
cssnano-preset-advanced: ^5.3.10
postcss: ^8.4.21
postcss-sort-media-queries: ^4.3.0
tslib: ^2.5.0
- checksum: 9aece7f723e7bdeecaa0cc9ce956ef590dfd1c11200f9799236ce80ad94f4653c717a0f16d8694a9f635fb21e0e5bbf3093ba59f8996c012b71062560d9e0fd3
+ checksum: 4a46af1e9327d6ebbed3f0f6e60cc1ca59d5c07d39be35c42c13b0a64795b0b1565585adfca063798be79100aca425ee0c12f28a816b56686fb97726bec00f40
languageName: node
linkType: hard
-"@docusaurus/logger@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/logger@npm:0.0.0-5566"
+"@docusaurus/logger@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/logger@npm:0.0.0-5580"
dependencies:
chalk: ^4.1.2
tslib: ^2.5.0
- checksum: aab288cf6f7c062bb54de296723bbd8deea3029267c49e3f2fb6ab86b462256c4cff805debfd93bd3b35daaefdb9f5bb1a07876434e0b4887e0ec6cf159594d0
+ checksum: 5792b6672623b70d38871713c47ba88cc0df6c8d224c28ac58d0512b8bf1b672786cc2fa0b1a79cc045c473e1e14b5a9cce4b01e623dd252893294d6aff9d1c2
languageName: node
linkType: hard
-"@docusaurus/mdx-loader@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/mdx-loader@npm:0.0.0-5566"
+"@docusaurus/mdx-loader@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/mdx-loader@npm:0.0.0-5580"
dependencies:
"@babel/parser": ^7.21.2
"@babel/traverse": ^7.21.2
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/logger": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
"@mdx-js/mdx": ^2.1.5
escape-html: ^1.0.3
estree-util-value-to-estree: ^2.1.0
@@ -1744,6 +1744,7 @@ __metadata:
image-size: ^1.0.2
mdast-util-mdx: ^2.0.0
mdast-util-to-string: ^3.0.0
+ rehype-raw: ^6.1.1
remark-comment: ^1.0.0
remark-directive: ^2.0.1
remark-emoji: ^2.2.0
@@ -1757,16 +1758,16 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: d4e82a3bdf8eefe7c3996c9ac3860a60f6bb028ab60dce31cef199d22da606631e7eacfeacd4e51c0181d3649026e081c353dea5f01317ec05b48c20b4e8cc39
+ checksum: b32812e0bca91a29761fda742a790f67077bb7e8860e20e83f009e297ca49017192fa14051ab82f1a3e66800de9e5a0b561b32ce2dffb4eecc0aee48116d3490
languageName: node
linkType: hard
-"@docusaurus/module-type-aliases@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/module-type-aliases@npm:0.0.0-5566"
+"@docusaurus/module-type-aliases@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/module-type-aliases@npm:0.0.0-5580"
dependencies:
"@docusaurus/react-loadable": 5.5.2
- "@docusaurus/types": 0.0.0-5566
+ "@docusaurus/types": 0.0.0-5580
"@types/history": ^4.7.11
"@types/react": "*"
"@types/react-router-config": "*"
@@ -1776,19 +1777,19 @@ __metadata:
peerDependencies:
react: "*"
react-dom: "*"
- checksum: 6f8a86386c54829f41e28d01a7ffe96b2a3211aaa9fd217f614154726620ce118a0a9bba5563b512bdbc4d3e4a03e1d73848caadbd7fe2b55e59e4ff1ae3633a
+ checksum: b5b3544f2f2d395c1d406a1b3f03f719aee3aa3fd7603c54af3b09c4fb8bb153cf2ee6697bc9cfc9fb2550a11a3e474a52c9f2c132924e2be60662ffc6379ace
languageName: node
linkType: hard
-"@docusaurus/plugin-client-redirects@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-client-redirects@npm:0.0.0-5566"
+"@docusaurus/plugin-client-redirects@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-client-redirects@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/logger": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-common": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
eta: ^2.0.1
fs-extra: ^11.1.0
lodash: ^4.17.21
@@ -1796,21 +1797,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 01976bc176541b7506680f56f74f2f5cf8d128f33eb6151bdd3bf09795a5ec5161356f4d7711c1fb63e7617fbdd2c6d08a4243870144b04fc1c02b67a98bdcb6
+ checksum: 7414d2026446862505d0c679ad3757107f28100ea76309d2a024ebe78865f2fcf3e3b85c3117361142e319362b5a5b22e6bce968763fdfd0741b6a60d2eb3f4b
languageName: node
linkType: hard
-"@docusaurus/plugin-content-blog@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-content-blog@npm:0.0.0-5566"
+"@docusaurus/plugin-content-blog@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-content-blog@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/logger": 0.0.0-5580
+ "@docusaurus/mdx-loader": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-common": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
cheerio: ^1.0.0-rc.12
feed: ^4.2.2
fs-extra: ^11.1.0
@@ -1823,21 +1824,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 1d1dfaf61c98de3e5722d1ea726dae29b75eb8747600583347aaca1dfe1ba0cb3b023d1af02ab8aac6e8750288ecd7f5b90bde5e5a693a822c874f0c053a24ea
+ checksum: 0c5d37fa84298ce12b2a0f5c1dea1ee7d72d4b28c545fd081f3c3e5f93f15a980d877dcf88718aa1f4b8a03abf3d0c8c0df10d0a2ef22aac7a8a96d2786f91c2
languageName: node
linkType: hard
-"@docusaurus/plugin-content-docs@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-content-docs@npm:0.0.0-5566"
+"@docusaurus/plugin-content-docs@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-content-docs@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/module-type-aliases": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/logger": 0.0.0-5580
+ "@docusaurus/mdx-loader": 0.0.0-5580
+ "@docusaurus/module-type-aliases": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
"@types/react-router-config": ^5.0.6
combine-promises: ^1.1.0
fs-extra: ^11.1.0
@@ -1850,133 +1851,133 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 32bb9c8c82ff4dfb9639b8fabda4d7b3c69c7a4a551719dba7495be8c7baf2ad96cb79b241904def5bab33f38c4e84a01d870f7200fe5c4eae74d380abaa6c60
+ checksum: fa58801bf0cebf055ed11adbadc3f27f7ed0ddb95174ab60bab76a21f759dc7abdfdd15f32141f7e259090c456634ee2556ac9a7daaaf7f5f6e3666d9f247d00
languageName: node
linkType: hard
-"@docusaurus/plugin-content-pages@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-content-pages@npm:0.0.0-5566"
+"@docusaurus/plugin-content-pages@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-content-pages@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/mdx-loader": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
fs-extra: ^11.1.0
tslib: ^2.5.0
webpack: ^5.76.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 6764c2d922d308305b0f71866f951b05a9cf885ff758e8982f865d8a52d77295802ae0d35cad20518b45a6a4daf7fbfbe2728fd5e078fdcc5104a4f21de0134f
+ checksum: 42bf814d9bf5540b8b2f8045f1577c49d3fca4a85069865695ff5dd9fa3f935283f9d46f920c4dd0a28190cf3b342d432b0985636bae9f05280835f73aec8c87
languageName: node
linkType: hard
-"@docusaurus/plugin-debug@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-debug@npm:0.0.0-5566"
+"@docusaurus/plugin-debug@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-debug@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
fs-extra: ^11.1.0
react-json-view: ^1.21.3
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 32a996e40cc58ebc2fd3cbfb9f6d33b7303952788d9c9b2f41f36b54a327939807e990784191f6f3a6169a4c56c7dabec2f7b369828d86f86414e32c3ac39dfe
+ checksum: dc58659adab3c859daa2e0dfc0b5b588d490a4d2bb313dcfdc9e92d559d0c4145c686b001805ad44a1f2a0f55f87524637a34844a4438cfeec479a2c24ec1c4d
languageName: node
linkType: hard
-"@docusaurus/plugin-google-analytics@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-google-analytics@npm:0.0.0-5566"
+"@docusaurus/plugin-google-analytics@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-google-analytics@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: a1549324beb15024b3cfc6ebc0fb1ef7017f28d4a7419c14d469cc24025a53e460bceeed7401e01900b530e06ec84c302de891aea165ed5c3885261eb8eccb12
+ checksum: f66a737a2b8b08648f28a279784e527fff34e2dba6f3022ec83b01a7f1781bfa0f208fac1db1fc919c1a01d405755c0816304cf4912e7b434b3924d5a968e6b0
languageName: node
linkType: hard
-"@docusaurus/plugin-google-gtag@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-google-gtag@npm:0.0.0-5566"
+"@docusaurus/plugin-google-gtag@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-google-gtag@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
"@types/gtag.js": ^0.0.12
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 3e98f82b2e2767205fc3d40673e6db5b188d2cd4c3ff83d7f6ff7588b0e2486a6d48c1a6ef37bae6a1fafb77309bbc4000eb2784bd4f01df21deb23235f13884
+ checksum: 6b05e7c2a8e4d757660acca1131a0e3abfd21c5544a31e93bd08ff9c75deb5f3b131ed0e47283bbc2f102e00947ab7a64ee4d5a53f300e98fc82ec13c4a6eda1
languageName: node
linkType: hard
-"@docusaurus/plugin-google-tag-manager@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-google-tag-manager@npm:0.0.0-5566"
+"@docusaurus/plugin-google-tag-manager@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-google-tag-manager@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 6ea3e845ad5bab9cec34947e851e3adbf27c609227ad4dec8c4bad6ff05ab2190028dea78a4c0d844b6c377357189d9b739ca83c330d8f46dfe25e48ee61037a
+ checksum: 8904b150b2faa05dc49fd957ca8c392605e53073d031dfedb098be2abae924c3faf4f5e26ecfea91fe452695699801de9882a85a4371a1a35ca05fd57de232ae
languageName: node
linkType: hard
-"@docusaurus/plugin-sitemap@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/plugin-sitemap@npm:0.0.0-5566"
+"@docusaurus/plugin-sitemap@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/plugin-sitemap@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/logger": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-common": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
fs-extra: ^11.1.0
sitemap: ^7.1.1
tslib: ^2.5.0
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 1ceb243d048aaadfd05aaa4bb02455d3e87281ba49c08ecaa2ceb58f2cfa94a52af55756e20c562a2fa0c01524396d754d116e6ecb0d7591c2718baeaa334688
+ checksum: 4243cf4ed649d8e9a29b122ce23f1a6194cc5d7fc244e33326320315d8ff2756b18b9d85a0f0d49df33b2e68224c1b747e87ad5fa9944e7572bc761f641c5b77
languageName: node
linkType: hard
-"@docusaurus/preset-classic@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/preset-classic@npm:0.0.0-5566"
+"@docusaurus/preset-classic@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/preset-classic@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/plugin-content-blog": 0.0.0-5566
- "@docusaurus/plugin-content-docs": 0.0.0-5566
- "@docusaurus/plugin-content-pages": 0.0.0-5566
- "@docusaurus/plugin-debug": 0.0.0-5566
- "@docusaurus/plugin-google-analytics": 0.0.0-5566
- "@docusaurus/plugin-google-gtag": 0.0.0-5566
- "@docusaurus/plugin-google-tag-manager": 0.0.0-5566
- "@docusaurus/plugin-sitemap": 0.0.0-5566
- "@docusaurus/theme-classic": 0.0.0-5566
- "@docusaurus/theme-common": 0.0.0-5566
- "@docusaurus/theme-search-algolia": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/plugin-content-blog": 0.0.0-5580
+ "@docusaurus/plugin-content-docs": 0.0.0-5580
+ "@docusaurus/plugin-content-pages": 0.0.0-5580
+ "@docusaurus/plugin-debug": 0.0.0-5580
+ "@docusaurus/plugin-google-analytics": 0.0.0-5580
+ "@docusaurus/plugin-google-gtag": 0.0.0-5580
+ "@docusaurus/plugin-google-tag-manager": 0.0.0-5580
+ "@docusaurus/plugin-sitemap": 0.0.0-5580
+ "@docusaurus/theme-classic": 0.0.0-5580
+ "@docusaurus/theme-common": 0.0.0-5580
+ "@docusaurus/theme-search-algolia": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: e7b1181b414f7795c540a4c9fed282ce350eaeee5006a9eb228cb42990d67ec933231f0af2f0329571ea135d87012a6ad606e9adaee61ae105573bbe6433a698
+ checksum: 3aa48aea40e33c2c7adcad54918e117b1f1d917eba110889d85a273e82b4bbf0d73d9e5bb8c5aaadd846ec341981bbe701014cbd93ddf55c83245ce1bc4a56a6
languageName: node
linkType: hard
@@ -1992,22 +1993,22 @@ __metadata:
languageName: node
linkType: hard
-"@docusaurus/theme-classic@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/theme-classic@npm:0.0.0-5566"
+"@docusaurus/theme-classic@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/theme-classic@npm:0.0.0-5580"
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/module-type-aliases": 0.0.0-5566
- "@docusaurus/plugin-content-blog": 0.0.0-5566
- "@docusaurus/plugin-content-docs": 0.0.0-5566
- "@docusaurus/plugin-content-pages": 0.0.0-5566
- "@docusaurus/theme-common": 0.0.0-5566
- "@docusaurus/theme-translations": 0.0.0-5566
- "@docusaurus/types": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/mdx-loader": 0.0.0-5580
+ "@docusaurus/module-type-aliases": 0.0.0-5580
+ "@docusaurus/plugin-content-blog": 0.0.0-5580
+ "@docusaurus/plugin-content-docs": 0.0.0-5580
+ "@docusaurus/plugin-content-pages": 0.0.0-5580
+ "@docusaurus/theme-common": 0.0.0-5580
+ "@docusaurus/theme-translations": 0.0.0-5580
+ "@docusaurus/types": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-common": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
"@mdx-js/react": ^2.1.5
clsx: ^1.2.1
copy-text-to-clipboard: ^3.0.1
@@ -2024,21 +2025,21 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 17ba0e0d5f27b0dfd700dd1c9536eae416268abc8d4b308cdf4015b2aff246153b4906e5aa292be58987ccc10b9fe0d0d3182bbba52e370c3116076787009206
+ checksum: 6a8f99d5143017d4754601fbf6487b697dc77945f730763917578abe0a1ef694607f888d824146aef86fdc2a620f0ddbd4925a23dcfc15b57d8338c1de73d526
languageName: node
linkType: hard
-"@docusaurus/theme-common@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/theme-common@npm:0.0.0-5566"
+"@docusaurus/theme-common@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/theme-common@npm:0.0.0-5580"
dependencies:
- "@docusaurus/mdx-loader": 0.0.0-5566
- "@docusaurus/module-type-aliases": 0.0.0-5566
- "@docusaurus/plugin-content-blog": 0.0.0-5566
- "@docusaurus/plugin-content-docs": 0.0.0-5566
- "@docusaurus/plugin-content-pages": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-common": 0.0.0-5566
+ "@docusaurus/mdx-loader": 0.0.0-5580
+ "@docusaurus/module-type-aliases": 0.0.0-5580
+ "@docusaurus/plugin-content-blog": 0.0.0-5580
+ "@docusaurus/plugin-content-docs": 0.0.0-5580
+ "@docusaurus/plugin-content-pages": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-common": 0.0.0-5580
"@types/history": ^4.7.11
"@types/react": "*"
"@types/react-router-config": "*"
@@ -2051,22 +2052,22 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 5f2978300b9a01c93d12e6fcd768259cab224207da15f6fd6889cc3e441fda61354e165f084f0353257e9c6d6af5184adb0f03ce7cf009845751657c3d52a00f
+ checksum: 99524cb36a520568f63140734daaea8d56b35c0c8de3c36cb0102a93a0714f31a68aa5302607b9113a68e1b9fb7d73fd53ac80aa82ce48bf6f2d9e304ce21da2
languageName: node
linkType: hard
-"@docusaurus/theme-search-algolia@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/theme-search-algolia@npm:0.0.0-5566"
+"@docusaurus/theme-search-algolia@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/theme-search-algolia@npm:0.0.0-5580"
dependencies:
"@docsearch/react": ^3.3.3
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/plugin-content-docs": 0.0.0-5566
- "@docusaurus/theme-common": 0.0.0-5566
- "@docusaurus/theme-translations": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
- "@docusaurus/utils-validation": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/logger": 0.0.0-5580
+ "@docusaurus/plugin-content-docs": 0.0.0-5580
+ "@docusaurus/theme-common": 0.0.0-5580
+ "@docusaurus/theme-translations": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
+ "@docusaurus/utils-validation": 0.0.0-5580
algoliasearch: ^4.15.0
algoliasearch-helper: ^3.12.0
clsx: ^1.2.1
@@ -2078,23 +2079,23 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: 89256f242ccc5d0e1aca92e817413a1e40d18de58fb069e283741b2e5dbc29b267b345fc6d602ad88ac0faf0a003c3a99d549e9bc9027aa79b0e767ad4d7b79c
+ checksum: efc6054ad979b9fde43bec8befeb41c0f9ca609832b51e4da42fdcdfd7e4ee599fd3e6949c576d9ff348c0ffd044a4dbb07638a1990f57586f493ba394369c96
languageName: node
linkType: hard
-"@docusaurus/theme-translations@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/theme-translations@npm:0.0.0-5566"
+"@docusaurus/theme-translations@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/theme-translations@npm:0.0.0-5580"
dependencies:
fs-extra: ^11.1.0
tslib: ^2.5.0
- checksum: e6595e675617669267b48e8cf79cbde8e65f279b8cdf0a3a45cb75f32ec0d2ddd59d0d02ac3f0aaa5328f0a13cd63a41aaff49a2251ce6665bba4abdfba71361
+ checksum: 1555956d2a7cf3122ded374937559ab19e6a6620fd0910d6bdbf48be78a10e595cfd9a0b9157c129f36f0bfae4297f07991f071a394c87cb9d2facee04597dd2
languageName: node
linkType: hard
-"@docusaurus/types@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/types@npm:0.0.0-5566"
+"@docusaurus/types@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/types@npm:0.0.0-5580"
dependencies:
"@types/history": ^4.7.11
"@types/react": "*"
@@ -2107,13 +2108,13 @@ __metadata:
peerDependencies:
react: ^16.8.4 || ^17.0.0
react-dom: ^16.8.4 || ^17.0.0
- checksum: aec430471a0019e650e168320c168f942ce8097022c45fb70e1ec86ba793b146f95a9d3cf434e3d9c985a45cea363f870cc51395dddaf0d0f600496ca6dbf810
+ checksum: 5df67f562c5d516eb020ecf63e6343e1d6d18e2716b1b530a24c5d63b48229623222861c1a69f0e4715c34a768f7c05599340f1af18ca1259709467fb1149af9
languageName: node
linkType: hard
-"@docusaurus/utils-common@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/utils-common@npm:0.0.0-5566"
+"@docusaurus/utils-common@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/utils-common@npm:0.0.0-5580"
dependencies:
tslib: ^2.5.0
peerDependencies:
@@ -2121,28 +2122,28 @@ __metadata:
peerDependenciesMeta:
"@docusaurus/types":
optional: true
- checksum: 14cc02aac8a59350244ab733f8c662b838aac9ed3bc156a75113cbf1e2bf50d3db2e722313f9b6fe2616dcfafef3abc858f064289a8a0539b97745c571ec3fca
+ checksum: 238b2f12607834164fa82f9a3154703b75609719888cf96f06c9f70c17ab76a46babcfd66dce4f2722ecaaf47742a40a606101f0578f2345845fa23b6f262b5d
languageName: node
linkType: hard
-"@docusaurus/utils-validation@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/utils-validation@npm:0.0.0-5566"
+"@docusaurus/utils-validation@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/utils-validation@npm:0.0.0-5580"
dependencies:
- "@docusaurus/logger": 0.0.0-5566
- "@docusaurus/utils": 0.0.0-5566
+ "@docusaurus/logger": 0.0.0-5580
+ "@docusaurus/utils": 0.0.0-5580
joi: ^17.8.3
js-yaml: ^4.1.0
tslib: ^2.5.0
- checksum: 85a3570134ee26c88571ccb409c08f8ed182257cd72eb564e2a553e0d32a9a0c8bf4daaf1c33c5805e77e47a9fb97d29dfa0a758d8a7224d70ece80e82b583cc
+ checksum: cf0602b2411738adc47f5596fd4a3076137aa754aca24a4630700dc6355fed07e9a66ec093e438fa1199d9076bebae036317d073a604e4f0666f8390ceb73b5d
languageName: node
linkType: hard
-"@docusaurus/utils@npm:0.0.0-5566":
- version: 0.0.0-5566
- resolution: "@docusaurus/utils@npm:0.0.0-5566"
+"@docusaurus/utils@npm:0.0.0-5580":
+ version: 0.0.0-5580
+ resolution: "@docusaurus/utils@npm:0.0.0-5580"
dependencies:
- "@docusaurus/logger": 0.0.0-5566
+ "@docusaurus/logger": 0.0.0-5580
"@svgr/webpack": ^6.5.1
escape-string-regexp: ^4.0.0
file-loader: ^6.2.0
@@ -2163,7 +2164,7 @@ __metadata:
peerDependenciesMeta:
"@docusaurus/types":
optional: true
- checksum: 82f682fdec2711d4608bb84f4a400e4f93bfbd4fde2da4cce37691b91e3e501bd409e7e86bae19bcc3b5dce576edcd33e078b25c28236ed1f15628d814cfe04e
+ checksum: c9b085d3724cba748b3db763f056742aace45cc58a636e23513ee6a7db781558377de5507b651b3566562fa71eebb42ac835034cdcbcc2c77edf9b4009c6a215
languageName: node
linkType: hard
@@ -2603,90 +2604,90 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-darwin-arm64@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-darwin-arm64@npm:1.3.51"
+"@swc/core-darwin-arm64@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-darwin-arm64@npm:1.3.59"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
-"@swc/core-darwin-x64@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-darwin-x64@npm:1.3.51"
+"@swc/core-darwin-x64@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-darwin-x64@npm:1.3.59"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
-"@swc/core-linux-arm-gnueabihf@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.51"
+"@swc/core-linux-arm-gnueabihf@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.59"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
-"@swc/core-linux-arm64-gnu@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-linux-arm64-gnu@npm:1.3.51"
+"@swc/core-linux-arm64-gnu@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-linux-arm64-gnu@npm:1.3.59"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
-"@swc/core-linux-arm64-musl@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-linux-arm64-musl@npm:1.3.51"
+"@swc/core-linux-arm64-musl@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-linux-arm64-musl@npm:1.3.59"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
-"@swc/core-linux-x64-gnu@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-linux-x64-gnu@npm:1.3.51"
+"@swc/core-linux-x64-gnu@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-linux-x64-gnu@npm:1.3.59"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
-"@swc/core-linux-x64-musl@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-linux-x64-musl@npm:1.3.51"
+"@swc/core-linux-x64-musl@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-linux-x64-musl@npm:1.3.59"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
-"@swc/core-win32-arm64-msvc@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-win32-arm64-msvc@npm:1.3.51"
+"@swc/core-win32-arm64-msvc@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-win32-arm64-msvc@npm:1.3.59"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
-"@swc/core-win32-ia32-msvc@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-win32-ia32-msvc@npm:1.3.51"
+"@swc/core-win32-ia32-msvc@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-win32-ia32-msvc@npm:1.3.59"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
-"@swc/core-win32-x64-msvc@npm:1.3.51":
- version: 1.3.51
- resolution: "@swc/core-win32-x64-msvc@npm:1.3.51"
+"@swc/core-win32-x64-msvc@npm:1.3.59":
+ version: 1.3.59
+ resolution: "@swc/core-win32-x64-msvc@npm:1.3.59"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@swc/core@npm:^1.3.46":
- version: 1.3.51
- resolution: "@swc/core@npm:1.3.51"
+ version: 1.3.59
+ resolution: "@swc/core@npm:1.3.59"
dependencies:
- "@swc/core-darwin-arm64": 1.3.51
- "@swc/core-darwin-x64": 1.3.51
- "@swc/core-linux-arm-gnueabihf": 1.3.51
- "@swc/core-linux-arm64-gnu": 1.3.51
- "@swc/core-linux-arm64-musl": 1.3.51
- "@swc/core-linux-x64-gnu": 1.3.51
- "@swc/core-linux-x64-musl": 1.3.51
- "@swc/core-win32-arm64-msvc": 1.3.51
- "@swc/core-win32-ia32-msvc": 1.3.51
- "@swc/core-win32-x64-msvc": 1.3.51
+ "@swc/core-darwin-arm64": 1.3.59
+ "@swc/core-darwin-x64": 1.3.59
+ "@swc/core-linux-arm-gnueabihf": 1.3.59
+ "@swc/core-linux-arm64-gnu": 1.3.59
+ "@swc/core-linux-arm64-musl": 1.3.59
+ "@swc/core-linux-x64-gnu": 1.3.59
+ "@swc/core-linux-x64-musl": 1.3.59
+ "@swc/core-win32-arm64-msvc": 1.3.59
+ "@swc/core-win32-ia32-msvc": 1.3.59
+ "@swc/core-win32-x64-msvc": 1.3.59
peerDependencies:
"@swc/helpers": ^0.5.0
dependenciesMeta:
@@ -2713,7 +2714,7 @@ __metadata:
peerDependenciesMeta:
"@swc/helpers":
optional: true
- checksum: f4e72d347b26353fbb48eae110a63c939922a38b20730ea010f5ae3dd15434f212dce582df05556ec35e677c089dfb6d00f5dd6fc655f8c9a7829458e4301469
+ checksum: 0283f39cf49cd12a6dc7062ef9ea4fa93e1a4f622ce46903a09605219f4f47ed5ad03925c6146d09ffe6ee02a89bb14e3c82eb86b4c60ecce4985ea6e078ffcf
languageName: node
linkType: hard
@@ -2991,6 +2992,13 @@ __metadata:
languageName: node
linkType: hard
+"@types/parse5@npm:^6.0.0":
+ version: 6.0.3
+ resolution: "@types/parse5@npm:6.0.3"
+ checksum: ddb59ee4144af5dfcc508a8dcf32f37879d11e12559561e65788756b95b33e6f03ea027d88e1f5408f9b7bfb656bf630ace31a2169edf44151daaf8dd58df1b7
+ languageName: node
+ linkType: hard
+
"@types/prop-types@npm:*":
version: 15.7.5
resolution: "@types/prop-types@npm:15.7.5"
@@ -3737,10 +3745,10 @@ __metadata:
version: 0.0.0-use.local
resolution: "backstage-microsite@workspace:."
dependencies:
- "@docusaurus/core": 0.0.0-5566
- "@docusaurus/module-type-aliases": 0.0.0-5566
- "@docusaurus/plugin-client-redirects": 0.0.0-5566
- "@docusaurus/preset-classic": 0.0.0-5566
+ "@docusaurus/core": 0.0.0-5580
+ "@docusaurus/module-type-aliases": 0.0.0-5580
+ "@docusaurus/plugin-client-redirects": 0.0.0-5580
+ "@docusaurus/preset-classic": 0.0.0-5580
"@spotify/prettier-config": ^14.0.0
"@swc/core": ^1.3.46
"@tsconfig/docusaurus": ^1.0.6
@@ -6132,6 +6140,21 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-from-parse5@npm:^7.0.0":
+ version: 7.1.2
+ resolution: "hast-util-from-parse5@npm:7.1.2"
+ dependencies:
+ "@types/hast": ^2.0.0
+ "@types/unist": ^2.0.0
+ hastscript: ^7.0.0
+ property-information: ^6.0.0
+ vfile: ^5.0.0
+ vfile-location: ^4.0.0
+ web-namespaces: ^2.0.0
+ checksum: 7b4ed5b508b1352127c6719f7b0c0880190cf9859fe54ccaf7c9228ecf623d36cef3097910b3874d2fe1aac6bf4cf45d3cc2303daac3135a05e9ade6534ddddb
+ languageName: node
+ linkType: hard
+
"hast-util-parse-selector@npm:^3.0.0":
version: 3.1.1
resolution: "hast-util-parse-selector@npm:3.1.1"
@@ -6141,6 +6164,25 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-raw@npm:^7.2.0":
+ version: 7.2.3
+ resolution: "hast-util-raw@npm:7.2.3"
+ dependencies:
+ "@types/hast": ^2.0.0
+ "@types/parse5": ^6.0.0
+ hast-util-from-parse5: ^7.0.0
+ hast-util-to-parse5: ^7.0.0
+ html-void-elements: ^2.0.0
+ parse5: ^6.0.0
+ unist-util-position: ^4.0.0
+ unist-util-visit: ^4.0.0
+ vfile: ^5.0.0
+ web-namespaces: ^2.0.0
+ zwitch: ^2.0.0
+ checksum: 21857eea3ffb8fd92d2d9be7793b56d0b2c40db03c4cfa14828855ae41d7c584917aa83efb7157220b2e41e25e95f81f24679ac342c35145e5f1c1d39015f81f
+ languageName: node
+ linkType: hard
+
"hast-util-to-estree@npm:^2.0.0":
version: 2.3.2
resolution: "hast-util-to-estree@npm:2.3.2"
@@ -6164,6 +6206,20 @@ __metadata:
languageName: node
linkType: hard
+"hast-util-to-parse5@npm:^7.0.0":
+ version: 7.1.0
+ resolution: "hast-util-to-parse5@npm:7.1.0"
+ dependencies:
+ "@types/hast": ^2.0.0
+ comma-separated-tokens: ^2.0.0
+ property-information: ^6.0.0
+ space-separated-tokens: ^2.0.0
+ web-namespaces: ^2.0.0
+ zwitch: ^2.0.0
+ checksum: 3a7f2175a3db599bbae7e49ba73d3e5e688e5efca7590ff50130ba108ad649f728402815d47db49146f6b94c14c934bf119915da9f6964e38802c122bcc8af6b
+ languageName: node
+ linkType: hard
+
"hast-util-whitespace@npm:^2.0.0":
version: 2.0.1
resolution: "hast-util-whitespace@npm:2.0.1"
@@ -6171,7 +6227,7 @@ __metadata:
languageName: node
linkType: hard
-"hastscript@npm:^7.1.0":
+"hastscript@npm:^7.0.0, hastscript@npm:^7.1.0":
version: 7.2.0
resolution: "hastscript@npm:7.2.0"
dependencies:
@@ -6276,6 +6332,13 @@ __metadata:
languageName: node
linkType: hard
+"html-void-elements@npm:^2.0.0":
+ version: 2.0.1
+ resolution: "html-void-elements@npm:2.0.1"
+ checksum: 06d41f13b9d5d6e0f39861c4bec9a9196fa4906d56cd5cf6cf54ad2e52a85bf960cca2bf9600026bde16c8331db171bedba5e5a35e2e43630c8f1d497b2fb658
+ languageName: node
+ linkType: hard
+
"html-webpack-plugin@npm:^5.5.0":
version: 5.5.0
resolution: "html-webpack-plugin@npm:5.5.0"
@@ -8735,6 +8798,13 @@ __metadata:
languageName: node
linkType: hard
+"parse5@npm:^6.0.0":
+ version: 6.0.1
+ resolution: "parse5@npm:6.0.1"
+ checksum: 7d569a176c5460897f7c8f3377eff640d54132b9be51ae8a8fa4979af940830b2b0c296ce75e5bd8f4041520aadde13170dbdec44889975f906098ea0002f4bd
+ languageName: node
+ linkType: hard
+
"parse5@npm:^7.0.0":
version: 7.1.2
resolution: "parse5@npm:7.1.2"
@@ -9901,6 +9971,17 @@ __metadata:
languageName: node
linkType: hard
+"rehype-raw@npm:^6.1.1":
+ version: 6.1.1
+ resolution: "rehype-raw@npm:6.1.1"
+ dependencies:
+ "@types/hast": ^2.0.0
+ hast-util-raw: ^7.2.0
+ unified: ^10.0.0
+ checksum: a1f9d309e609f49fb1f1e06e722705f4dd2e569653a89f756eaccb33b612cf1bb511216a81d10a619d11d047afc161e4b3cb99b957df05a8ba8fdbd5843f949a
+ languageName: node
+ linkType: hard
+
"relateurl@npm:^0.2.7":
version: 0.2.7
resolution: "relateurl@npm:0.2.7"
@@ -11526,6 +11607,13 @@ __metadata:
languageName: node
linkType: hard
+"web-namespaces@npm:^2.0.0":
+ version: 2.0.1
+ resolution: "web-namespaces@npm:2.0.1"
+ checksum: b6d9f02f1a43d0ef0848a812d89c83801d5bbad57d8bb61f02eb6d7eb794c3736f6cc2e1191664bb26136594c8218ac609f4069722c6f56d9fc2d808fa9271c6
+ languageName: node
+ linkType: hard
+
"webidl-conversions@npm:^3.0.0":
version: 3.0.1
resolution: "webidl-conversions@npm:3.0.1"
diff --git a/package.json b/package.json
index 44778ae926..003d4e704e 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"@types/react": "^17",
"@types/react-dom": "^17"
},
- "version": "1.14.0-next.1",
+ "version": "1.15.0-next.0",
"dependencies": {
"@backstage/errors": "workspace:^",
"@manypkg/get-packages": "^1.1.3"
diff --git a/packages/app-defaults/CHANGELOG.md b/packages/app-defaults/CHANGELOG.md
index 5d781b58ca..d18d5e0006 100644
--- a/packages/app-defaults/CHANGELOG.md
+++ b/packages/app-defaults/CHANGELOG.md
@@ -1,5 +1,60 @@
# @backstage/app-defaults
+## 1.4.0-next.0
+
+### Minor Changes
+
+- 1fd38bc4141a: **MUI v5 Support:** Adding platform-wide support for MUI v5 allowing a transition phase for migrating central plugins & components over. We still support v4 instances & plugins by adding a
+
+ To allow the future support of plugins & components using MUI v5 you want to upgrade your `AppTheme`'s to using the `UnifiedThemeProvider`
+
+ ```diff
+ Provider: ({ children }) => (
+ -
+ - {children}
+ -
+ +
+ ),
+ ```
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-app-api@1.8.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
+## 1.3.1
+
+### Patch Changes
+
+- 575d9178eff: Added a System Icon for resource entities.
+ This can be obtained using:
+
+ ```ts
+ useApp().getSystemIcon('kind:resource');
+ ```
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/core-app-api@1.8.0
+ - @backstage/core-components@0.13.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
+## 1.3.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
## 1.3.1-next.1
### Patch Changes
diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json
index 0a8f2e5dc1..2d1e2b3746 100644
--- a/packages/app-defaults/package.json
+++ b/packages/app-defaults/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/app-defaults",
"description": "Provides the default wiring of a Backstage App",
- "version": "1.3.1-next.1",
+ "version": "1.4.0-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/app-defaults/src/defaults/themes.tsx b/packages/app-defaults/src/defaults/themes.tsx
index 397c160168..10368bd3bc 100644
--- a/packages/app-defaults/src/defaults/themes.tsx
+++ b/packages/app-defaults/src/defaults/themes.tsx
@@ -15,11 +15,12 @@
*/
import React from 'react';
-import { darkTheme, lightTheme } from '@backstage/theme';
+import {
+ UnifiedThemeProvider,
+ themes as builtinThemes,
+} from '@backstage/theme';
import DarkIcon from '@material-ui/icons/Brightness2';
import LightIcon from '@material-ui/icons/WbSunny';
-import { ThemeProvider } from '@material-ui/core/styles';
-import CssBaseline from '@material-ui/core/CssBaseline';
import { AppTheme } from '@backstage/core-plugin-api';
export const themes: AppTheme[] = [
@@ -29,9 +30,7 @@ export const themes: AppTheme[] = [
variant: 'light',
icon: ,
Provider: ({ children }) => (
-
- {children}
-
+
),
},
{
@@ -40,9 +39,7 @@ export const themes: AppTheme[] = [
variant: 'dark',
icon: ,
Provider: ({ children }) => (
-
- {children}
-
+
),
},
];
diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md
index b20fafb995..f087ae6da0 100644
--- a/packages/app/CHANGELOG.md
+++ b/packages/app/CHANGELOG.md
@@ -1,5 +1,217 @@
# example-app
+## 0.2.84-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-kubernetes@0.9.2-next.0
+ - @backstage/plugin-user-settings@0.7.4-next.0
+ - @backstage/plugin-catalog-import@0.9.9-next.0
+ - @backstage/plugin-scaffolder-react@1.4.1-next.0
+ - @backstage/plugin-octopus-deploy@0.2.1-next.0
+ - @backstage/plugin-scaffolder@1.13.2-next.0
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/plugin-techdocs@1.6.3-next.0
+ - @backstage/plugin-github-actions@0.6.0-next.0
+ - @backstage/plugin-stack-overflow@0.1.16-next.0
+ - @backstage/plugin-home@0.5.3-next.0
+ - @backstage/app-defaults@1.4.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/cli@0.22.8-next.0
+ - @backstage/plugin-techdocs-react@1.1.7-next.0
+ - @backstage/plugin-dynatrace@6.0.0-next.0
+ - @backstage/plugin-pagerduty@0.5.12-next.0
+ - @backstage/plugin-catalog-graph@0.2.31-next.0
+ - @backstage/plugin-org@0.6.9-next.0
+ - @backstage/plugin-airbrake@0.3.19-next.0
+ - @backstage/plugin-api-docs@0.9.4-next.0
+ - @backstage/plugin-azure-devops@0.3.1-next.0
+ - @backstage/plugin-azure-sites@0.1.8-next.0
+ - @backstage/plugin-badges@0.2.43-next.0
+ - @internal/plugin-catalog-customized@0.0.11-next.0
+ - @backstage/plugin-circleci@0.3.19-next.0
+ - @backstage/plugin-cloudbuild@0.3.19-next.0
+ - @backstage/plugin-code-coverage@0.2.12-next.0
+ - @backstage/plugin-cost-insights@0.12.8-next.0
+ - @backstage/plugin-entity-feedback@0.2.2-next.0
+ - @backstage/plugin-explore@0.4.4-next.0
+ - @backstage/plugin-gocd@0.1.25-next.0
+ - @backstage/plugin-jenkins@0.8.1-next.0
+ - @backstage/plugin-kafka@0.3.19-next.0
+ - @backstage/plugin-lighthouse@0.4.4-next.0
+ - @backstage/plugin-linguist@0.1.4-next.0
+ - @backstage/plugin-newrelic-dashboard@0.2.12-next.0
+ - @backstage/plugin-playlist@0.1.10-next.0
+ - @backstage/plugin-puppetdb@0.1.2-next.0
+ - @backstage/plugin-rollbar@0.4.19-next.0
+ - @backstage/plugin-search@1.3.1-next.0
+ - @backstage/plugin-sentry@0.5.4-next.0
+ - @backstage/plugin-tech-insights@0.3.11-next.0
+ - @backstage/plugin-todo@0.2.21-next.0
+ - @backstage/plugin-techdocs-module-addons-contrib@1.0.14-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/integration-react@1.1.14-next.0
+ - @backstage/plugin-apache-airflow@0.2.12-next.0
+ - @backstage/plugin-devtools@0.1.1-next.0
+ - @backstage/plugin-gcalendar@0.3.15-next.0
+ - @backstage/plugin-gcp-projects@0.3.38-next.0
+ - @backstage/plugin-graphiql@0.2.51-next.0
+ - @backstage/plugin-microsoft-calendar@0.1.4-next.0
+ - @backstage/plugin-newrelic@0.3.37-next.0
+ - @backstage/plugin-permission-react@0.4.12
+ - @backstage/plugin-search-react@1.6.1-next.0
+ - @backstage/plugin-shortcuts@0.3.11-next.0
+ - @backstage/plugin-stackstorm@0.1.3-next.0
+ - @backstage/plugin-tech-radar@0.6.5-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-linguist-common@0.1.0
+ - @backstage/plugin-search-common@1.2.3
+
+## 0.2.83
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-tech-insights@0.3.10
+ - @backstage/plugin-scaffolder@1.13.1
+ - @backstage/plugin-devtools@0.1.0
+ - @backstage/plugin-kubernetes@0.9.0
+ - @backstage/plugin-search@1.3.0
+ - @backstage/plugin-home@0.5.2
+ - @backstage/plugin-explore@0.4.3
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-techdocs@1.6.2
+ - @backstage/app-defaults@1.3.1
+ - @backstage/plugin-badges@0.2.42
+ - @backstage/plugin-tech-radar@0.6.4
+ - @backstage/cli@0.22.7
+ - @backstage/plugin-stack-overflow@0.1.15
+ - @backstage/plugin-gcalendar@0.3.14
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-app-api@1.8.0
+ - @backstage/plugin-scaffolder-react@1.4.0
+ - @backstage/plugin-catalog-graph@0.2.30
+ - @backstage/plugin-circleci@0.3.18
+ - @backstage/plugin-search-react@1.6.0
+ - @backstage/plugin-org@0.6.8
+ - @backstage/core-components@0.13.1
+ - @backstage/plugin-azure-devops@0.3.0
+ - @backstage/plugin-jenkins@0.8.0
+ - @backstage/plugin-octopus-deploy@0.2.0
+ - @backstage/plugin-techdocs-module-addons-contrib@1.0.13
+ - @backstage/plugin-user-settings@0.7.3
+ - @backstage/plugin-shortcuts@0.3.10
+ - @backstage/integration-react@1.1.13
+ - @backstage/plugin-airbrake@0.3.18
+ - @backstage/plugin-api-docs@0.9.3
+ - @backstage/plugin-azure-sites@0.1.7
+ - @backstage/plugin-cloudbuild@0.3.18
+ - @backstage/plugin-code-coverage@0.2.11
+ - @backstage/plugin-cost-insights@0.12.7
+ - @backstage/plugin-dynatrace@5.0.0
+ - @backstage/plugin-entity-feedback@0.2.1
+ - @backstage/plugin-gcp-projects@0.3.37
+ - @backstage/plugin-github-actions@0.5.18
+ - @backstage/plugin-gocd@0.1.24
+ - @backstage/plugin-graphiql@0.2.50
+ - @backstage/plugin-kafka@0.3.18
+ - @backstage/plugin-lighthouse@0.4.3
+ - @backstage/plugin-linguist@0.1.3
+ - @backstage/plugin-microsoft-calendar@0.1.3
+ - @backstage/plugin-newrelic@0.3.36
+ - @backstage/plugin-pagerduty@0.5.11
+ - @backstage/plugin-playlist@0.1.9
+ - @backstage/plugin-puppetdb@0.1.1
+ - @backstage/plugin-rollbar@0.4.18
+ - @backstage/plugin-sentry@0.5.3
+ - @backstage/plugin-stackstorm@0.1.2
+ - @backstage/plugin-techdocs-react@1.1.6
+ - @backstage/plugin-todo@0.2.20
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-apache-airflow@0.2.11
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-catalog-import@0.9.8
+ - @backstage/plugin-linguist-common@0.1.0
+ - @backstage/plugin-newrelic-dashboard@0.2.11
+ - @backstage/plugin-permission-react@0.4.12
+ - @backstage/plugin-search-common@1.2.3
+ - @internal/plugin-catalog-customized@0.0.10
+
+## 0.2.83-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools@0.1.0-next.0
+ - @backstage/plugin-kubernetes@0.9.0-next.2
+ - @backstage/plugin-home@0.5.2-next.2
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/plugin-badges@0.2.42-next.2
+ - @backstage/plugin-tech-radar@0.6.4-next.2
+ - @backstage/plugin-scaffolder-react@1.4.0-next.2
+ - @backstage/plugin-explore@0.4.3-next.2
+ - @backstage/plugin-org@0.6.8-next.2
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/plugin-azure-devops@0.3.0-next.2
+ - @backstage/plugin-stack-overflow@0.1.15-next.2
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-airbrake@0.3.18-next.2
+ - @backstage/plugin-api-docs@0.9.3-next.2
+ - @backstage/plugin-azure-sites@0.1.7-next.2
+ - @backstage/plugin-catalog-graph@0.2.30-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/plugin-circleci@0.3.18-next.2
+ - @backstage/plugin-cloudbuild@0.3.18-next.2
+ - @backstage/plugin-code-coverage@0.2.11-next.2
+ - @backstage/plugin-cost-insights@0.12.7-next.2
+ - @backstage/plugin-dynatrace@5.0.0-next.2
+ - @backstage/plugin-entity-feedback@0.2.1-next.2
+ - @backstage/plugin-gcalendar@0.3.14-next.1
+ - @backstage/plugin-gcp-projects@0.3.37-next.1
+ - @backstage/plugin-github-actions@0.5.18-next.2
+ - @backstage/plugin-gocd@0.1.24-next.2
+ - @backstage/plugin-graphiql@0.2.50-next.1
+ - @backstage/plugin-jenkins@0.7.17-next.2
+ - @backstage/plugin-kafka@0.3.18-next.2
+ - @backstage/plugin-lighthouse@0.4.3-next.2
+ - @backstage/plugin-linguist@0.1.3-next.2
+ - @backstage/plugin-microsoft-calendar@0.1.3-next.1
+ - @backstage/plugin-newrelic@0.3.36-next.1
+ - @backstage/plugin-octopus-deploy@0.2.0-next.2
+ - @backstage/plugin-pagerduty@0.5.11-next.2
+ - @backstage/plugin-playlist@0.1.9-next.2
+ - @backstage/plugin-puppetdb@0.1.1-next.2
+ - @backstage/plugin-rollbar@0.4.18-next.2
+ - @backstage/plugin-scaffolder@1.13.1-next.2
+ - @backstage/plugin-search@1.3.0-next.2
+ - @backstage/plugin-sentry@0.5.3-next.2
+ - @backstage/plugin-shortcuts@0.3.10-next.2
+ - @backstage/plugin-stackstorm@0.1.2-next.1
+ - @backstage/plugin-tech-insights@0.3.10-next.2
+ - @backstage/plugin-techdocs@1.6.2-next.2
+ - @backstage/plugin-techdocs-module-addons-contrib@1.0.13-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/plugin-todo@0.2.20-next.2
+ - @backstage/plugin-user-settings@0.7.3-next.2
+ - @backstage/plugin-apache-airflow@0.2.11-next.1
+ - @backstage/plugin-catalog-import@0.9.8-next.2
+ - @backstage/plugin-newrelic-dashboard@0.2.11-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+ - @internal/plugin-catalog-customized@0.0.10-next.2
+
## 0.2.83-next.1
### Patch Changes
diff --git a/packages/app/package.json b/packages/app/package.json
index d4941ecd48..010b2dd648 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "example-app",
- "version": "0.2.83-next.1",
+ "version": "0.2.84-next.0",
"private": true,
"backstage": {
"role": "frontend"
@@ -29,6 +29,7 @@
"@backstage/plugin-cloudbuild": "workspace:^",
"@backstage/plugin-code-coverage": "workspace:^",
"@backstage/plugin-cost-insights": "workspace:^",
+ "@backstage/plugin-devtools": "workspace:^",
"@backstage/plugin-dynatrace": "workspace:^",
"@backstage/plugin-entity-feedback": "workspace:^",
"@backstage/plugin-explore": "workspace:^",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 19edd9640d..bc9eac8c64 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -109,6 +109,8 @@ import { TwoColumnLayout } from './components/scaffolder/customScaffolderLayouts
import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
import { StackstormPage } from '@backstage/plugin-stackstorm';
import { PuppetDbPage } from '@backstage/plugin-puppetdb';
+import { DevToolsPage } from '@backstage/plugin-devtools';
+import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
const app = createApp({
apis,
@@ -291,6 +293,9 @@ const routes = (
} />
} />
} />
+ }>
+ {customDevToolsPage}
+
);
diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx
index 904fa4a7d4..ffaf72296a 100644
--- a/packages/app/src/components/Root/Root.tsx
+++ b/packages/app/src/components/Root/Root.tsx
@@ -52,6 +52,7 @@ import { MyGroupsSidebarItem } from '@backstage/plugin-org';
import { SearchModal } from '../search/SearchModal';
import Score from '@material-ui/icons/Score';
import { useApp } from '@backstage/core-plugin-api';
+import BuildIcon from '@material-ui/icons/Build';
const useSidebarLogoStyles = makeStyles({
root: {
@@ -176,6 +177,7 @@ export const Root = ({ children }: PropsWithChildren<{}>) => (
to="/settings"
>
+
{children}
diff --git a/packages/app/src/components/devtools/CustomDevToolsPage.tsx b/packages/app/src/components/devtools/CustomDevToolsPage.tsx
new file mode 100644
index 0000000000..dfe70426aa
--- /dev/null
+++ b/packages/app/src/components/devtools/CustomDevToolsPage.tsx
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ConfigContent,
+ ExternalDependenciesContent,
+ InfoContent,
+} from '@backstage/plugin-devtools';
+import { DevToolsLayout } from '@backstage/plugin-devtools';
+import React from 'react';
+
+const DevToolsPage = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const customDevToolsPage = ;
diff --git a/packages/app/src/components/home/HomePage.tsx b/packages/app/src/components/home/HomePage.tsx
index 26a7ab60f3..da1d0d425c 100644
--- a/packages/app/src/components/home/HomePage.tsx
+++ b/packages/app/src/components/home/HomePage.tsx
@@ -21,12 +21,15 @@ import {
ClockConfig,
HomePageStarredEntities,
CustomHomepageGrid,
+ HomePageToolkit,
+ HomePageCompanyLogo,
} from '@backstage/plugin-home';
import { Content, Header, Page } from '@backstage/core-components';
import { HomePageSearchBar } from '@backstage/plugin-search';
import { HomePageCalendar } from '@backstage/plugin-gcalendar';
import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar';
import React from 'react';
+import HomeIcon from '@material-ui/icons/Home';
const clockConfigs: ClockConfig[] = [
{
@@ -55,12 +58,26 @@ const timeFormat: Intl.DateTimeFormatOptions = {
const defaultConfig = [
{
- component: 'HomePageSearchBar',
+ component: 'CompanyLogo',
x: 0,
y: 0,
width: 12,
height: 1,
},
+ {
+ component: 'WelcomeTitle',
+ x: 0,
+ y: 1,
+ width: 12,
+ height: 1,
+ },
+ {
+ component: 'HomePageSearchBar',
+ x: 0,
+ y: 2,
+ width: 12,
+ height: 1,
+ },
];
export const homePage = (
@@ -78,6 +95,17 @@ export const homePage = (
+
+
+ ,
+ },
+ ]}
+ />
diff --git a/packages/app/src/components/home/templates/CustomizableTemplate.stories.tsx b/packages/app/src/components/home/templates/CustomizableTemplate.stories.tsx
new file mode 100644
index 0000000000..e587e52da4
--- /dev/null
+++ b/packages/app/src/components/home/templates/CustomizableTemplate.stories.tsx
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ HomePageStarredEntities,
+ HomePageRandomJoke,
+ CustomHomepageGrid,
+} from '@backstage/plugin-home';
+import {
+ starredEntitiesApiRef,
+ MockStarredEntitiesApi,
+ entityRouteRef,
+ catalogApiRef,
+} from '@backstage/plugin-catalog-react';
+import { wrapInTestApp, TestApiProvider } from '@backstage/test-utils';
+import { configApiRef } from '@backstage/core-plugin-api';
+import { ConfigReader } from '@backstage/config';
+import { searchApiRef } from '@backstage/plugin-search-react';
+import { HomePageSearchBar, searchPlugin } from '@backstage/plugin-search';
+import { HomePageCalendar } from '@backstage/plugin-gcalendar';
+import { MicrosoftCalendarCard } from '@backstage/plugin-microsoft-calendar';
+import React, { ComponentType } from 'react';
+
+const entities = [
+ {
+ apiVersion: '1',
+ kind: 'Component',
+ metadata: {
+ name: 'mock-starred-entity',
+ title: 'Mock Starred Entity!',
+ },
+ },
+ {
+ apiVersion: '1',
+ kind: 'Component',
+ metadata: {
+ name: 'mock-starred-entity-2',
+ title: 'Mock Starred Entity 2!',
+ },
+ },
+ {
+ apiVersion: '1',
+ kind: 'Component',
+ metadata: {
+ name: 'mock-starred-entity-3',
+ title: 'Mock Starred Entity 3!',
+ },
+ },
+ {
+ apiVersion: '1',
+ kind: 'Component',
+ metadata: {
+ name: 'mock-starred-entity-4',
+ title: 'Mock Starred Entity 4!',
+ },
+ },
+];
+
+const mockCatalogApi = {
+ getEntities: async () => ({ items: entities }),
+};
+
+const starredEntitiesApi = new MockStarredEntitiesApi();
+starredEntitiesApi.toggleStarred('component:default/example-starred-entity');
+starredEntitiesApi.toggleStarred('component:default/example-starred-entity-2');
+starredEntitiesApi.toggleStarred('component:default/example-starred-entity-3');
+starredEntitiesApi.toggleStarred('component:default/example-starred-entity-4');
+
+export default {
+ title: 'Plugins/Home/Templates',
+ decorators: [
+ (Story: ComponentType<{}>) =>
+ wrapInTestApp(
+ <>
+ Promise.resolve({ results: [] }) }],
+ [
+ configApiRef,
+ new ConfigReader({
+ backend: {
+ baseUrl: 'https://localhost:7007',
+ },
+ }),
+ ],
+ ]}
+ >
+
+
+ >,
+ {
+ mountedRoutes: {
+ '/hello-company': searchPlugin.routes.root,
+ '/catalog/:namespace/:kind/:name': entityRouteRef,
+ },
+ },
+ ),
+ ],
+};
+export const CustomizableTemplate = () => {
+ // This is the default configuration that is shown to the user
+ // when first arriving to the homepage.
+ const defaultConfig = [
+ {
+ component: 'HomePageSearchBar',
+ x: 0,
+ y: 0,
+ width: 12,
+ height: 5,
+ },
+ {
+ component: 'HomePageRandomJoke',
+ x: 0,
+ y: 2,
+ width: 6,
+ height: 16,
+ },
+ {
+ component: 'HomePageStarredEntities',
+ x: 6,
+ y: 2,
+ width: 6,
+ height: 12,
+ },
+ ];
+
+ return (
+
+ // Insert the allowed widgets inside the grid. User can add, organize and
+ // remove the widgets as they want.
+
+
+
+
+
+
+ );
+};
diff --git a/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx b/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx
index 9cf2e8ab35..52fab3c55f 100644
--- a/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx
+++ b/packages/app/src/components/home/templates/DefaultTemplate.stories.tsx
@@ -140,15 +140,16 @@ export default {
};
const useStyles = makeStyles(theme => ({
- searchBar: {
- display: 'flex',
+ searchBarInput: {
maxWidth: '60vw',
- backgroundColor: theme.palette.background.paper,
- boxShadow: theme.shadows[1],
- padding: '8px 0',
- borderRadius: '50px',
margin: 'auto',
+ backgroundColor: theme.palette.background.paper,
+ borderRadius: '50px',
+ boxShadow: theme.shadows[1],
},
+ searchBarOutline: {
+ borderStyle: 'none'
+ }
}));
const useLogoStyles = makeStyles(theme => ({
@@ -177,9 +178,9 @@ export const DefaultTemplate = () => {
className={container}
logo={}
/>
-
+
diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md
index 00e7582ad3..3a6eadf23b 100644
--- a/packages/backend-app-api/CHANGELOG.md
+++ b/packages/backend-app-api/CHANGELOG.md
@@ -1,5 +1,38 @@
# @backstage/backend-app-api
+## 0.4.4-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config-loader@1.3.1-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.15-next.0
+ - @backstage/plugin-permission-node@0.7.9-next.0
+
+## 0.4.3
+
+### Patch Changes
+
+- cf13b482f9e: Switch `configServiceFactory` to use `ConfigSources` from `@backstage/config-loader` to load config.
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/config-loader@1.3.0
+ - @backstage/plugin-permission-node@0.7.8
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.4.3-next.1
### Patch Changes
diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json
index a21484ff2e..679c9fa9ca 100644
--- a/packages/backend-app-api/package.json
+++ b/packages/backend-app-api/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-app-api",
"description": "Core API used by Backstage backend apps",
- "version": "0.4.3-next.1",
+ "version": "0.4.4-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md
index fc6d99cf93..a354795e21 100644
--- a/packages/backend-common/CHANGELOG.md
+++ b/packages/backend-common/CHANGELOG.md
@@ -1,5 +1,41 @@
# @backstage/backend-common
+## 0.18.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config-loader@1.3.1-next.0
+ - @backstage/backend-app-api@0.4.4-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/integration-aws-node@0.1.3
+ - @backstage/config@1.0.7
+ - @backstage/backend-dev-utils@0.1.1
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 0.18.5
+
+### Patch Changes
+
+- 0297f7a54af: Remove the direct dependency on deprecated "request" library
+- 284db225083: Updated the `DatabaseManager` to include the plugin id in the Postgres application name of the database connections created for each plugin.
+- 3659c71c5d9: Standardize `@aws-sdk` v3 versions
+- 42d817e76ab: Added `HostDiscovery` to supersede deprecated `SingleHostDiscovery` (deprecated due to name)
+- Updated dependencies
+ - @backstage/config-loader@1.3.0
+ - @backstage/integration@1.4.5
+ - @backstage/integration-aws-node@0.1.3
+ - @backstage/backend-app-api@0.4.3
+ - @backstage/backend-dev-utils@0.1.1
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.18.5-next.1
### Patch Changes
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index 045a385218..79d955be88 100644
--- a/packages/backend-common/package.json
+++ b/packages/backend-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-common",
"description": "Common functionality library for Backstage backends",
- "version": "0.18.5-next.1",
+ "version": "0.18.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
@@ -46,10 +46,10 @@
"test:kubernetes": "backstage-cli package test -t KubernetesContainerRunner --no-watch"
},
"dependencies": {
- "@aws-sdk/abort-controller": "^3.208.0",
- "@aws-sdk/client-s3": "^3.208.0",
- "@aws-sdk/credential-providers": "^3.208.0",
- "@aws-sdk/types": "^3.208.0",
+ "@aws-sdk/abort-controller": "^3.310.0",
+ "@aws-sdk/client-s3": "^3.310.0",
+ "@aws-sdk/credential-providers": "^3.310.0",
+ "@aws-sdk/types": "^3.310.0",
"@backstage/backend-app-api": "workspace:^",
"@backstage/backend-dev-utils": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
@@ -114,7 +114,7 @@
}
},
"devDependencies": {
- "@aws-sdk/util-stream-node": "^3.208.0",
+ "@aws-sdk/util-stream-node": "^3.310.0",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/archiver": "^5.1.0",
diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md
index 52043b4ecf..5f1ceea06c 100644
--- a/packages/backend-defaults/CHANGELOG.md
+++ b/packages/backend-defaults/CHANGELOG.md
@@ -1,5 +1,23 @@
# @backstage/backend-defaults
+## 0.1.11-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.4.4-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/backend-plugin-api@0.5.3-next.0
+
+## 0.1.10
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/backend-app-api@0.4.3
+ - @backstage/backend-plugin-api@0.5.2
+
## 0.1.10-next.1
### Patch Changes
diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json
index d99e462efa..4677d9af89 100644
--- a/packages/backend-defaults/package.json
+++ b/packages/backend-defaults/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-defaults",
"description": "Backend defaults used by Backstage backend apps",
- "version": "0.1.10-next.1",
+ "version": "0.1.11-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-next/CHANGELOG.md b/packages/backend-next/CHANGELOG.md
index 2eea18b4af..d26412f02d 100644
--- a/packages/backend-next/CHANGELOG.md
+++ b/packages/backend-next/CHANGELOG.md
@@ -1,5 +1,59 @@
# example-backend-next
+## 0.0.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.14.1-next.0
+ - @backstage/plugin-search-backend-module-explore@0.1.2-next.0
+ - @backstage/plugin-catalog-backend@1.9.2-next.0
+ - @backstage/plugin-kubernetes-backend@0.11.1-next.0
+ - @backstage/plugin-search-backend-module-catalog@0.1.2-next.0
+ - @backstage/plugin-search-backend-module-techdocs@0.1.2-next.0
+ - @backstage/plugin-techdocs-backend@1.6.3-next.0
+ - @backstage/plugin-todo-backend@0.1.43-next.0
+ - @backstage/plugin-app-backend@0.3.46-next.0
+ - @backstage/backend-defaults@0.1.11-next.0
+ - @backstage/plugin-auth-node@0.2.15-next.0
+ - @backstage/plugin-permission-backend@0.5.21-next.0
+ - @backstage/plugin-permission-common@0.7.5
+ - @backstage/plugin-permission-node@0.7.9-next.0
+ - @backstage/plugin-search-backend@1.3.2-next.0
+ - @backstage/plugin-search-backend-node@1.2.2-next.0
+
+## 0.0.11
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.14.0
+ - @backstage/plugin-catalog-backend@1.9.1
+ - @backstage/plugin-kubernetes-backend@0.11.0
+ - @backstage/plugin-todo-backend@0.1.42
+ - @backstage/plugin-permission-node@0.7.8
+ - @backstage/plugin-search-backend@1.3.1
+ - @backstage/backend-defaults@0.1.10
+ - @backstage/plugin-app-backend@0.3.45
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/plugin-search-backend-module-catalog@0.1.1
+ - @backstage/plugin-search-backend-module-explore@0.1.1
+ - @backstage/plugin-search-backend-module-techdocs@0.1.1
+ - @backstage/plugin-techdocs-backend@1.6.2
+ - @backstage/plugin-permission-backend@0.5.20
+ - @backstage/plugin-search-backend-node@1.2.1
+ - @backstage/plugin-permission-common@0.7.5
+
+## 0.0.11-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.0-next.2
+ - @backstage/plugin-search-backend@1.3.1-next.2
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+
## 0.0.11-next.1
### Patch Changes
diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json
index bf913492ac..664360eaa0 100644
--- a/packages/backend-next/package.json
+++ b/packages/backend-next/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend-next",
- "version": "0.0.11-next.1",
+ "version": "0.0.12-next.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/backend-openapi-utils/CHANGELOG.md b/packages/backend-openapi-utils/CHANGELOG.md
index 015a54cf45..0255bbbf73 100644
--- a/packages/backend-openapi-utils/CHANGELOG.md
+++ b/packages/backend-openapi-utils/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/backend-openapi-utils
+## 0.0.2
+
+### Patch Changes
+
+- fe16bd39e83: Use permalinks for links including a line number reference
+- 27956d78671: Adjusted README accordingly after the generated output now has a `.generated.ts` extension
+- 021cfbb5152: Corrected resolution of parameter nested schema to use central schemas.
+- 799c33047ed: Updated README to reflect changes in `@backstage/repo-tools`.
+
+## 0.0.2-next.1
+
+### Patch Changes
+
+- fe16bd39e83: Use permalinks for links including a line number reference
+- 021cfbb5152: Corrected resolution of parameter nested schema to use central schemas.
+
## 0.0.2-next.0
### Patch Changes
diff --git a/packages/backend-openapi-utils/README.md b/packages/backend-openapi-utils/README.md
index fe22ac2cf2..ba286f94e8 100644
--- a/packages/backend-openapi-utils/README.md
+++ b/packages/backend-openapi-utils/README.md
@@ -8,7 +8,7 @@ This package is meant to provide a typed Express router for an OpenAPI spec. Bas
### Configuration
-1. Run `yarn --cwd backstage-cli package schema:openapi:generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.generated.ts`. The command will try to execute both a lint and prettier step on the generated file, where applicable.
+1. Run `yarn --cwd backstage-cli package schema openapi generate` to translate your `src/schema/openapi.yaml` to a new Typescript file in `src/schema/openapi.generated.ts`. The command will try to execute both a lint and prettier step on the generated file, where applicable.
2. In your plugin's `src/service/createRouter.ts`,
@@ -42,4 +42,4 @@ Router() as ApiRouter>
### Runtime validation
-Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level.
+Using a package like [`express-openapi-validator`](https://www.npmjs.com/package/express-openapi-validator), would allow us to remove [validation of request bodies with `AJV`](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/src/service/util.ts#L58). However, `AJV` currently doesn't have support for OpenAPI 3.1 and `express-openapi-validator` enforces full URL matching for paths, meaning it cannot be mounted at the router level.
diff --git a/packages/backend-openapi-utils/package.json b/packages/backend-openapi-utils/package.json
index e59da29840..89a4621a49 100644
--- a/packages/backend-openapi-utils/package.json
+++ b/packages/backend-openapi-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-openapi-utils",
"description": "OpenAPI typescript support.",
- "version": "0.0.2-next.0",
+ "version": "0.0.2",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md
index 9a28b1dd3a..97b1bb4664 100644
--- a/packages/backend-plugin-api/CHANGELOG.md
+++ b/packages/backend-plugin-api/CHANGELOG.md
@@ -1,5 +1,27 @@
# @backstage/backend-plugin-api
+## 0.5.3-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.15-next.0
+ - @backstage/plugin-permission-common@0.7.5
+
+## 0.5.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/config@1.0.7
+ - @backstage/types@1.0.2
+ - @backstage/plugin-permission-common@0.7.5
+
## 0.5.2-next.1
### Patch Changes
diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json
index 58e103e647..117fd931d9 100644
--- a/packages/backend-plugin-api/package.json
+++ b/packages/backend-plugin-api/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-plugin-api",
"description": "Core API used by Backstage backend plugins",
- "version": "0.5.2-next.1",
+ "version": "0.5.3-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-tasks/CHANGELOG.md b/packages/backend-tasks/CHANGELOG.md
index 1ef0203eb1..70532f865e 100644
--- a/packages/backend-tasks/CHANGELOG.md
+++ b/packages/backend-tasks/CHANGELOG.md
@@ -1,5 +1,25 @@
# @backstage/backend-tasks
+## 0.5.3-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 0.5.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.5.2-next.1
### Patch Changes
diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json
index b0f5c3afb3..7a76b1a004 100644
--- a/packages/backend-tasks/package.json
+++ b/packages/backend-tasks/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-tasks",
"description": "Common distributed task management library for Backstage backends",
- "version": "0.5.2-next.1",
+ "version": "0.5.3-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-test-utils/CHANGELOG.md b/packages/backend-test-utils/CHANGELOG.md
index 3ce5006003..8b3c5de2c7 100644
--- a/packages/backend-test-utils/CHANGELOG.md
+++ b/packages/backend-test-utils/CHANGELOG.md
@@ -1,5 +1,31 @@
# @backstage/backend-test-utils
+## 0.1.38-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.4.4-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.15-next.0
+
+## 0.1.37
+
+### Patch Changes
+
+- 63af7f6d53f: Allow specifying custom Docker registry for database tests
+- b1eb268bf9d: Added `POSTGRES_11` and `POSTGRES_12` as supported test database IDs.
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/backend-app-api@0.4.3
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/config@1.0.7
+ - @backstage/types@1.0.2
+
## 0.1.37-next.1
### Patch Changes
diff --git a/packages/backend-test-utils/README.md b/packages/backend-test-utils/README.md
index d0f52388bb..f22cb1bd3c 100644
--- a/packages/backend-test-utils/README.md
+++ b/packages/backend-test-utils/README.md
@@ -12,6 +12,26 @@ cd plugins/my-plugin-backend
yarn add --dev @backstage/backend-test-utils
```
+## Environment variables
+
+- `BACKSTAGE_TEST_DISABLE_DOCKER`
+ - Setting the value to `1` disables Docker for tests
+- `CI`
+ - Setting the value to `1` enables long-running tests, including the ones utilizing Docker
+- `BACKSTAGE_TEST_DOCKER_REGISTRY`
+ - Docker registry mirror address where to pull images for tests, for example `mycompany.docker.io/mirror`
+ - See [documentation](https://node.testcontainers.org/configuration/) for information
+ about authentication (`DOCKER_AUTH_CONFIG`)
+
+Connection strings for different databases that are used for testing. The value of the
+string should point to the running instance of the database.
+
+- `BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING`
+- `BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING`
+- `BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING`
+- `BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING`
+- `BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING`
+
## Documentation
- [Backstage Readme](https://github.com/backstage/backstage/blob/master/README.md)
diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json
index b23d3b411e..b8a747a871 100644
--- a/packages/backend-test-utils/package.json
+++ b/packages/backend-test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/backend-test-utils",
"description": "Test helpers library for Backstage backends",
- "version": "0.1.37-next.1",
+ "version": "0.1.38-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-test-utils/src/database/types.ts b/packages/backend-test-utils/src/database/types.ts
index 89ec21e7cc..8c1d6f5193 100644
--- a/packages/backend-test-utils/src/database/types.ts
+++ b/packages/backend-test-utils/src/database/types.ts
@@ -16,6 +16,7 @@
import { DatabaseManager } from '@backstage/backend-common';
import { Knex } from 'knex';
+import { getDockerImageForName } from '../util/getDockerImageForName';
/**
* The possible databases to test against.
@@ -48,35 +49,35 @@ export const allDatabases: Record =
POSTGRES_13: {
name: 'Postgres 13.x',
driver: 'pg',
- dockerImageName: 'postgres:13',
+ dockerImageName: getDockerImageForName('postgres:13'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES13_CONNECTION_STRING',
},
POSTGRES_12: {
name: 'Postgres 12.x',
driver: 'pg',
- dockerImageName: 'postgres:12',
+ dockerImageName: getDockerImageForName('postgres:12'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES12_CONNECTION_STRING',
},
POSTGRES_11: {
name: 'Postgres 11.x',
driver: 'pg',
- dockerImageName: 'postgres:11',
+ dockerImageName: getDockerImageForName('postgres:11'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES11_CONNECTION_STRING',
},
POSTGRES_9: {
name: 'Postgres 9.x',
driver: 'pg',
- dockerImageName: 'postgres:9',
+ dockerImageName: getDockerImageForName('postgres:9'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_POSTGRES9_CONNECTION_STRING',
},
MYSQL_8: {
name: 'MySQL 8.x',
driver: 'mysql2',
- dockerImageName: 'mysql:8',
+ dockerImageName: getDockerImageForName('mysql:8'),
connectionStringEnvironmentVariableName:
'BACKSTAGE_TEST_DATABASE_MYSQL8_CONNECTION_STRING',
},
diff --git a/packages/backend-test-utils/src/util/getDockerImageForName.ts b/packages/backend-test-utils/src/util/getDockerImageForName.ts
new file mode 100644
index 0000000000..7869e7b418
--- /dev/null
+++ b/packages/backend-test-utils/src/util/getDockerImageForName.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export const getDockerImageForName = (name: string) => {
+ return process.env.BACKSTAGE_TEST_DOCKER_REGISTRY
+ ? `${process.env.BACKSTAGE_TEST_DOCKER_REGISTRY}/${name}`
+ : name;
+};
diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md
index e676df6839..8ba25739ed 100644
--- a/packages/backend/CHANGELOG.md
+++ b/packages/backend/CHANGELOG.md
@@ -1,5 +1,126 @@
# example-backend
+## 0.2.84-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/plugin-scaffolder-backend@1.14.1-next.0
+ - @backstage/plugin-jenkins-backend@0.2.1-next.0
+ - @backstage/plugin-linguist-backend@0.3.0-next.0
+ - @backstage/plugin-devtools-backend@0.1.1-next.0
+ - @backstage/plugin-adr-backend@0.3.4-next.0
+ - @backstage/plugin-auth-backend@0.18.4-next.0
+ - @backstage/plugin-badges-backend@0.2.1-next.0
+ - @backstage/plugin-catalog-backend@1.9.2-next.0
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/plugin-code-coverage-backend@0.2.12-next.0
+ - @backstage/plugin-entity-feedback-backend@0.1.4-next.0
+ - @backstage/plugin-kubernetes-backend@0.11.1-next.0
+ - @backstage/plugin-lighthouse-backend@0.2.2-next.0
+ - @backstage/plugin-playlist-backend@0.3.2-next.0
+ - @backstage/plugin-tech-insights-backend@0.5.12-next.0
+ - @backstage/plugin-techdocs-backend@1.6.3-next.0
+ - @backstage/plugin-todo-backend@0.1.43-next.0
+ - example-app@0.2.84-next.0
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.15-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/plugin-app-backend@0.3.46-next.0
+ - @backstage/plugin-explore-backend@0.0.8-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/plugin-auth-node@0.2.15-next.0
+ - @backstage/plugin-azure-devops-backend@0.3.25-next.0
+ - @backstage/plugin-azure-sites-backend@0.1.8-next.0
+ - @backstage/plugin-events-backend@0.2.7-next.0
+ - @backstage/plugin-events-node@0.2.7-next.0
+ - @backstage/plugin-graphql-backend@0.1.36-next.0
+ - @backstage/plugin-kafka-backend@0.2.39-next.0
+ - @backstage/plugin-permission-backend@0.5.21-next.0
+ - @backstage/plugin-permission-common@0.7.5
+ - @backstage/plugin-permission-node@0.7.9-next.0
+ - @backstage/plugin-proxy-backend@0.2.40-next.0
+ - @backstage/plugin-rollbar-backend@0.1.43-next.0
+ - @backstage/plugin-search-backend@1.3.2-next.0
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.1-next.0
+ - @backstage/plugin-search-backend-module-pg@0.5.7-next.0
+ - @backstage/plugin-search-backend-node@1.2.2-next.0
+ - @backstage/plugin-search-common@1.2.3
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.30-next.0
+ - @backstage/plugin-tech-insights-node@0.4.4-next.0
+
+## 0.2.83
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.14.0
+ - @backstage/plugin-devtools-backend@0.1.0
+ - @backstage/plugin-catalog-backend@1.9.1
+ - @backstage/backend-common@0.18.5
+ - @backstage/plugin-kubernetes-backend@0.11.0
+ - @backstage/plugin-auth-backend@0.18.3
+ - @backstage/plugin-badges-backend@0.2.0
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.0
+ - @backstage/integration@1.4.5
+ - @backstage/plugin-todo-backend@0.1.42
+ - @backstage/plugin-jenkins-backend@0.2.0
+ - @backstage/plugin-azure-sites-backend@0.1.7
+ - @backstage/plugin-permission-node@0.7.8
+ - @backstage/plugin-search-backend@1.3.1
+ - example-app@0.2.83
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-app-backend@0.3.45
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/plugin-entity-feedback-backend@0.1.3
+ - @backstage/plugin-events-backend@0.2.6
+ - @backstage/plugin-playlist-backend@0.3.1
+ - @backstage/plugin-rollbar-backend@0.1.42
+ - @backstage/plugin-search-backend-module-pg@0.5.6
+ - @backstage/plugin-tech-insights-backend@0.5.11
+ - @backstage/plugin-techdocs-backend@1.6.2
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.14
+ - @backstage/plugin-adr-backend@0.3.3
+ - @backstage/plugin-azure-devops-backend@0.3.24
+ - @backstage/plugin-code-coverage-backend@0.2.11
+ - @backstage/plugin-explore-backend@0.0.7
+ - @backstage/plugin-graphql-backend@0.1.35
+ - @backstage/plugin-kafka-backend@0.2.38
+ - @backstage/plugin-lighthouse-backend@0.2.1
+ - @backstage/plugin-linguist-backend@0.2.2
+ - @backstage/plugin-permission-backend@0.5.20
+ - @backstage/plugin-proxy-backend@0.2.39
+ - @backstage/plugin-search-backend-node@1.2.1
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.29
+ - @backstage/plugin-tech-insights-node@0.4.3
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-events-node@0.2.6
+ - @backstage/plugin-permission-common@0.7.5
+ - @backstage/plugin-search-common@1.2.3
+
+## 0.2.83-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-backend@0.1.0-next.0
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/plugin-badges-backend@0.2.0-next.2
+ - @backstage/plugin-search-backend-module-elasticsearch@1.3.0-next.2
+ - @backstage/plugin-kubernetes-backend@0.11.0-next.2
+ - @backstage/plugin-auth-backend@0.18.3-next.2
+ - @backstage/plugin-search-backend@1.3.1-next.2
+ - example-app@0.2.83-next.2
+ - @backstage/plugin-scaffolder-backend@1.13.2-next.2
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.14-next.2
+ - @backstage/config@1.0.7
+
## 0.2.83-next.1
### Patch Changes
diff --git a/packages/backend/Dockerfile b/packages/backend/Dockerfile
index eca6ce82c2..da25f9736c 100644
--- a/packages/backend/Dockerfile
+++ b/packages/backend/Dockerfile
@@ -13,11 +13,14 @@ FROM node:16-bullseye-slim
# Install sqlite3 dependencies. You can skip this if you don't use sqlite3 in the image,
# in which case you should also move better-sqlite3 to "devDependencies" in package.json.
+# Additionally, we install dependencies for `techdocs.generator.runIn: local`.
+# https://backstage.io/docs/features/techdocs/getting-started#disabling-docker-in-docker-situation-optional
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update && \
- apt-get install -y --no-install-recommends libsqlite3-dev python3 build-essential && \
- yarn config set python /usr/bin/python3
+ apt-get install -y --no-install-recommends libsqlite3-dev python3 python3-pip build-essential && \
+ yarn config set python /usr/bin/python3 && \
+ pip3 install mkdocs-techdocs-core==1.1.7
# From here on we use the least-privileged `node` user to run the backend.
WORKDIR /app
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 900ee150fc..f225e35d98 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.2.83-next.1",
+ "version": "0.2.84-next.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -42,6 +42,7 @@
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-code-coverage-backend": "workspace:^",
+ "@backstage/plugin-devtools-backend": "workspace:^",
"@backstage/plugin-entity-feedback-backend": "workspace:^",
"@backstage/plugin-events-backend": "workspace:^",
"@backstage/plugin-events-node": "workspace:^",
diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts
index 593d30bc3b..976ad9a577 100644
--- a/packages/backend/src/index.ts
+++ b/packages/backend/src/index.ts
@@ -64,6 +64,7 @@ import playlist from './plugins/playlist';
import adr from './plugins/adr';
import lighthouse from './plugins/lighthouse';
import linguist from './plugins/linguist';
+import devTools from './plugins/devtools';
import { PluginEnvironment } from './types';
import { ServerPermissionClient } from '@backstage/plugin-permission-node';
import { DefaultIdentityClient } from '@backstage/plugin-auth-node';
@@ -158,8 +159,8 @@ async function main() {
const eventsEnv = useHotMemoize(module, () => createEnv('events'));
const exploreEnv = useHotMemoize(module, () => createEnv('explore'));
const lighthouseEnv = useHotMemoize(module, () => createEnv('lighthouse'));
-
const linguistEnv = useHotMemoize(module, () => createEnv('linguist'));
+ const devToolsEnv = useHotMemoize(module, () => createEnv('devtools'));
const apiRouter = Router();
apiRouter.use('/catalog', await catalog(catalogEnv));
@@ -185,6 +186,7 @@ async function main() {
apiRouter.use('/entity-feedback', await entityFeedback(entityFeedbackEnv));
apiRouter.use('/adr', await adr(adrEnv));
apiRouter.use('/linguist', await linguist(linguistEnv));
+ apiRouter.use('/devtools', await devTools(devToolsEnv));
apiRouter.use(notFoundHandler());
await lighthouse(lighthouseEnv);
diff --git a/packages/backend/src/plugins/badges.ts b/packages/backend/src/plugins/badges.ts
index 37529689f3..fa50731809 100644
--- a/packages/backend/src/plugins/badges.ts
+++ b/packages/backend/src/plugins/badges.ts
@@ -28,5 +28,8 @@ export default async function createPlugin(
config: env.config,
discovery: env.discovery,
badgeFactories: createDefaultBadgeFactories(),
+ tokenManager: env.tokenManager,
+ logger: env.logger,
+ identity: env.identity,
});
}
diff --git a/packages/backend/src/plugins/devtools.ts b/packages/backend/src/plugins/devtools.ts
new file mode 100644
index 0000000000..8e1767ddb1
--- /dev/null
+++ b/packages/backend/src/plugins/devtools.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createRouter } from '@backstage/plugin-devtools-backend';
+import { Router } from 'express';
+import type { PluginEnvironment } from '../types';
+
+export default async function createPlugin(
+ env: PluginEnvironment,
+): Promise {
+ return createRouter({
+ logger: env.logger,
+ config: env.config,
+ permissions: env.permissions,
+ });
+}
diff --git a/packages/catalog-client/CHANGELOG.md b/packages/catalog-client/CHANGELOG.md
index 12c6e1b9c9..5b7d668947 100644
--- a/packages/catalog-client/CHANGELOG.md
+++ b/packages/catalog-client/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/catalog-client
+## 1.4.2-next.0
+
+### Patch Changes
+
+- cc936b529676: Updated API docs
+- Updated dependencies
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+
## 1.4.1
### Patch Changes
diff --git a/packages/catalog-client/package.json b/packages/catalog-client/package.json
index 010269dee6..77d3b58735 100644
--- a/packages/catalog-client/package.json
+++ b/packages/catalog-client/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/catalog-client",
"description": "An isomorphic client for the catalog backend",
- "version": "1.4.1",
+ "version": "1.4.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/catalog-client/src/types/api.ts b/packages/catalog-client/src/types/api.ts
index ee6579efca..5285bda2f7 100644
--- a/packages/catalog-client/src/types/api.ts
+++ b/packages/catalog-client/src/types/api.ts
@@ -353,6 +353,10 @@ export type Location = {
export type AddLocationRequest = {
type?: string;
target: string;
+ /**
+ * If set to true, the location will not be added, but the response will
+ * contain the entities that match the given location.
+ */
dryRun?: boolean;
};
@@ -363,8 +367,13 @@ export type AddLocationRequest = {
*/
export type AddLocationResponse = {
location: Location;
+ /**
+ * The entities matching this location. Will only be filled in dryRun mode
+ */
entities: Entity[];
- // Only set in dryRun mode.
+ /**
+ * True, if the location exists. Will only be filled in dryRun mode
+ */
exists?: boolean;
};
diff --git a/packages/catalog-model/examples/domains/artists-domain.yaml b/packages/catalog-model/examples/domains/artists-domain.yaml
index 2a74425791..0dc1b91cb5 100644
--- a/packages/catalog-model/examples/domains/artists-domain.yaml
+++ b/packages/catalog-model/examples/domains/artists-domain.yaml
@@ -3,6 +3,7 @@ kind: Domain
metadata:
name: artists
description: Everything related to artists
+ title: Artists
links:
- url: http://example.com/domain/artists/
title: Domain Readme
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index a8c7207650..386effadda 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,36 @@
# @backstage/cli
+## 0.22.8-next.0
+
+### Patch Changes
+
+- 20b7da6f1311: Enforcing MUI v5 specific linting to minimize bundle size.
+- Updated dependencies
+ - @backstage/config-loader@1.3.1-next.0
+ - @backstage/config@1.0.7
+ - @backstage/release-manifests@0.0.9
+ - @backstage/cli-common@0.1.12
+ - @backstage/cli-node@0.1.0
+ - @backstage/errors@1.1.5
+ - @backstage/eslint-plugin@0.1.3
+ - @backstage/types@1.0.2
+
+## 0.22.7
+
+### Patch Changes
+
+- 473db605a4f: Enable strict config checking during `backstage-cli config:check` with the new `--strict` option which will surface schema errors.
+- d548886872d: Deprecated the use of React 16
+- Updated dependencies
+ - @backstage/config-loader@1.3.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/cli-node@0.1.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/eslint-plugin@0.1.3
+ - @backstage/release-manifests@0.0.9
+ - @backstage/types@1.0.2
+
## 0.22.7-next.0
### Patch Changes
diff --git a/packages/cli/cli-report.md b/packages/cli/cli-report.md
index bad849d283..39bee2ed05 100644
--- a/packages/cli/cli-report.md
+++ b/packages/cli/cli-report.md
@@ -58,6 +58,7 @@ Options:
--lax
--frontend
--deprecated
+ --strict
--config
-h, --help
```
diff --git a/packages/cli/config/eslint-factory.js b/packages/cli/config/eslint-factory.js
index dde853fdff..36b51324fc 100644
--- a/packages/cli/config/eslint-factory.js
+++ b/packages/cli/config/eslint-factory.js
@@ -24,6 +24,7 @@ const { join: joinPath } = require('path');
* - `tsRules`: Additional ESLint rules to apply to TypeScript
* - `testRules`: Additional ESLint rules to apply to tests
* - `restrictedImports`: Additional paths to add to no-restricted-imports
+ * - `restrictedImportsPattern`: Additional patterns to add to no-restricted-imports
* - `restrictedSrcImports`: Additional paths to add to no-restricted-imports in src files
* - `restrictedTestImports`: Additional paths to add to no-restricted-imports in test files
* - `restrictedSyntax`: Additional patterns to add to no-restricted-syntax
@@ -44,6 +45,7 @@ function createConfig(dir, extraConfig = {}) {
testRules,
restrictedImports,
+ restrictedImportPatterns,
restrictedSrcImports,
restrictedTestImports,
restrictedSyntax,
@@ -114,6 +116,7 @@ function createConfig(dir, extraConfig = {}) {
'*.test*',
'**/__testUtils__/**',
'**/__mocks__/**',
+ ...(restrictedImportPatterns ?? []),
],
},
],
@@ -208,9 +211,19 @@ function createConfigForRole(dir, role, extraConfig = {}) {
name: '@material-ui/icons/', // because this is possible too ._.
message: "Please import '@material-ui/icons/' instead.",
},
+ {
+ // https://mui.com/material-ui/guides/minimizing-bundle-size/
+ name: '@mui/material',
+ message: "Please import '@mui/material/...' instead.",
+ },
...require('module').builtinModules,
...(extraConfig.restrictedImports ?? []),
],
+ // https://mui.com/material-ui/guides/minimizing-bundle-size/
+ restrictedImportPatterns: [
+ '@mui/*/*/*',
+ ...(extraConfig.restrictedImportPatterns ?? []),
+ ],
tsRules: {
'react/prop-types': 0,
...extraConfig.tsRules,
diff --git a/packages/cli/package.json b/packages/cli/package.json
index 9de7b3dbb2..82f0667d5e 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/cli",
"description": "CLI for developing Backstage plugins and apps",
- "version": "0.22.7-next.0",
+ "version": "0.22.8-next.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/cli/src/commands/config/validate.ts b/packages/cli/src/commands/config/validate.ts
index c3d05291e0..c81d82832e 100644
--- a/packages/cli/src/commands/config/validate.ts
+++ b/packages/cli/src/commands/config/validate.ts
@@ -24,5 +24,6 @@ export default async (opts: OptionValues) => {
mockEnv: opts.lax,
fullVisibility: !opts.frontend,
withDeprecatedKeys: opts.deprecated,
+ strict: opts.strict,
});
};
diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts
index ba11a53cfd..3a40839e25 100644
--- a/packages/cli/src/commands/index.ts
+++ b/packages/cli/src/commands/index.ts
@@ -345,6 +345,10 @@ export function registerCommands(program: Command) {
.option('--lax', 'Do not require environment variables to be set')
.option('--frontend', 'Only validate the frontend configuration')
.option('--deprecated', 'Output deprecated configuration settings')
+ .option(
+ '--strict',
+ 'Enable strict config validation, forbidding errors and unknown keys',
+ )
.option(...configOption)
.description(
'Validate that the given configuration loads and matches schema',
diff --git a/packages/cli/src/commands/start/startFrontend.ts b/packages/cli/src/commands/start/startFrontend.ts
index e066a40e7a..00f7818b41 100644
--- a/packages/cli/src/commands/start/startFrontend.ts
+++ b/packages/cli/src/commands/start/startFrontend.ts
@@ -32,6 +32,30 @@ interface StartAppOptions {
configPaths: string[];
}
+function checkReactVersion() {
+ try {
+ // Make sure we're looking at the root of the target repo
+ const reactPkgPath = require.resolve('react/package.json', {
+ paths: [paths.targetRoot],
+ });
+ const reactPkg = require(reactPkgPath);
+ if (reactPkg.version.startsWith('16.')) {
+ console.log(
+ chalk.yellow(
+ `
+⚠️ ⚠️
+⚠️ You are using React version 16, which is deprecated for use in Backstage. ⚠️
+⚠️ Please upgrade to React 17 by updating your packages/app dependencies. ⚠️
+⚠️ ⚠️
+`,
+ ),
+ );
+ }
+ } catch {
+ /* ignored */
+ }
+}
+
export async function startFrontend(options: StartAppOptions) {
if (options.verifyVersions) {
const lockfile = await Lockfile.load(paths.resolveTargetRoot('yarn.lock'));
@@ -65,6 +89,8 @@ export async function startFrontend(options: StartAppOptions) {
}
}
+ checkReactVersion();
+
const { name } = await fs.readJson(paths.resolveTarget('package.json'));
const config = await loadCliConfig({
args: options.configPaths,
diff --git a/packages/cli/src/lib/config.ts b/packages/cli/src/lib/config.ts
index b34e1280c0..aaf3fcfcb9 100644
--- a/packages/cli/src/lib/config.ts
+++ b/packages/cli/src/lib/config.ts
@@ -32,6 +32,7 @@ type Options = {
withFilteredKeys?: boolean;
withDeprecatedKeys?: boolean;
fullVisibility?: boolean;
+ strict?: boolean;
};
export async function loadCliConfig(options: Options) {
@@ -70,6 +71,7 @@ export async function loadCliConfig(options: Options) {
dependencies: localPackageNames,
// Include the package.json in the project root if it exists
packagePaths: [paths.resolveTargetRoot('package.json')],
+ noUndeclaredProperties: options.strict,
});
const { appConfigs } = await loadConfig({
@@ -93,6 +95,7 @@ export async function loadCliConfig(options: Options) {
: ['frontend'],
withFilteredKeys: options.withFilteredKeys,
withDeprecatedKeys: options.withDeprecatedKeys,
+ ignoreSchemaErrors: !options.strict,
});
const frontendConfig = ConfigReader.fromConfigs(frontendAppConfigs);
diff --git a/packages/config-loader/CHANGELOG.md b/packages/config-loader/CHANGELOG.md
index 9b2d34f957..ce613acbb5 100644
--- a/packages/config-loader/CHANGELOG.md
+++ b/packages/config-loader/CHANGELOG.md
@@ -1,5 +1,67 @@
# @backstage/config-loader
+## 1.3.1-next.0
+
+### Patch Changes
+
+- f25427f665f7: Fix a bug where config items with `/` in the key were incorrectly handled.
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 1.3.0
+
+### Minor Changes
+
+- 201206132da: Introduced a new config source system to replace `loadConfig`. There is a new `ConfigSource` interface along with utilities provided by `ConfigSources`, as well as a number of built-in configuration source implementations. The new system is more flexible and makes it easier to create new and reusable sources of configuration, such as loading configuration from secret providers.
+
+ The following is an example of how to load configuration using the default behavior:
+
+ ```ts
+ const source = ConfigSources.default({
+ argv: options?.argv,
+ remote: options?.remote,
+ });
+ const config = await ConfigSources.toConfig(source);
+ ```
+
+ The `ConfigSource` interface looks like this:
+
+ ```ts
+ export interface ConfigSource {
+ readConfigData(options?: ReadConfigDataOptions): AsyncConfigSourceIterator;
+ }
+ ```
+
+ It is best implemented using an async iterator:
+
+ ```ts
+ class MyConfigSource implements ConfigSource {
+ async *readConfigData() {
+ yield {
+ config: [
+ {
+ context: 'example',
+ data: { backend: { baseUrl: 'http://localhost' } },
+ },
+ ],
+ };
+ }
+ }
+ ```
+
+### Patch Changes
+
+- 7c116bcac7f: Fixed the way that some request errors are thrown
+- 473db605a4f: Added a new `noUndeclaredProperties` option to `SchemaLoader` to support enforcing that there are no extra keys when verifying config.
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 1.3.0-next.0
### Minor Changes
diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md
index 1cc353fd4c..8cd469d4d7 100644
--- a/packages/config-loader/api-report.md
+++ b/packages/config-loader/api-report.md
@@ -180,14 +180,17 @@ export function loadConfigSchema(
): Promise;
// @public
-export type LoadConfigSchemaOptions =
+export type LoadConfigSchemaOptions = (
| {
dependencies: string[];
packagePaths?: string[];
}
| {
serialized: JsonObject;
- };
+ }
+) & {
+ noUndeclaredProperties?: boolean;
+};
// @public
export function mergeConfigSchemas(schemas: JSONSchema7[]): JSONSchema7;
diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json
index 0aa071c206..741637e40b 100644
--- a/packages/config-loader/package.json
+++ b/packages/config-loader/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/config-loader",
"description": "Config loading functionality used by Backstage backend, and CLI",
- "version": "1.3.0-next.0",
+ "version": "1.3.1-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.cjs.js",
diff --git a/packages/config-loader/src/schema/compile.test.ts b/packages/config-loader/src/schema/compile.test.ts
index c2541751e7..df81923684 100644
--- a/packages/config-loader/src/schema/compile.test.ts
+++ b/packages/config-loader/src/schema/compile.test.ts
@@ -170,4 +170,64 @@ describe('compileConfigSchemas', () => {
visibilityBySchemaPath: new Map(),
});
});
+
+ it('should handle slashes correctly', () => {
+ const validate = compileConfigSchemas([
+ {
+ path: 'a1',
+ value: {
+ type: 'object',
+ properties: {
+ '/circleci/api': {
+ type: 'object',
+ properties: {
+ target: {
+ type: 'string',
+ },
+ headers: {
+ type: 'object',
+ properties: {
+ 'Circle-Token': {
+ type: 'string',
+ visibility: 'secret',
+ },
+ },
+ },
+ },
+ },
+ '/gocd': { type: 'string', visibility: 'backend' },
+ },
+ },
+ },
+ ]);
+ expect(
+ validate([
+ {
+ data: {
+ '/circleci/api': {
+ target: 'test',
+ headers: {
+ 'Circle-Token': 'my-token',
+ },
+ },
+ '/gocd': 'test',
+ },
+ context: 'test',
+ },
+ ]),
+ ).toEqual({
+ visibilityByDataPath: new Map(
+ Object.entries({
+ '//circleci/api/headers/Circle-Token': 'secret',
+ }),
+ ),
+ visibilityBySchemaPath: new Map(
+ Object.entries({
+ '/properties//circleci/api/properties/headers/properties/Circle-Token':
+ 'secret',
+ }),
+ ),
+ deprecationByDataPath: new Map(),
+ });
+ });
});
diff --git a/packages/config-loader/src/schema/compile.ts b/packages/config-loader/src/schema/compile.ts
index d193e7fbf2..63a8e4b8a6 100644
--- a/packages/config-loader/src/schema/compile.ts
+++ b/packages/config-loader/src/schema/compile.ts
@@ -25,6 +25,8 @@ import {
CONFIG_VISIBILITIES,
ConfigVisibility,
} from './types';
+import { SchemaObject } from 'json-schema-traverse';
+import { normalizeAjvPath } from './utils';
/**
* This takes a collection of Backstage configuration schemas from various
@@ -35,6 +37,9 @@ import {
*/
export function compileConfigSchemas(
schemas: ConfigSchemaPackageEntry[],
+ options?: {
+ noUndeclaredProperties?: boolean;
+ },
): ValidationFunc {
// The ajv instance below is stateful and doesn't really allow for additional
// output during validation. We work around this by having this extra piece
@@ -62,10 +67,7 @@ export function compileConfigSchemas(
return false;
}
if (visibility && visibility !== 'backend') {
- const normalizedPath = context.instancePath.replace(
- /\['?(.*?)'?\]/g,
- (_, segment) => `/${segment}`,
- );
+ const normalizedPath = normalizeAjvPath(context.instancePath);
visibilityByDataPath.set(normalizedPath, visibility);
}
return true;
@@ -81,10 +83,7 @@ export function compileConfigSchemas(
if (context?.instancePath === undefined) {
return false;
}
- const normalizedPath = context.instancePath.replace(
- /\['?(.*?)'?\]/g,
- (_, segment) => `/${segment}`,
- );
+ const normalizedPath = normalizeAjvPath(context.instancePath);
// create mapping of deprecation description and data path of property
deprecationByDataPath.set(normalizedPath, deprecationDescription);
return true;
@@ -102,12 +101,24 @@ export function compileConfigSchemas(
const merged = mergeConfigSchemas(schemas.map(_ => _.value));
+ if (options?.noUndeclaredProperties) {
+ traverse(merged, (schema: SchemaObject) => {
+ /**
+ * The `additionalProperties` key can only be applied to `type: object` in the JSON
+ * schema.
+ */
+ if (schema?.type === 'object') {
+ schema.additionalProperties ||= false;
+ }
+ });
+ }
+
const validate = ajv.compile(merged);
const visibilityBySchemaPath = new Map();
traverse(merged, (schema, path) => {
if (schema.visibility && schema.visibility !== 'backend') {
- visibilityBySchemaPath.set(path, schema.visibility);
+ visibilityBySchemaPath.set(normalizeAjvPath(path), schema.visibility);
}
});
diff --git a/packages/config-loader/src/schema/filtering.ts b/packages/config-loader/src/schema/filtering.ts
index e9a0327e06..1c2d0c122a 100644
--- a/packages/config-loader/src/schema/filtering.ts
+++ b/packages/config-loader/src/schema/filtering.ts
@@ -21,6 +21,7 @@ import {
TransformFunc,
ValidationError,
} from './types';
+import { normalizeAjvPath } from './utils';
/**
* This filters data by visibility by discovering the visibility of each
@@ -163,7 +164,10 @@ export function filterErrorsByVisibility(
// We don't use this method for all the errors as the data path is more robust
// and doesn't require us to properly trim the schema path.
if (error.keyword === 'required') {
- const trimmedPath = error.schemaPath.slice(1, -'/required'.length);
+ const trimmedPath = normalizeAjvPath(error.schemaPath).slice(
+ 1,
+ -'/required'.length,
+ );
const fullPath = `${trimmedPath}/properties/${error.params.missingProperty}`;
if (
visibleSchemaPaths.some(visiblePath => visiblePath.startsWith(fullPath))
@@ -173,7 +177,8 @@ export function filterErrorsByVisibility(
}
const vis =
- visibilityByDataPath.get(error.instancePath) ?? DEFAULT_CONFIG_VISIBILITY;
+ visibilityByDataPath.get(normalizeAjvPath(error.instancePath)) ??
+ DEFAULT_CONFIG_VISIBILITY;
return vis && includeVisibilities.includes(vis);
});
}
diff --git a/packages/config-loader/src/schema/load.ts b/packages/config-loader/src/schema/load.ts
index 0309f64ea0..8ef971184f 100644
--- a/packages/config-loader/src/schema/load.ts
+++ b/packages/config-loader/src/schema/load.ts
@@ -25,6 +25,7 @@ import {
ConfigSchemaPackageEntry,
CONFIG_VISIBILITIES,
} from './types';
+import { normalizeAjvPath } from './utils';
/**
* Options that control the loading of configuration schema files in the backend.
@@ -32,12 +33,16 @@ import {
* @public
*/
export type LoadConfigSchemaOptions =
- | {
- dependencies: string[];
- packagePaths?: string[];
- }
- | {
- serialized: JsonObject;
+ | (
+ | {
+ dependencies: string[];
+ packagePaths?: string[];
+ }
+ | {
+ serialized: JsonObject;
+ }
+ ) & {
+ noUndeclaredProperties?: boolean;
};
function errorsToError(errors: ValidationError[]): Error {
@@ -45,7 +50,9 @@ function errorsToError(errors: ValidationError[]): Error {
const paramStr = Object.entries(params)
.map(([name, value]) => `${name}=${value}`)
.join(' ');
- return `Config ${message || ''} { ${paramStr} } at ${instancePath}`;
+ return `Config ${message || ''} { ${paramStr} } at ${normalizeAjvPath(
+ instancePath,
+ )}`;
});
const error = new Error(`Config validation failed, ${messages.join('; ')}`);
(error as any).messages = messages;
@@ -77,7 +84,9 @@ export async function loadConfigSchema(
schemas = serialized.schemas as ConfigSchemaPackageEntry[];
}
- const validate = compileConfigSchemas(schemas);
+ const validate = compileConfigSchemas(schemas, {
+ noUndeclaredProperties: options.noUndeclaredProperties,
+ });
return {
process(
diff --git a/packages/config-loader/src/schema/utils.ts b/packages/config-loader/src/schema/utils.ts
new file mode 100644
index 0000000000..25525a8c50
--- /dev/null
+++ b/packages/config-loader/src/schema/utils.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * AJV does some encoding to schema and instance paths. Revert those encodings to
+ * easier to read and parse values.
+ * See https://github.com/ajv-validator/ajv/blob/master/lib/compile/util.ts#L69.
+ *
+ * @param path Path from AJV.
+ * @returns Updated path with correct characters.
+ */
+export function normalizeAjvPath(path: string) {
+ return path
+ .replace(/~1/g, '/')
+ .replace(/\['?(.*?)'?\]/g, (_, segment) => `/${segment}`);
+}
diff --git a/packages/config-loader/src/sources/FileConfigSource.test.ts b/packages/config-loader/src/sources/FileConfigSource.test.ts
index c6980006c0..61cdeed272 100644
--- a/packages/config-loader/src/sources/FileConfigSource.test.ts
+++ b/packages/config-loader/src/sources/FileConfigSource.test.ts
@@ -66,7 +66,7 @@ describe('FileConfigSource', () => {
setTimeout(() => {
tmp.write('a.yaml', 'a: 2');
- }, 10);
+ }, 100);
await expect(readN(source, 2)).resolves.toEqual([
[{ data: { a: 1 }, context: 'a.yaml', path: tmp.resolve('a.yaml') }],
@@ -132,7 +132,7 @@ describe('FileConfigSource', () => {
setTimeout(() => {
tmp.write('x.yaml', '${MY_OTHER_VALUE}');
- }, 10);
+ }, 100);
await expect(readN(source, 2)).resolves.toEqual([
[{ data: { a: '6' }, context: 'a.yaml', path: tmp.resolve('a.yaml') }],
@@ -152,7 +152,7 @@ describe('FileConfigSource', () => {
setTimeout(() => {
tmp.write('x.txt', '9');
- }, 10);
+ }, 100);
await expect(readN(source, 2)).resolves.toEqual([
[{ data: { a: '8' }, context: 'a.yaml', path: tmp.resolve('a.yaml') }],
diff --git a/packages/config-loader/src/sources/RemoteConfigSource.ts b/packages/config-loader/src/sources/RemoteConfigSource.ts
index ff75060f56..3a8f6bf812 100644
--- a/packages/config-loader/src/sources/RemoteConfigSource.ts
+++ b/packages/config-loader/src/sources/RemoteConfigSource.ts
@@ -149,7 +149,7 @@ export class RemoteConfigSource implements ConfigSource {
signal: signal as import('node-fetch').RequestInit['signal'],
});
if (!res.ok) {
- throw ResponseError.fromResponse(res);
+ throw await ResponseError.fromResponse(res);
}
const content = await res.text();
diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md
index f4179e6fb6..f6df0da032 100644
--- a/packages/core-app-api/CHANGELOG.md
+++ b/packages/core-app-api/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/core-app-api
+## 1.8.0
+
+### Minor Changes
+
+- c89437db899: The analytics' `navigate` event will now include the route parameters as attributes of the navigate event
+
+### Patch Changes
+
+- b645d70034a: Fixed a bug in the Azure auth provider which prevented getting access tokens with multiple scopes for one resource
+- 42d817e76ab: Added `FrontendHostDiscovery` for config driven discovery implementation
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/types@1.0.2
+ - @backstage/version-bridge@1.0.4
+
## 1.8.0-next.1
### Minor Changes
diff --git a/packages/core-app-api/config.d.ts b/packages/core-app-api/config.d.ts
index 2de1d96114..e8cc879c90 100644
--- a/packages/core-app-api/config.d.ts
+++ b/packages/core-app-api/config.d.ts
@@ -117,8 +117,8 @@ export interface Config {
};
/**
- $ Enable redirect authentication flow type, instead of a popup for authentication
- * default value: 'false'
+ * Enable redirect authentication flow type, instead of a popup for authentication.
+ * @defaultValue false
* @visibility frontend
*/
enableExperimentalRedirectFlow?: boolean;
diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json
index 5d99754068..4416fa99e1 100644
--- a/packages/core-app-api/package.json
+++ b/packages/core-app-api/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-app-api",
"description": "Core app API used by Backstage apps",
- "version": "1.8.0-next.1",
+ "version": "1.8.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts
index c71799ed98..1c51dd0698 100644
--- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.test.ts
@@ -79,6 +79,16 @@ describe('MicrosoftAuth', () => {
'Requested access token with scopes from multiple Azure resources: one-resource, other-resource. Access tokens can only have a single audience.',
);
});
+
+ it('succeeds when requesting multiple scopes for the same resource', async () => {
+ const accessTokenPromise = microsoftAuth.getAccessToken(
+ 'same-resource/one-scope same-resource/other-scope',
+ );
+
+ await expect(accessTokenPromise).resolves.toEqual(
+ 'tokenForOtherResource',
+ );
+ });
});
describe('without a refresh token', () => {
diff --git a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts
index fdffb3fc45..c57c65d830 100644
--- a/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/microsoft/MicrosoftAuth.ts
@@ -91,10 +91,15 @@ export default class MicrosoftAuth {
}
private static resourceForScopes(scope: string): Promise {
- const audiences = scope
- .split(' ')
- .map(MicrosoftAuth.resourceForScope)
- .filter(aud => aud !== 'openid');
+ const audiences = [
+ ...new Set(
+ scope
+ .split(' ')
+ .map(MicrosoftAuth.resourceForScope)
+ .filter(aud => aud !== 'openid'),
+ ),
+ ];
+
if (audiences.length > 1) {
return Promise.reject(
new Error(
diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts
index 3d9005b0c0..c34407ca3a 100644
--- a/packages/core-app-api/src/app/types.ts
+++ b/packages/core-app-api/src/app/types.ts
@@ -236,21 +236,13 @@ export type AppOptions = {
* title: 'Light Theme',
* variant: 'light',
* icon: ,
- * Provider: ({ children }) => (
- *
- * {children}
- *
- * ),
+ * Provider: ({ children }) => ,
* }, {
* id: 'dark',
* title: 'Dark Theme',
* variant: 'dark',
* icon: ,
- * Provider: ({ children }) => (
- *
- * {children}
- *
- * ),
+ * Provider: ({ children }) => ,
* }]
* ```
*/
diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md
index 825b793d2c..8f3b626157 100644
--- a/packages/core-components/CHANGELOG.md
+++ b/packages/core-components/CHANGELOG.md
@@ -1,5 +1,42 @@
# @backstage/core-components
+## 0.13.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/version-bridge@1.0.4
+
+## 0.13.1
+
+### Patch Changes
+
+- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
+- e97769f7c0b: Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action.
+- b1f13cb38aa: Fix accessibility issue with Edit Metadata Link on screen readers missing notice about opening in a new tab.
+- 26cff1a5dfb: Start capturing sidebar click events in analytics by default.
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/version-bridge@1.0.4
+
+## 0.13.1-next.1
+
+### Patch Changes
+
+- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
+- e97769f7c0b: Fix accessibility issue on controlled select input on tab navigation + keyboard enter/space action.
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.13.1-next.0
### Patch Changes
diff --git a/packages/core-components/package.json b/packages/core-components/package.json
index cdb5a6e4a8..e3e27e492b 100644
--- a/packages/core-components/package.json
+++ b/packages/core-components/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
- "version": "0.13.1-next.0",
+ "version": "0.13.2-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/core-components/src/components/Link/Link.tsx b/packages/core-components/src/components/Link/Link.tsx
index be91753ea3..4d58fdfa77 100644
--- a/packages/core-components/src/components/Link/Link.tsx
+++ b/packages/core-components/src/components/Link/Link.tsx
@@ -188,6 +188,9 @@ export const Link = React.forwardRef(
-
+
Login Required
{authRedirect ? (
diff --git a/packages/core-components/src/components/Select/Select.test.tsx b/packages/core-components/src/components/Select/Select.test.tsx
index e9828e70fd..71159150e4 100644
--- a/packages/core-components/src/components/Select/Select.test.tsx
+++ b/packages/core-components/src/components/Select/Select.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { fireEvent, render } from '@testing-library/react';
+import { fireEvent, render, within } from '@testing-library/react';
import React from 'react';
import { SelectComponent as Select } from './Select';
@@ -45,7 +45,7 @@ describe('', () => {
expect(input.textContent).toBe('All results');
// Simulate click on input
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
expect(getByText('test 1')).toBeInTheDocument();
const option = getByText('test 1');
diff --git a/packages/core-components/src/components/Select/Select.tsx b/packages/core-components/src/components/Select/Select.tsx
index 1b57e0d4da..dbf5601112 100644
--- a/packages/core-components/src/components/Select/Select.tsx
+++ b/packages/core-components/src/components/Select/Select.tsx
@@ -16,7 +16,6 @@
import Box from '@material-ui/core/Box';
import Checkbox from '@material-ui/core/Checkbox';
import Chip from '@material-ui/core/Chip';
-import ClickAwayListener from '@material-ui/core/ClickAwayListener';
import FormControl from '@material-ui/core/FormControl';
import InputBase from '@material-ui/core/InputBase';
import InputLabel from '@material-ui/core/InputLabel';
@@ -171,7 +170,7 @@ export function SelectComponent(props: SelectProps) {
onChange(event.target.value as SelectedItems);
};
- const handleClick = (event: React.ChangeEvent) => {
+ const handleOpen = (event: React.ChangeEvent) => {
if (disabled) {
event.preventDefault();
return;
@@ -184,7 +183,7 @@ export function SelectComponent(props: SelectProps) {
});
};
- const handleClickAway = () => {
+ const handleClose = () => {
setOpen(false);
};
@@ -196,88 +195,85 @@ export function SelectComponent(props: SelectProps) {
return (
-
-
- {label}
- }
- label={label}
- tabIndex={0}
- renderValue={s =>
- multiple && (value as any[]).length !== 0 ? (
-
- {(s as string[]).map(selectedValue => (
- el.value === selectedValue)?.value}
- label={
- items.find(el => el.value === selectedValue)?.label
- }
- clickable
- onDelete={handleDelete(selectedValue)}
- className={classes.chip}
- />
- ))}
-
- ) : (
-
- {(value as any[]).length === 0
- ? placeholder || ''
- : items.find(el => el.value === s)?.label}
-
- )
- }
- IconComponent={() =>
- !isOpen ? :
- }
- MenuProps={{
- anchorOrigin: {
- vertical: 'bottom',
- horizontal: 'left',
- },
- transformOrigin: {
- vertical: 'top',
- horizontal: 'left',
- },
- getContentAnchorEl: null,
- }}
- >
- {placeholder && !multiple && (
-
- )}
- {native
- ? items &&
- items.map(item => (
-
- ))
- : items &&
- items.map(item => (
-
+
+ {label}
+ }
+ label={label}
+ tabIndex={0}
+ renderValue={s =>
+ multiple && (value as any[]).length !== 0 ? (
+
+ {(s as string[]).map(selectedValue => (
+ el.value === selectedValue)?.value}
+ label={items.find(el => el.value === selectedValue)?.label}
+ clickable
+ onDelete={handleDelete(selectedValue)}
+ className={classes.chip}
+ />
))}
-
-
-
+
+ ) : (
+
+ {(value as any[]).length === 0
+ ? placeholder || ''
+ : items.find(el => el.value === s)?.label}
+
+ )
+ }
+ IconComponent={() =>
+ !isOpen ? :
+ }
+ MenuProps={{
+ anchorOrigin: {
+ vertical: 'bottom',
+ horizontal: 'left',
+ },
+ transformOrigin: {
+ vertical: 'top',
+ horizontal: 'left',
+ },
+ getContentAnchorEl: null,
+ }}
+ >
+ {placeholder && !multiple && (
+
+ )}
+ {native
+ ? items &&
+ items.map(item => (
+
+ ))
+ : items &&
+ items.map(item => (
+
+ ))}
+
+
);
}
diff --git a/packages/core-components/src/components/Table/Table.tsx b/packages/core-components/src/components/Table/Table.tsx
index 848c85eb60..5cea3dd6b6 100644
--- a/packages/core-components/src/components/Table/Table.tsx
+++ b/packages/core-components/src/components/Table/Table.tsx
@@ -105,7 +105,6 @@ const StyledMTableHeader = withStyles(
borderTop: `1px solid ${theme.palette.grey.A100}`,
borderBottom: `1px solid ${theme.palette.grey.A100}`,
// withStyles hasn't a generic overload for theme
- color: (theme as BackstageTheme).palette.textSubtle,
fontWeight: theme.typography.fontWeightBold,
position: 'static',
wordBreak: 'normal',
diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md
index 948c4017a3..52639751e7 100644
--- a/packages/create-app/CHANGELOG.md
+++ b/packages/create-app/CHANGELOG.md
@@ -1,5 +1,28 @@
# @backstage/create-app
+## 0.5.2-next.0
+
+### Patch Changes
+
+- Bumped create-app version.
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+
+## 0.5.1
+
+### Patch Changes
+
+- 1d5e42655cd: Correct command to create new plugins
+- e04bb20bdc4: Bumped create-app version.
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+
+## 0.5.1-next.2
+
+### Patch Changes
+
+- 1d5e42655cd: Correct command to create new plugins
+
## 0.5.1-next.1
### Patch Changes
@@ -2595,7 +2618,7 @@
fields that clarify a previous grey area such that the intent of the alteration
is made explicit. This is caught by `tsc` and your editor if you are using the
`@ts-check` and `@param` syntax in your migration files
- ([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
+ ([example](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
which we strongly recommend.
See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more
@@ -2665,7 +2688,7 @@
fields that clarify a previous grey area such that the intent of the alteration
is made explicit. This is caught by `tsc` and your editor if you are using the
`@ts-check` and `@param` syntax in your migration files
- ([example](https://github.com/backstage/backstage/blob/master/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
+ ([example](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/catalog-backend/migrations/20220116144621_remove_legacy.js#L17)),
which we strongly recommend.
See the [`knex` documentation](https://knexjs.org/#Schema-alter) for more
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index ef61e1c5b8..08b67c7ea1 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
- "version": "0.5.1-next.1",
+ "version": "0.5.2-next.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/create-app/templates/default-app/package.json.hbs b/packages/create-app/templates/default-app/package.json.hbs
index f00500af68..0682114a35 100644
--- a/packages/create-app/templates/default-app/package.json.hbs
+++ b/packages/create-app/templates/default-app/package.json.hbs
@@ -39,10 +39,7 @@
},
"resolutions": {
"@types/react": "^17",
- "@types/react-dom": "^17",
- "@octokit/rest": "18.5.6",
- "@octokit/plugin-rest-endpoint-methods": "5.16.2",
- "@octokit/plugin-paginate-rest": "2.7.0"
+ "@types/react-dom": "^17"
},
"prettier": "@spotify/prettier-config",
"lint-staged": {
diff --git a/packages/create-app/templates/default-app/plugins/README.md b/packages/create-app/templates/default-app/plugins/README.md
index 58dc32cb08..d7865fdba3 100644
--- a/packages/create-app/templates/default-app/plugins/README.md
+++ b/packages/create-app/templates/default-app/plugins/README.md
@@ -4,6 +4,6 @@ This is where your own plugins and their associated modules live, each in a
separate folder of its own.
If you want to create a new plugin here, go to your project root directory, run
-the command `yarn backstage-cli create`, and follow the on-screen instructions.
+the command `yarn new`, and follow the on-screen instructions.
You can also check out existing plugins on [the plugin marketplace](https://backstage.io/plugins)!
diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md
index a6aeda0901..75a72c7f1d 100644
--- a/packages/dev-utils/CHANGELOG.md
+++ b/packages/dev-utils/CHANGELOG.md
@@ -1,5 +1,49 @@
# @backstage/dev-utils
+## 1.0.16-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/test-utils@1.4.0-next.0
+ - @backstage/app-defaults@1.4.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-app-api@1.8.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/integration-react@1.1.14-next.0
+ - @backstage/catalog-model@1.3.0
+
+## 1.0.15
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/app-defaults@1.3.1
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-app-api@1.8.0
+ - @backstage/core-components@0.13.1
+ - @backstage/integration-react@1.1.13
+ - @backstage/test-utils@1.3.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 1.0.15-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 1.0.15-next.1
### Patch Changes
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index ca5057c928..e3b057395e 100644
--- a/packages/dev-utils/package.json
+++ b/packages/dev-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/dev-utils",
"description": "Utilities for developing Backstage plugins.",
- "version": "1.0.15-next.1",
+ "version": "1.0.16-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/e2e-test/CHANGELOG.md b/packages/e2e-test/CHANGELOG.md
index 2486a83f5c..363337002e 100644
--- a/packages/e2e-test/CHANGELOG.md
+++ b/packages/e2e-test/CHANGELOG.md
@@ -1,5 +1,30 @@
# e2e-test
+## 0.2.4-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.2-next.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.1.5
+
+## 0.2.3
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.1.5
+
+## 0.2.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.5.1-next.2
+
## 0.2.3-next.1
### Patch Changes
diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json
index 513a9c6a96..e17f320c31 100644
--- a/packages/e2e-test/package.json
+++ b/packages/e2e-test/package.json
@@ -1,7 +1,7 @@
{
"name": "e2e-test",
"description": "E2E test for verifying Backstage packages",
- "version": "0.2.3-next.1",
+ "version": "0.2.4-next.0",
"private": true,
"backstage": {
"role": "cli"
diff --git a/packages/integration-aws-node/CHANGELOG.md b/packages/integration-aws-node/CHANGELOG.md
index 40192bdfaf..181b08c4ee 100644
--- a/packages/integration-aws-node/CHANGELOG.md
+++ b/packages/integration-aws-node/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/integration-aws-node
+## 0.1.3
+
+### Patch Changes
+
+- 3659c71c5d9: Standardize `@aws-sdk` v3 versions
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 0.1.2
### Patch Changes
diff --git a/packages/integration-aws-node/package.json b/packages/integration-aws-node/package.json
index 7afc31cc47..8eec21a143 100644
--- a/packages/integration-aws-node/package.json
+++ b/packages/integration-aws-node/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-aws-node",
"description": "Helpers for fetching AWS account credentials",
- "version": "0.1.2",
+ "version": "0.1.3",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
@@ -32,11 +32,11 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
- "@aws-sdk/client-sts": "^3.208.0",
- "@aws-sdk/credential-provider-node": "^3.208.0",
- "@aws-sdk/credential-providers": "^3.208.0",
- "@aws-sdk/types": "^3.208.0",
- "@aws-sdk/util-arn-parser": "^3.208.0",
+ "@aws-sdk/client-sts": "^3.310.0",
+ "@aws-sdk/credential-provider-node": "^3.310.0",
+ "@aws-sdk/credential-providers": "^3.310.0",
+ "@aws-sdk/types": "^3.310.0",
+ "@aws-sdk/util-arn-parser": "^3.310.0",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^"
},
diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md
index ac62e9aab7..7a893b10b7 100644
--- a/packages/integration-react/CHANGELOG.md
+++ b/packages/integration-react/CHANGELOG.md
@@ -1,5 +1,37 @@
# @backstage/integration-react
+## 1.1.14-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 1.1.13
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/integration@1.4.5
+ - @backstage/core-components@0.13.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## 1.1.13-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 1.1.13-next.1
### Patch Changes
diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json
index e59c8f48c4..086d616510 100644
--- a/packages/integration-react/package.json
+++ b/packages/integration-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration-react",
"description": "Frontend package for managing integrations towards external systems",
- "version": "1.1.13-next.1",
+ "version": "1.1.14-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/integration/CHANGELOG.md b/packages/integration/CHANGELOG.md
index 92eff86bb5..7dcef4fbc9 100644
--- a/packages/integration/CHANGELOG.md
+++ b/packages/integration/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/integration
+## 1.4.5
+
+### Patch Changes
+
+- b026275bcc8: Fixed a bug where the wrong credentials would be selected when using multiple GitHub app integrations.
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 1.4.5-next.0
### Patch Changes
diff --git a/packages/integration/package.json b/packages/integration/package.json
index fbe8ac1fc3..14b64b3089 100644
--- a/packages/integration/package.json
+++ b/packages/integration/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/integration",
"description": "Helpers for managing integrations towards external systems",
- "version": "1.4.5-next.0",
+ "version": "1.4.5",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md
index e31c05532c..7f1ece48d4 100644
--- a/packages/repo-tools/CHANGELOG.md
+++ b/packages/repo-tools/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/repo-tools
+## 0.3.0
+
+### Minor Changes
+
+- 799c33047ed: **BREAKING**: The OpenAPI commands are now found within the `schema openapi` sub-command. For example `yarn backstage-repo-tools schema:openapi:verify` is now `yarn backstage-repo-tools schema openapi verify`.
+- 27956d78671: Generated OpenAPI files now have a `.generated.ts` file name and a warning header at the top, to highlight that they should not be edited by hand.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/cli-node@0.1.0
+ - @backstage/errors@1.1.5
+
## 0.3.0-next.0
### Minor Changes
diff --git a/packages/repo-tools/cli-report.md b/packages/repo-tools/cli-report.md
index b361bac02f..789b4bc407 100644
--- a/packages/repo-tools/cli-report.md
+++ b/packages/repo-tools/cli-report.md
@@ -14,8 +14,7 @@ Options:
Commands:
api-reports [options] [paths...]
type-deps
- schema:openapi:verify [paths...]
- schema:openapi:generate [paths...]
+ schema [command]
help [command]
```
@@ -37,19 +36,46 @@ Options:
-h, --help
```
-### `backstage-repo-tools schema:openapi:generate`
+### `backstage-repo-tools schema`
```
-Usage: backstage-repo-tools schema:openapi:generate [options] [paths...]
+Usage: backstage-repo-tools schema [options] [command] [command]
+
+Options:
+ -h, --help
+
+Commands:
+ openapi [command]
+ help [command]
+```
+
+### `backstage-repo-tools schema openapi`
+
+```
+Usage: backstage-repo-tools schema openapi [options] [command] [command]
+
+Options:
+ -h, --help
+
+Commands:
+ verify [paths...]
+ generate [paths...]
+ help [command]
+```
+
+### `backstage-repo-tools schema openapi generate`
+
+```
+Usage: backstage-repo-tools schema openapi generate [options] [paths...]
Options:
-h, --help
```
-### `backstage-repo-tools schema:openapi:verify`
+### `backstage-repo-tools schema openapi verify`
```
-Usage: backstage-repo-tools schema:openapi:verify [options] [paths...]
+Usage: backstage-repo-tools schema openapi verify [options] [paths...]
Options:
-h, --help
diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json
index 74c39fc9a8..85f683a999 100644
--- a/packages/repo-tools/package.json
+++ b/packages/repo-tools/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/repo-tools",
"description": "CLI for Backstage repo tooling ",
- "version": "0.3.0-next.0",
+ "version": "0.3.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/repo-tools/src/commands/index.ts b/packages/repo-tools/src/commands/index.ts
index f4d02df3bc..a8324c7d33 100644
--- a/packages/repo-tools/src/commands/index.ts
+++ b/packages/repo-tools/src/commands/index.ts
@@ -18,6 +18,30 @@ import { assertError } from '@backstage/errors';
import { Command } from 'commander';
import { exitWithError } from '../lib/errors';
+function registerSchemaCommand(program: Command) {
+ const command = program
+ .command('schema [command]')
+ .description('Various tools for working with API schema');
+
+ const openApiCommand = command
+ .command('openapi [command]')
+ .description('Tooling for OpenApi schema');
+
+ openApiCommand
+ .command('verify [paths...]')
+ .description(
+ 'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.generated.ts` file.',
+ )
+ .action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand)));
+
+ openApiCommand
+ .command('generate [paths...]')
+ .description(
+ 'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.',
+ )
+ .action(lazy(() => import('./openapi/generate').then(m => m.bulkCommand)));
+}
+
export function registerCommands(program: Command) {
program
.command('api-reports [paths...]')
@@ -63,19 +87,7 @@ export function registerCommands(program: Command) {
.description('Find inconsistencies in types of all packages and plugins')
.action(lazy(() => import('./type-deps/type-deps').then(m => m.default)));
- program
- .command('schema:openapi:verify [paths...]')
- .description(
- 'Verify that all OpenAPI schemas are valid and have a matching `schemas/openapi.generated.ts` file.',
- )
- .action(lazy(() => import('./openapi/verify').then(m => m.bulkCommand)));
-
- program
- .command('schema:openapi:generate [paths...]')
- .description(
- 'Generates a Typescript file from an OpenAPI yaml spec. For use with the `@backstage/backend-openapi-utils` ApiRouter type.',
- )
- .action(lazy(() => import('./openapi/generate').then(m => m.bulkCommand)));
+ registerSchemaCommand(program);
}
// Wraps an action function so that it always exits and handles errors
diff --git a/packages/repo-tools/src/commands/openapi/verify.ts b/packages/repo-tools/src/commands/openapi/verify.ts
index f6dda66738..4c4f310f73 100644
--- a/packages/repo-tools/src/commands/openapi/verify.ts
+++ b/packages/repo-tools/src/commands/openapi/verify.ts
@@ -47,7 +47,7 @@ async function verify(directoryPath: string) {
if (!isEqual(schema.default, yaml)) {
const path = relativePath(cliPaths.targetRoot, directoryPath);
throw new Error(
- `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools schema:openapi:generate ${path}\` to regenerate \`${TS_SCHEMA_PATH}\`.`,
+ `\`${YAML_SCHEMA_PATH}\` and \`${TS_SCHEMA_PATH}\` do not match. Please run \`yarn backstage-repo-tools schema openapi generate ${path}\` to regenerate \`${TS_SCHEMA_PATH}\`.`,
);
}
}
diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md
index 98bf3668d6..798ceb5317 100644
--- a/packages/techdocs-cli-embedded-app/CHANGELOG.md
+++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md
@@ -1,5 +1,61 @@
# techdocs-cli-embedded-app
+## 0.2.83-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-techdocs@1.6.3-next.0
+ - @backstage/test-utils@1.4.0-next.0
+ - @backstage/app-defaults@1.4.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/cli@0.22.8-next.0
+ - @backstage/plugin-techdocs-react@1.1.7-next.0
+ - @backstage/plugin-catalog@1.11.1-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/integration-react@1.1.14-next.0
+ - @backstage/catalog-model@1.3.0
+
+## 0.2.82
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-techdocs@1.6.2
+ - @backstage/app-defaults@1.3.1
+ - @backstage/cli@0.22.7
+ - @backstage/plugin-catalog@1.11.0
+ - @backstage/core-app-api@1.8.0
+ - @backstage/core-components@0.13.1
+ - @backstage/integration-react@1.1.13
+ - @backstage/test-utils@1.3.1
+ - @backstage/plugin-techdocs-react@1.1.6
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.2.82-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/app-defaults@1.3.1-next.2
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-techdocs@1.6.2-next.2
+ - @backstage/plugin-techdocs-react@1.1.6-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.82-next.1
### Patch Changes
diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json
index 01f5f8128c..27da72de5f 100644
--- a/packages/techdocs-cli-embedded-app/package.json
+++ b/packages/techdocs-cli-embedded-app/package.json
@@ -1,6 +1,6 @@
{
"name": "techdocs-cli-embedded-app",
- "version": "0.2.82-next.1",
+ "version": "0.2.83-next.0",
"private": true,
"backstage": {
"role": "frontend"
diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md
index 35f3b4bc28..49cf10e01a 100644
--- a/packages/techdocs-cli/CHANGELOG.md
+++ b/packages/techdocs-cli/CHANGELOG.md
@@ -1,5 +1,27 @@
# @techdocs/cli
+## 1.4.3-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.3.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/plugin-techdocs-node@1.7.2-next.0
+
+## 1.4.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/plugin-techdocs-node@1.7.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.7
+
## 1.4.2-next.1
### Patch Changes
diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json
index 8b533b4453..5d25f057f6 100644
--- a/packages/techdocs-cli/package.json
+++ b/packages/techdocs-cli/package.json
@@ -1,7 +1,7 @@
{
"name": "@techdocs/cli",
"description": "Utility CLI for managing TechDocs sites in Backstage.",
- "version": "1.4.2-next.1",
+ "version": "1.4.3-next.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md
index 9fd73a6840..4463f5646c 100644
--- a/packages/test-utils/CHANGELOG.md
+++ b/packages/test-utils/CHANGELOG.md
@@ -1,5 +1,46 @@
# @backstage/test-utils
+## 1.4.0-next.0
+
+### Minor Changes
+
+- 1fd38bc4141a: Test App Wrapper is now using `UnifiedThemeProvider` for supporting MUI v5 next to MUI v4 in tests.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+ - @backstage/types@1.0.2
+ - @backstage/plugin-permission-common@0.7.5
+
+## 1.3.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/core-app-api@1.8.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/types@1.0.2
+ - @backstage/plugin-permission-common@0.7.5
+ - @backstage/plugin-permission-react@0.4.12
+
+## 1.3.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-app-api@1.8.0-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
## 1.3.1-next.1
### Patch Changes
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index 80d5a2e620..992075922e 100644
--- a/packages/test-utils/package.json
+++ b/packages/test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/test-utils",
"description": "Utilities to test Backstage plugins and apps.",
- "version": "1.3.1-next.1",
+ "version": "1.4.0-next.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/test-utils/src/testUtils/appWrappers.tsx b/packages/test-utils/src/testUtils/appWrappers.tsx
index 8b644ad2d1..6545cc312a 100644
--- a/packages/test-utils/src/testUtils/appWrappers.tsx
+++ b/packages/test-utils/src/testUtils/appWrappers.tsx
@@ -17,9 +17,7 @@
import React, { ComponentType, ReactNode, ReactElement } from 'react';
import { MemoryRouter } from 'react-router-dom';
import { Route } from 'react-router-dom';
-import { lightTheme } from '@backstage/theme';
-import { ThemeProvider } from '@material-ui/core/styles';
-import { CssBaseline } from '@material-ui/core';
+import { UnifiedThemeProvider, themes } from '@backstage/theme';
import MockIcon from '@material-ui/icons/AcUnit';
import { createSpecializedApp } from '@backstage/core-app-api';
import {
@@ -142,9 +140,9 @@ export function createTestAppWrapper(
title: 'Test App Theme',
variant: 'light',
Provider: ({ children }) => (
-
- {children}
-
+
+ {children}
+
),
},
],
diff --git a/packages/theme/CHANGELOG.md b/packages/theme/CHANGELOG.md
index e2490f346e..bb1e52b792 100644
--- a/packages/theme/CHANGELOG.md
+++ b/packages/theme/CHANGELOG.md
@@ -1,5 +1,42 @@
# @backstage/theme
+## 0.4.0-next.0
+
+### Minor Changes
+
+- 1fd38bc4141a: **MUI v5 Support:** Adding platform-wide support for MUI v5 allowing a transition phase for migrating central plugins & components over. We still support v4 instances & plugins by adding a
+
+ To allow the future support of plugins & components using MUI v5 you want to upgrade your `AppTheme`'s to using the `UnifiedThemeProvider`
+
+ ```diff
+ Provider: ({ children }) => (
+ -
+ - {children}
+ -
+ +
+ ),
+ ```
+
+## 0.3.0
+
+### Minor Changes
+
+- 98c0c199b15: Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493.
+
+### Patch Changes
+
+- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
+
+## 0.3.0-next.0
+
+### Minor Changes
+
+- 98c0c199b15: Updates light theme's primary foreground and `running` status indicator colours to meet WCAG. Previously #2E77D0 changed to #1F5493.
+
+### Patch Changes
+
+- 83b45f9df50: Fix accessibility issue with Backstage Table's header style
+
## 0.2.19
### Patch Changes
diff --git a/packages/theme/api-report.md b/packages/theme/api-report.md
index 863ab81b68..fe05ca6b4e 100644
--- a/packages/theme/api-report.md
+++ b/packages/theme/api-report.md
@@ -3,13 +3,19 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+import type { ComponentsProps } from '@material-ui/core/styles/props';
import { Overrides } from '@material-ui/core/styles/overrides';
-import { Palette } from '@material-ui/core/styles/createPalette';
-import { PaletteOptions } from '@material-ui/core/styles/createPalette';
-import { Theme } from '@material-ui/core';
-import { ThemeOptions } from '@material-ui/core';
+import type { Palette } from '@material-ui/core/styles/createPalette';
+import type { PaletteOptions } from '@material-ui/core/styles/createPalette';
+import { PaletteOptions as PaletteOptions_2 } from '@mui/material/styles';
+import { ReactNode } from 'react';
+import { Theme } from '@mui/material/styles';
+import { Theme as Theme_2 } from '@material-ui/core';
+import { ThemeOptions } from '@mui/material/styles';
+import { ThemeOptions as ThemeOptions_2 } from '@material-ui/core/styles';
+import type { ThemeOptions as ThemeOptions_3 } from '@material-ui/core';
-// @public
+// @public @deprecated
export type BackstagePalette = Palette & BackstagePaletteAdditions;
// @public
@@ -75,12 +81,12 @@ export type BackstagePaletteAdditions = {
};
};
-// @public
+// @public @deprecated
export type BackstagePaletteOptions = PaletteOptions &
BackstagePaletteAdditions;
-// @public
-export interface BackstageTheme extends Theme {
+// @public @deprecated
+export interface BackstageTheme extends Theme_2 {
// (undocumented)
getPageTheme: (selector: PageThemeSelector) => PageTheme;
// (undocumented)
@@ -90,7 +96,13 @@ export interface BackstageTheme extends Theme {
}
// @public
-export interface BackstageThemeOptions extends ThemeOptions {
+export type BackstageThemeAdditions = {
+ page: PageTheme;
+ getPageTheme: (selector: PageThemeSelector) => PageTheme;
+};
+
+// @public @deprecated
+export interface BackstageThemeOptions extends ThemeOptions_3 {
// (undocumented)
getPageTheme: (selector: PageThemeSelector) => PageTheme;
// (undocumented)
@@ -99,22 +111,86 @@ export interface BackstageThemeOptions extends ThemeOptions {
palette: BackstagePaletteOptions;
}
+// @public
+export interface BaseThemeOptionsInput {
+ // (undocumented)
+ defaultPageTheme?: string;
+ // (undocumented)
+ fontFamily?: string;
+ // (undocumented)
+ htmlFontSize?: number;
+ // (undocumented)
+ pageTheme?: Record;
+ // (undocumented)
+ palette: PaletteOptions;
+}
+
// @public
export const colorVariants: Record;
// @public
-export function createTheme(options: SimpleThemeOptions): BackstageTheme;
+export function createBaseThemeOptions(
+ options: BaseThemeOptionsInput,
+): {
+ palette: PaletteOptions;
+ typography: {
+ htmlFontSize: number;
+ fontFamily: string;
+ h1: {
+ fontSize: number;
+ fontWeight: number;
+ marginBottom: number;
+ };
+ h2: {
+ fontSize: number;
+ fontWeight: number;
+ marginBottom: number;
+ };
+ h3: {
+ fontSize: number;
+ fontWeight: number;
+ marginBottom: number;
+ };
+ h4: {
+ fontWeight: number;
+ fontSize: number;
+ marginBottom: number;
+ };
+ h5: {
+ fontWeight: number;
+ fontSize: number;
+ marginBottom: number;
+ };
+ h6: {
+ fontWeight: number;
+ fontSize: number;
+ marginBottom: number;
+ };
+ };
+ page: PageTheme;
+ getPageTheme: ({ themeId }: PageThemeSelector) => PageTheme;
+};
+
+// @public @deprecated
+export function createTheme(options: SimpleThemeOptions): Theme_2;
+
+// @public @deprecated
+export function createThemeOptions(options: SimpleThemeOptions): ThemeOptions_3;
+
+// @public @deprecated
+export function createThemeOverrides(theme: Theme_2): Overrides;
// @public
-export function createThemeOptions(
- options: SimpleThemeOptions,
-): BackstageThemeOptions;
+export function createUnifiedTheme(options: UnifiedThemeOptions): UnifiedTheme;
// @public
-export function createThemeOverrides(theme: BackstageTheme): Overrides;
+export function createUnifiedThemeFromV4(options: ThemeOptions_2): UnifiedTheme;
+
+// @public @deprecated
+export const darkTheme: Theme_2;
// @public
-export const darkTheme: BackstageTheme;
+export const defaultComponentThemes: ThemeOptions['components'];
// @public
export function genPageTheme(props: {
@@ -125,8 +201,8 @@ export function genPageTheme(props: {
};
}): PageTheme;
-// @public
-export const lightTheme: BackstageTheme;
+// @public @deprecated
+export const lightTheme: Theme_2;
// @public
export type PageTheme = {
@@ -145,14 +221,214 @@ export type PageThemeSelector = {
};
// @public
-export const shapes: Record;
+export const palettes: {
+ light: {
+ type: 'light';
+ mode: 'light';
+ background: {
+ default: string;
+ paper: string;
+ };
+ status: {
+ ok: string;
+ warning: string;
+ error: string;
+ running: string;
+ pending: string;
+ aborted: string;
+ };
+ bursts: {
+ fontColor: string;
+ slackChannelText: string;
+ backgroundColor: {
+ default: string;
+ };
+ gradient: {
+ linear: string;
+ };
+ };
+ primary: {
+ main: string;
+ };
+ banner: {
+ info: string;
+ error: string;
+ text: string;
+ link: string;
+ closeButtonColor: string;
+ warning: string;
+ };
+ border: string;
+ textContrast: string;
+ textVerySubtle: string;
+ textSubtle: string;
+ highlight: string;
+ errorBackground: string;
+ warningBackground: string;
+ infoBackground: string;
+ errorText: string;
+ infoText: string;
+ warningText: string;
+ linkHover: string;
+ link: string;
+ gold: string;
+ navigation: {
+ background: string;
+ indicator: string;
+ color: string;
+ selectedColor: string;
+ navItem: {
+ hoverBackground: string;
+ };
+ submenu: {
+ background: string;
+ };
+ };
+ pinSidebarButton: {
+ icon: string;
+ background: string;
+ };
+ tabbar: {
+ indicator: string;
+ };
+ };
+ dark: {
+ type: 'dark';
+ mode: 'dark';
+ background: {
+ default: string;
+ paper: string;
+ };
+ status: {
+ ok: string;
+ warning: string;
+ error: string;
+ running: string;
+ pending: string;
+ aborted: string;
+ };
+ bursts: {
+ fontColor: string;
+ slackChannelText: string;
+ backgroundColor: {
+ default: string;
+ };
+ gradient: {
+ linear: string;
+ };
+ };
+ primary: {
+ main: string;
+ dark: string;
+ };
+ secondary: {
+ main: string;
+ };
+ banner: {
+ info: string;
+ error: string;
+ text: string;
+ link: string;
+ closeButtonColor: string;
+ warning: string;
+ };
+ border: string;
+ textContrast: string;
+ textVerySubtle: string;
+ textSubtle: string;
+ highlight: string;
+ errorBackground: string;
+ warningBackground: string;
+ infoBackground: string;
+ errorText: string;
+ infoText: string;
+ warningText: string;
+ linkHover: string;
+ link: string;
+ gold: string;
+ navigation: {
+ background: string;
+ indicator: string;
+ color: string;
+ selectedColor: string;
+ navItem: {
+ hoverBackground: string;
+ };
+ submenu: {
+ background: string;
+ };
+ };
+ pinSidebarButton: {
+ icon: string;
+ background: string;
+ };
+ tabbar: {
+ indicator: string;
+ };
+ };
+};
// @public
+export const shapes: Record;
+
+// @public @deprecated
export type SimpleThemeOptions = {
- palette: BackstagePaletteOptions;
- defaultPageTheme: string;
+ palette: PaletteOptions;
+ defaultPageTheme?: string;
pageTheme?: Record;
fontFamily?: string;
htmlFontSize?: number;
};
+
+// @public
+export const themes: {
+ light: UnifiedTheme;
+ dark: UnifiedTheme;
+};
+
+// @public
+export function transformV5ComponentThemesToV4(
+ theme: Theme,
+ components?: ThemeOptions['components'],
+): {
+ overrides: Overrides;
+ props: ComponentsProps;
+};
+
+// @public
+export interface UnifiedTheme {
+ // (undocumented)
+ getTheme(version: string): unknown | undefined;
+}
+
+// @public
+export interface UnifiedThemeOptions {
+ // (undocumented)
+ components?: ThemeOptions['components'];
+ // (undocumented)
+ defaultPageTheme?: string;
+ // (undocumented)
+ fontFamily?: string;
+ // (undocumented)
+ htmlFontSize?: number;
+ // (undocumented)
+ pageTheme?: Record;
+ // (undocumented)
+ palette: PaletteOptions & PaletteOptions_2;
+}
+
+// @public
+export function UnifiedThemeProvider(
+ props: UnifiedThemeProviderProps,
+): JSX.Element;
+
+// @public
+export interface UnifiedThemeProviderProps {
+ // (undocumented)
+ children: ReactNode;
+ // (undocumented)
+ noCssBaseline?: boolean;
+ // (undocumented)
+ theme: UnifiedTheme;
+}
```
diff --git a/packages/theme/package.json b/packages/theme/package.json
index e102c067d7..dd90f5c834 100644
--- a/packages/theme/package.json
+++ b/packages/theme/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/theme",
"description": "material-ui theme for use with Backstage.",
- "version": "0.2.19",
+ "version": "0.4.0-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
@@ -32,9 +32,13 @@
"test": "backstage-cli package test"
},
"dependencies": {
- "@material-ui/core": "^4.12.2"
+ "@emotion/react": "^11.10.5",
+ "@emotion/styled": "^11.10.5",
+ "@mui/material": "^5.12.2"
},
"peerDependencies": {
+ "@material-ui/core": "^4.12.2",
+ "@types/react": "^16.13.1 || ^17.0.0",
"react": "^16.13.1 || ^17.0.0",
"react-dom": "^16.13.1 || ^17.0.0"
},
diff --git a/packages/theme/src/base/createBaseThemeOptions.ts b/packages/theme/src/base/createBaseThemeOptions.ts
new file mode 100644
index 0000000000..e54e920deb
--- /dev/null
+++ b/packages/theme/src/base/createBaseThemeOptions.ts
@@ -0,0 +1,98 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { PageTheme, PageThemeSelector } from './types';
+import { pageTheme as defaultPageThemes } from './pageTheme';
+
+const DEFAULT_HTML_FONT_SIZE = 16;
+const DEFAULT_FONT_FAMILY =
+ '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif';
+const DEFAULT_PAGE_THEME = 'home';
+
+/**
+ * Options for {@link createBaseThemeOptions}.
+ *
+ * @public
+ */
+export interface BaseThemeOptionsInput {
+ palette: PaletteOptions;
+ defaultPageTheme?: string;
+ pageTheme?: Record;
+ fontFamily?: string;
+ htmlFontSize?: number;
+}
+
+/**
+ * A helper for creating theme options.
+ *
+ * @public
+ */
+export function createBaseThemeOptions(
+ options: BaseThemeOptionsInput,
+) {
+ const {
+ palette,
+ htmlFontSize = DEFAULT_HTML_FONT_SIZE,
+ fontFamily = DEFAULT_FONT_FAMILY,
+ defaultPageTheme = DEFAULT_PAGE_THEME,
+ pageTheme = defaultPageThemes,
+ } = options;
+
+ if (!pageTheme[defaultPageTheme]) {
+ throw new Error(`${defaultPageTheme} is not defined in pageTheme.`);
+ }
+
+ return {
+ palette,
+ typography: {
+ htmlFontSize,
+ fontFamily,
+ h1: {
+ fontSize: 54,
+ fontWeight: 700,
+ marginBottom: 10,
+ },
+ h2: {
+ fontSize: 40,
+ fontWeight: 700,
+ marginBottom: 8,
+ },
+ h3: {
+ fontSize: 32,
+ fontWeight: 700,
+ marginBottom: 6,
+ },
+ h4: {
+ fontWeight: 700,
+ fontSize: 28,
+ marginBottom: 6,
+ },
+ h5: {
+ fontWeight: 700,
+ fontSize: 24,
+ marginBottom: 4,
+ },
+ h6: {
+ fontWeight: 700,
+ fontSize: 20,
+ marginBottom: 2,
+ },
+ },
+ page: pageTheme[defaultPageTheme],
+ getPageTheme: ({ themeId }: PageThemeSelector) =>
+ pageTheme[themeId] ?? pageTheme[defaultPageTheme],
+ };
+}
diff --git a/packages/theme/src/base/index.ts b/packages/theme/src/base/index.ts
new file mode 100644
index 0000000000..4bb80aa42b
--- /dev/null
+++ b/packages/theme/src/base/index.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { createBaseThemeOptions } from './createBaseThemeOptions';
+export type { BaseThemeOptionsInput } from './createBaseThemeOptions';
+export { colorVariants, genPageTheme, pageTheme, shapes } from './pageTheme';
+export { palettes } from './palettes';
+export type {
+ BackstageThemeAdditions,
+ BackstagePaletteAdditions,
+ PageTheme,
+ PageThemeSelector,
+} from './types';
diff --git a/packages/theme/src/pageTheme.ts b/packages/theme/src/base/pageTheme.ts
similarity index 99%
rename from packages/theme/src/pageTheme.ts
rename to packages/theme/src/base/pageTheme.ts
index dd36d7641b..12a73b6089 100644
--- a/packages/theme/src/pageTheme.ts
+++ b/packages/theme/src/base/pageTheme.ts
@@ -13,6 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
import { PageTheme } from './types';
/**
diff --git a/packages/theme/src/themes.ts b/packages/theme/src/base/palettes.ts
similarity index 86%
rename from packages/theme/src/themes.ts
rename to packages/theme/src/base/palettes.ts
index eb2ef7ce41..5e70afd7b8 100644
--- a/packages/theme/src/themes.ts
+++ b/packages/theme/src/base/palettes.ts
@@ -14,26 +14,24 @@
* limitations under the License.
*/
-import { createTheme } from './baseTheme';
-import { pageTheme } from './pageTheme';
-import { yellow } from '@material-ui/core/colors';
-
/**
- * The default Backstage light theme.
+ * Built-in Backstage color palettes.
*
* @public
*/
-export const lightTheme = createTheme({
- palette: {
- type: 'light',
+export const palettes = {
+ light: {
+ type: 'light' as const,
+ mode: 'light' as const,
background: {
default: '#F8F8F8',
+ paper: '#FFFFFF',
},
status: {
ok: '#1DB954',
warning: '#FF9800',
error: '#E22134',
- running: '#2E77D0',
+ running: '#1F5493',
pending: '#FFED51',
aborted: '#757575',
},
@@ -48,7 +46,7 @@ export const lightTheme = createTheme({
},
},
primary: {
- main: '#2E77D0',
+ main: '#1F5493',
},
banner: {
info: '#2E77D0',
@@ -71,7 +69,7 @@ export const lightTheme = createTheme({
warningText: '#000000',
linkHover: '#2196F3',
link: '#0A6EBE',
- gold: yellow.A700,
+ gold: '#FFD600',
navigation: {
background: '#171717',
indicator: '#9BF0E1',
@@ -92,20 +90,12 @@ export const lightTheme = createTheme({
indicator: '#9BF0E1',
},
},
- defaultPageTheme: 'home',
- pageTheme,
-});
-
-/**
- * The default Backstage dark theme.
- *
- * @public
- */
-export const darkTheme = createTheme({
- palette: {
- type: 'dark',
+ dark: {
+ type: 'dark' as const,
+ mode: 'dark' as const,
background: {
default: '#333333',
+ paper: '#424242',
},
status: {
ok: '#71CF88',
@@ -153,7 +143,7 @@ export const darkTheme = createTheme({
warningText: '#000000',
linkHover: '#82BAFD',
link: '#9CC9FF',
- gold: yellow.A700,
+ gold: '#FFD600',
navigation: {
background: '#424242',
indicator: '#9BF0E1',
@@ -174,6 +164,4 @@ export const darkTheme = createTheme({
indicator: '#9BF0E1',
},
},
- defaultPageTheme: 'home',
- pageTheme,
-});
+};
diff --git a/packages/theme/src/types.ts b/packages/theme/src/base/types.ts
similarity index 63%
rename from packages/theme/src/types.ts
rename to packages/theme/src/base/types.ts
index 0295e491f1..8f71ce4292 100644
--- a/packages/theme/src/types.ts
+++ b/packages/theme/src/base/types.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Backstage Authors
+ * Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,12 +14,6 @@
* limitations under the License.
*/
-import { Theme, ThemeOptions } from '@material-ui/core';
-import {
- PaletteOptions,
- Palette,
-} from '@material-ui/core/styles/createPalette';
-
/**
* Backstage specific additions to the material-ui palette.
*
@@ -90,21 +84,6 @@ export type BackstagePaletteAdditions = {
};
};
-/**
- * The full Backstage palette.
- *
- * @public
- */
-export type BackstagePalette = Palette & BackstagePaletteAdditions;
-
-/**
- * The full Backstage palette options.
- *
- * @public
- */
-export type BackstagePaletteOptions = PaletteOptions &
- BackstagePaletteAdditions;
-
/**
* Selector for what page theme to use.
*
@@ -114,48 +93,6 @@ export type PageThemeSelector = {
themeId: string;
};
-/**
- * A Backstage theme.
- *
- * @public
- */
-export interface BackstageTheme extends Theme {
- palette: BackstagePalette;
- page: PageTheme;
- getPageTheme: (selector: PageThemeSelector) => PageTheme;
-}
-
-/**
- * Backstage theme options.
- *
- * @public
- * @remarks
- *
- * This is essentially a partial theme definition made by the user, that then
- * gets merged together with defaults and other values to form the final
- * {@link BackstageTheme}.
- *
- */
-export interface BackstageThemeOptions extends ThemeOptions {
- palette: BackstagePaletteOptions;
- page: PageTheme;
- getPageTheme: (selector: PageThemeSelector) => PageTheme;
-}
-
-/**
- * A simpler configuration for creating a new theme that just tweaks some parts
- * of the backstage one.
- *
- * @public
- */
-export type SimpleThemeOptions = {
- palette: BackstagePaletteOptions;
- defaultPageTheme: string;
- pageTheme?: Record;
- fontFamily?: string;
- htmlFontSize?: number;
-};
-
/**
* The theme definitions for a given layout page.
*
@@ -167,3 +104,13 @@ export type PageTheme = {
backgroundImage: string;
fontColor: string;
};
+
+/**
+ * Backstage specific additions to the material-ui theme.
+ *
+ * @public
+ */
+export type BackstageThemeAdditions = {
+ page: PageTheme;
+ getPageTheme: (selector: PageThemeSelector) => PageTheme;
+};
diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts
index 41f827bf43..b0e3590273 100644
--- a/packages/theme/src/index.ts
+++ b/packages/theme/src/index.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Backstage Authors
+ * Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@
* @packageDocumentation
*/
-export * from './themes';
-export * from './baseTheme';
-export * from './types';
-export * from './pageTheme';
+export * from './unified';
+export * from './base';
+export * from './v4';
+export * from './v5';
diff --git a/packages/theme/src/unified/MuiClassNameSetup.ts b/packages/theme/src/unified/MuiClassNameSetup.ts
new file mode 100644
index 0000000000..42b948af32
--- /dev/null
+++ b/packages/theme/src/unified/MuiClassNameSetup.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { unstable_ClassNameGenerator as ClassNameGenerator } from '@mui/material/className';
+
+/**
+ * This API is introduced in @mui/material (v5.0.5) as a replacement of deprecated createGenerateClassName & only affects v5 MUI components from `@mui/*`
+ */
+ClassNameGenerator.configure(componentName => {
+ return `v5-${componentName}`;
+});
diff --git a/packages/theme/src/unified/UnifiedTheme.tsx b/packages/theme/src/unified/UnifiedTheme.tsx
new file mode 100644
index 0000000000..0c5f80357a
--- /dev/null
+++ b/packages/theme/src/unified/UnifiedTheme.tsx
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Theme as Mui4Theme,
+ ThemeOptions as ThemeOptionsV4,
+ createTheme,
+} from '@material-ui/core/styles';
+import type { PaletteOptions as PaletteOptionsV4 } from '@material-ui/core/styles/createPalette';
+import { PaletteOptions as PaletteOptionsV5 } from '@mui/material/styles';
+import {
+ adaptV4Theme,
+ Theme as Mui5Theme,
+ createTheme as createV5Theme,
+ ThemeOptions as ThemeOptionsV5,
+} from '@mui/material/styles';
+import { transformV5ComponentThemesToV4 } from './overrides';
+import { PageTheme } from '../base/types';
+import { defaultComponentThemes } from '../v5';
+import { createBaseThemeOptions } from '../base/createBaseThemeOptions';
+import { UnifiedTheme } from './types';
+
+export class UnifiedThemeHolder implements UnifiedTheme {
+ #themes = new Map();
+
+ constructor(v4?: Mui4Theme, v5?: Mui5Theme) {
+ this.#themes = new Map();
+ if (v4) {
+ this.#themes.set('v4', v4);
+ }
+ if (v5) {
+ this.#themes.set('v5', v5);
+ }
+ }
+
+ getTheme(version: string): unknown | undefined {
+ return this.#themes.get(version);
+ }
+}
+
+/**
+ * Options for creating a new {@link UnifiedTheme}.
+ *
+ * @public
+ */
+export interface UnifiedThemeOptions {
+ palette: PaletteOptionsV4 & PaletteOptionsV5;
+ defaultPageTheme?: string;
+ pageTheme?: Record;
+ fontFamily?: string;
+ htmlFontSize?: number;
+ components?: ThemeOptionsV5['components'];
+}
+
+/**
+ * Creates a new {@link UnifiedTheme} using the provided options.
+ *
+ * @public
+ */
+export function createUnifiedTheme(options: UnifiedThemeOptions): UnifiedTheme {
+ const themeOptions = createBaseThemeOptions(options);
+ const components = { ...defaultComponentThemes, ...options.components };
+ const v5Theme = createV5Theme({ ...themeOptions, components });
+
+ // TODO: Not super relevant in the beginning
+ /* const mui4Styles = maybeLoadMui4Styles();
+ if (!mui4Styles) {
+ return new UnifiedThemeHolder(undefined, v5Theme);
+ } */
+
+ const v4Overrides = transformV5ComponentThemesToV4(v5Theme, components);
+ const v4Theme = { ...createTheme(themeOptions), ...v4Overrides };
+ return new UnifiedThemeHolder(v4Theme, v5Theme);
+}
+
+/**
+ * Creates a new {@link UnifiedTheme} using MUI v4 theme options.
+ * Note that this uses `adaptV4Theme` from MUI v5, which is deprecated.
+ *
+ * @public
+ */
+export function createUnifiedThemeFromV4(
+ options: ThemeOptionsV4,
+): UnifiedTheme {
+ const v5Theme = adaptV4Theme(options as any);
+ const v4Theme = createTheme(options);
+ return new UnifiedThemeHolder(v4Theme, v5Theme);
+}
diff --git a/packages/theme/src/unified/UnifiedThemeProvider.tsx b/packages/theme/src/unified/UnifiedThemeProvider.tsx
new file mode 100644
index 0000000000..c7054e0e55
--- /dev/null
+++ b/packages/theme/src/unified/UnifiedThemeProvider.tsx
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import React, { ReactNode } from 'react';
+import './MuiClassNameSetup';
+import { ThemeProvider } from '@material-ui/core/styles';
+import {
+ StyledEngineProvider,
+ ThemeProvider as Mui5Provider,
+} from '@mui/material/styles';
+import CSSBaseline from '@mui/material/CssBaseline';
+import { UnifiedTheme } from './types';
+
+/**
+ * Props for {@link UnifiedThemeProvider}.
+ *
+ * @public
+ */
+export interface UnifiedThemeProviderProps {
+ children: ReactNode;
+ theme: UnifiedTheme;
+ noCssBaseline?: boolean;
+}
+
+/**
+ * Provides themes for all MUI versions supported by the provided unified theme.
+ *
+ * @public
+ */
+export function UnifiedThemeProvider(
+ props: UnifiedThemeProviderProps,
+): JSX.Element {
+ const { children, theme, noCssBaseline = false } = props;
+
+ const v4Theme = theme.getTheme('v4');
+ const v5Theme = theme.getTheme('v5');
+
+ let cssBaseline: JSX.Element | undefined = undefined;
+ if (!noCssBaseline) {
+ cssBaseline = ;
+ }
+
+ let result = (
+ <>
+ {cssBaseline}
+ {children}
+ >
+ );
+
+ if (v4Theme) {
+ result = {result};
+ }
+
+ if (v5Theme) {
+ result = (
+
+ {result}
+
+ );
+ }
+
+ return result;
+}
diff --git a/packages/theme/src/unified/index.ts b/packages/theme/src/unified/index.ts
new file mode 100644
index 0000000000..f5c805d3ed
--- /dev/null
+++ b/packages/theme/src/unified/index.ts
@@ -0,0 +1,23 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { transformV5ComponentThemesToV4 } from './overrides';
+export { createUnifiedTheme, createUnifiedThemeFromV4 } from './UnifiedTheme';
+export type { UnifiedThemeOptions } from './UnifiedTheme';
+export { themes } from './themes';
+export { UnifiedThemeProvider } from './UnifiedThemeProvider';
+export type { UnifiedThemeProviderProps } from './UnifiedThemeProvider';
+export type { UnifiedTheme } from './types';
diff --git a/packages/theme/src/unified/overrides.test.ts b/packages/theme/src/unified/overrides.test.ts
new file mode 100644
index 0000000000..e84cedade6
--- /dev/null
+++ b/packages/theme/src/unified/overrides.test.ts
@@ -0,0 +1,165 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Theme } from '@mui/material/styles';
+import { transformV5ComponentThemesToV4 } from './overrides';
+
+describe('transformV5ComponentThemesToV4', () => {
+ const mockTheme = {
+ palette: {
+ primary: {
+ main: 'red',
+ },
+ },
+ } as unknown as Theme;
+ it('transforms empty component themes', () => {
+ expect(transformV5ComponentThemesToV4(mockTheme)).toEqual({
+ overrides: {},
+ props: {},
+ });
+ expect(transformV5ComponentThemesToV4(mockTheme, {})).toEqual({
+ overrides: {},
+ props: {},
+ });
+ expect(
+ transformV5ComponentThemesToV4(mockTheme, {
+ MuiButton: {
+ styleOverrides: undefined,
+ defaultProps: undefined,
+ },
+ }),
+ ).toEqual({ overrides: {}, props: {} });
+ expect(
+ transformV5ComponentThemesToV4(mockTheme, {
+ MuiButton: {
+ styleOverrides: {},
+ defaultProps: {},
+ },
+ }),
+ ).toEqual({ overrides: { MuiButton: {} }, props: { MuiButton: {} } });
+ });
+
+ it('transforms component themes', () => {
+ expect(
+ transformV5ComponentThemesToV4(mockTheme, {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ color: 'green',
+ },
+ },
+ defaultProps: {
+ disableRipple: true,
+ },
+ },
+ }),
+ ).toEqual({
+ overrides: {
+ MuiButton: {
+ root: {
+ color: 'green',
+ },
+ },
+ },
+ props: {
+ MuiButton: {
+ disableRipple: true,
+ },
+ },
+ });
+ expect(
+ transformV5ComponentThemesToV4(mockTheme, {
+ MuiButton: {
+ styleOverrides: {
+ root: ({ theme }) => ({
+ color: theme.palette.primary.main,
+ }),
+ },
+ },
+ }),
+ ).toEqual({
+ overrides: {
+ MuiButton: {
+ root: {
+ color: 'red',
+ },
+ },
+ },
+ props: {},
+ });
+ });
+
+ it('transforms CSSBaseline theme', () => {
+ expect(
+ transformV5ComponentThemesToV4(mockTheme, {
+ MuiCssBaseline: {
+ styleOverrides: theme => ({
+ html: {
+ color: theme.palette.primary.main,
+ },
+ }),
+ defaultProps: {
+ enableColorScheme: true,
+ },
+ },
+ }),
+ ).toEqual({
+ overrides: {
+ MuiCssBaseline: {
+ '@global': {
+ html: {
+ color: 'red',
+ },
+ },
+ },
+ },
+ props: {
+ MuiCssBaseline: {
+ enableColorScheme: true,
+ },
+ },
+ });
+ });
+
+ it('transform state styles', () => {
+ expect(
+ transformV5ComponentThemesToV4(mockTheme, {
+ MuiButton: {
+ styleOverrides: {
+ root: {
+ color: 'green',
+ '&.Mui-disabled': {
+ color: 'red',
+ },
+ },
+ },
+ },
+ }),
+ ).toEqual({
+ overrides: {
+ MuiButton: {
+ root: {
+ color: 'green',
+ },
+ disabled: {
+ color: 'red',
+ },
+ },
+ },
+ props: {},
+ });
+ });
+});
diff --git a/packages/theme/src/unified/overrides.ts b/packages/theme/src/unified/overrides.ts
new file mode 100644
index 0000000000..7417fccedc
--- /dev/null
+++ b/packages/theme/src/unified/overrides.ts
@@ -0,0 +1,142 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type { Overrides } from '@material-ui/core/styles/overrides';
+import type { ComponentsProps } from '@material-ui/core/styles/props';
+import { ComponentsOverrides, Theme, ThemeOptions } from '@mui/material/styles';
+import { CSSProperties } from 'react';
+
+type V5Override = ComponentsOverrides[Exclude<
+ keyof ComponentsOverrides,
+ 'MuiCssBaseline'
+>];
+type V4Override = Overrides[keyof Overrides];
+type StaticStyleRules = Record<
+ string,
+ CSSProperties | Record
+>;
+
+// Converts callback-based overrides to static styles, e.g.
+// { root: theme => ({ color: theme.color }) } -> { root: { color: 'red' } }
+function adaptV5CssBaselineOverride(
+ theme: Theme,
+ overrides: ComponentsOverrides['MuiCssBaseline'],
+): StaticStyleRules | undefined {
+ if (!overrides || typeof overrides === 'string') {
+ return undefined;
+ }
+
+ const styles = typeof overrides === 'function' ? overrides(theme) : overrides;
+ if (styles) {
+ return { '@global': styles } as StaticStyleRules;
+ }
+
+ return undefined;
+}
+
+// Converts callback-based overrides to static styles, e.g.
+// { root: theme => ({ color: theme.color }) } -> { root: { color: 'red' } }
+function adaptV5Override(
+ theme: Theme,
+ overrides: V5Override,
+): StaticStyleRules | undefined {
+ if (!overrides || typeof overrides === 'string') {
+ return undefined;
+ }
+ if (typeof overrides === 'object') {
+ return Object.fromEntries(
+ Object.entries(overrides).map(([className, style]) => {
+ if (typeof style === 'function') {
+ return [className, style({ theme })];
+ }
+ return [className, style];
+ }),
+ );
+ }
+ return overrides as StaticStyleRules;
+}
+
+const stateStyleKeyPattern = /^&.Mui-([\w-]+)$/;
+
+// Move state style overrides to the top level, e.g.
+// { root: { '&.Mui-active': { color: 'red' } } } -> { active: { color: 'red' } }
+function extractV5StateOverrides(
+ overrides: StaticStyleRules | undefined,
+): StaticStyleRules | undefined {
+ let output = overrides;
+ if (!overrides || typeof overrides !== 'object') {
+ return output;
+ }
+ for (const className of Object.keys(overrides)) {
+ const styles = overrides[className];
+ if (!styles || typeof styles !== 'object') {
+ continue;
+ }
+ for (const _styleKey of Object.keys(styles)) {
+ const styleKey = _styleKey as keyof typeof styles;
+ const match = styleKey.match(stateStyleKeyPattern);
+ if (match) {
+ const [, state] = match;
+ const { [styleKey]: stateStyles, ...restStyles } = styles;
+ if (stateStyles) {
+ output = {
+ ...output,
+ [className]: restStyles,
+ [state]: stateStyles,
+ };
+ }
+ }
+ }
+ }
+ return output;
+}
+
+/**
+ * Transform MUI v5 component themes into a v4 theme props and overrides.
+ *
+ * @public
+ */
+export function transformV5ComponentThemesToV4(
+ theme: Theme,
+ components: ThemeOptions['components'] = {},
+): { overrides: Overrides; props: ComponentsProps } {
+ const overrides: Record = {};
+ const props: Record = {};
+
+ for (const name of Object.keys(components)) {
+ const component = components[name as keyof typeof components];
+ if (!component) {
+ continue;
+ }
+ if ('styleOverrides' in component) {
+ if (name === 'MuiCssBaseline') {
+ overrides[name] = adaptV5CssBaselineOverride(
+ theme,
+ component.styleOverrides as ComponentsOverrides['MuiCssBaseline'],
+ );
+ } else {
+ overrides[name] = extractV5StateOverrides(
+ adaptV5Override(theme, component.styleOverrides as V5Override),
+ );
+ }
+ }
+ if ('defaultProps' in component) {
+ props[name] = component.defaultProps;
+ }
+ }
+
+ return { overrides, props };
+}
diff --git a/packages/theme/src/unified/themes.ts b/packages/theme/src/unified/themes.ts
new file mode 100644
index 0000000000..24e1b1026b
--- /dev/null
+++ b/packages/theme/src/unified/themes.ts
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { palettes } from '../base';
+import { createUnifiedTheme } from './UnifiedTheme';
+
+/**
+ * Built-in Backstage MUI themes.
+ *
+ * @public
+ */
+export const themes = {
+ light: createUnifiedTheme({ palette: palettes.light }),
+ dark: createUnifiedTheme({ palette: palettes.dark }),
+};
diff --git a/packages/theme/src/unified/types.ts b/packages/theme/src/unified/types.ts
new file mode 100644
index 0000000000..f090a99b83
--- /dev/null
+++ b/packages/theme/src/unified/types.ts
@@ -0,0 +1,26 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * A container of one theme for multiple different MUI versions.
+ *
+ * Currently known keys are 'v4' and 'v5'.
+ *
+ * @public
+ */
+export interface UnifiedTheme {
+ getTheme(version: string): unknown | undefined;
+}
diff --git a/packages/theme/src/v4/baseTheme.ts b/packages/theme/src/v4/baseTheme.ts
new file mode 100644
index 0000000000..8143183ec0
--- /dev/null
+++ b/packages/theme/src/v4/baseTheme.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Theme as Mui5Theme } from '@mui/material/styles';
+import { createTheme as createMuiTheme } from '@material-ui/core/styles';
+import type {
+ GridProps,
+ SwitchProps,
+ Theme,
+ ThemeOptions,
+} from '@material-ui/core';
+import { Overrides } from '@material-ui/core/styles/overrides';
+import { SimpleThemeOptions } from './types';
+import { createBaseThemeOptions } from '../base';
+import { defaultComponentThemes } from '../v5';
+import { transformV5ComponentThemesToV4 } from '../unified/overrides';
+
+/**
+ * An old helper for creating MUI v4 theme options.
+ *
+ * @public
+ * @deprecated Use {@link createBaseThemeOptions} instead.
+ */
+export function createThemeOptions(options: SimpleThemeOptions): ThemeOptions {
+ return {
+ props: {
+ MuiGrid: defaultComponentThemes?.MuiGrid
+ ?.defaultProps as Partial,
+ MuiSwitch: defaultComponentThemes?.MuiSwitch
+ ?.defaultProps as Partial,
+ },
+ ...createBaseThemeOptions(options),
+ };
+}
+
+/**
+ * * An old helper for creating MUI v4 theme overrides.
+ *
+ * @public
+ * @deprecated Use {@link defaultComponentThemes} with {@link transformV5ComponentThemesToV4} instead.
+ */
+export function createThemeOverrides(theme: Theme): Overrides {
+ return transformV5ComponentThemesToV4(
+ // Safe but we have to make sure we don't use mui5 specific stuff in the default component themes
+ theme as unknown as Mui5Theme,
+ defaultComponentThemes,
+ ).overrides;
+}
+
+/**
+ * The old method to create a Backstage MUI v4 theme using a palette.
+ * The theme is created with the common Backstage options and component styles.
+ *
+ * @public
+ * @deprecated Use {@link createUnifiedTheme} instead.
+ */
+export function createTheme(options: SimpleThemeOptions): Theme {
+ const themeOptions = createThemeOptions(options);
+ const baseTheme = createMuiTheme(themeOptions);
+ const overrides = createThemeOverrides(baseTheme);
+ const theme = { ...baseTheme, overrides };
+ return theme;
+}
diff --git a/packages/theme/src/v4/index.ts b/packages/theme/src/v4/index.ts
new file mode 100644
index 0000000000..fad349b849
--- /dev/null
+++ b/packages/theme/src/v4/index.ts
@@ -0,0 +1,29 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { darkTheme, lightTheme } from './themes';
+export {
+ createTheme,
+ createThemeOptions,
+ createThemeOverrides,
+} from './baseTheme';
+export type {
+ BackstagePalette,
+ BackstagePaletteOptions,
+ BackstageTheme,
+ BackstageThemeOptions,
+ SimpleThemeOptions,
+} from './types';
diff --git a/packages/theme/src/v4/themes.ts b/packages/theme/src/v4/themes.ts
new file mode 100644
index 0000000000..488ee58831
--- /dev/null
+++ b/packages/theme/src/v4/themes.ts
@@ -0,0 +1,38 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createTheme } from './baseTheme';
+import { palettes } from '../base';
+
+/**
+ * The old MUI v4 Backstage light theme.
+ *
+ * @public
+ * @deprecated Use {@link themes.light} instead.
+ */
+export const lightTheme = createTheme({
+ palette: palettes.light,
+});
+
+/**
+ * The old MUI v4 Backstage dark theme.
+ *
+ * @public
+ * @deprecated Use {@link themes.dark} instead.
+ */
+export const darkTheme = createTheme({
+ palette: palettes.dark,
+});
diff --git a/packages/theme/src/v4/types.ts b/packages/theme/src/v4/types.ts
new file mode 100644
index 0000000000..8bca5b8c3b
--- /dev/null
+++ b/packages/theme/src/v4/types.ts
@@ -0,0 +1,104 @@
+/*
+ * Copyright 2020 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import type {
+ Theme as MuiTheme,
+ ThemeOptions as MuiThemeOptions,
+} from '@material-ui/core';
+import type {
+ PaletteOptions as MuiPaletteOptions,
+ Palette as MuiPalette,
+} from '@material-ui/core/styles/createPalette';
+import {
+ BackstagePaletteAdditions,
+ BackstageThemeAdditions,
+ PageTheme,
+ PageThemeSelector,
+} from '../base/types';
+
+/**
+ * The full Backstage palette.
+ *
+ * @public
+ * @deprecated This type is deprecated, the MUI Palette type is now always extended instead.
+ */
+export type BackstagePalette = MuiPalette & BackstagePaletteAdditions;
+
+/**
+ * The full Backstage palette options.
+ *
+ * @public
+ * @deprecated This type is deprecated, the MUI PaletteOptions type is now always extended instead.
+ */
+export type BackstagePaletteOptions = MuiPaletteOptions &
+ BackstagePaletteAdditions;
+
+/**
+ * Backstage theme options.
+ *
+ * @public
+ * @deprecated This type is deprecated, the MUI ThemeOptions type is now always extended instead.
+ * @remarks
+ *
+ * This is essentially a partial theme definition made by the user, that then
+ * gets merged together with defaults and other values to form the final
+ * {@link BackstageTheme}.
+ *
+ */
+export interface BackstageThemeOptions extends MuiThemeOptions {
+ palette: BackstagePaletteOptions;
+ page: PageTheme;
+ getPageTheme: (selector: PageThemeSelector) => PageTheme;
+}
+
+/**
+ * A Backstage theme.
+ *
+ * @public
+ * @deprecated This type is deprecated, the MUI Theme type is now always extended instead.
+ */
+export interface BackstageTheme extends MuiTheme {
+ palette: BackstagePalette;
+ page: PageTheme;
+ getPageTheme: (selector: PageThemeSelector) => PageTheme;
+}
+
+/**
+ * A simpler configuration for creating a new theme that just tweaks some parts
+ * of the backstage one.
+ *
+ * @public
+ * @deprecated Use {@link BaseThemeOptionsInput} instead.
+ */
+export type SimpleThemeOptions = {
+ palette: MuiPaletteOptions;
+ defaultPageTheme?: string;
+ pageTheme?: Record;
+ fontFamily?: string;
+ htmlFontSize?: number;
+};
+
+declare module '@material-ui/core/styles/createPalette' {
+ interface Palette extends BackstagePaletteAdditions {}
+
+ interface PaletteOptions extends BackstagePaletteAdditions {}
+}
+
+declare module '@material-ui/core/styles/createTheme' {
+ interface Theme extends BackstageThemeAdditions {}
+
+ interface ThemeOptions extends BackstageThemeAdditions {}
+}
diff --git a/packages/theme/src/baseTheme.ts b/packages/theme/src/v5/defaultComponentThemes.ts
similarity index 51%
rename from packages/theme/src/baseTheme.ts
rename to packages/theme/src/v5/defaultComponentThemes.ts
index 53f3a3ef2b..14656b007d 100644
--- a/packages/theme/src/baseTheme.ts
+++ b/packages/theme/src/v5/defaultComponentThemes.ts
@@ -1,5 +1,5 @@
/*
- * Copyright 2020 The Backstage Authors
+ * Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,121 +14,51 @@
* limitations under the License.
*/
-import { createTheme as createMuiTheme } from '@material-ui/core/styles';
-import { darken, lighten } from '@material-ui/core/styles/colorManipulator';
-import { Overrides } from '@material-ui/core/styles/overrides';
-import {
- BackstageTheme,
- BackstageThemeOptions,
- SimpleThemeOptions,
-} from './types';
-import { pageTheme as defaultPageThemes } from './pageTheme';
-
-const DEFAULT_HTML_FONT_SIZE = 16;
-const DEFAULT_FONT_FAMILY =
- '"Helvetica Neue", Helvetica, Roboto, Arial, sans-serif';
-
-/**
- * A helper for creating theme options.
- *
- * @public
- */
-export function createThemeOptions(
- options: SimpleThemeOptions,
-): BackstageThemeOptions {
- const {
- palette,
- htmlFontSize = DEFAULT_HTML_FONT_SIZE,
- fontFamily = DEFAULT_FONT_FAMILY,
- defaultPageTheme,
- pageTheme = defaultPageThemes,
- } = options;
-
- if (!pageTheme[defaultPageTheme]) {
- throw new Error(`${defaultPageTheme} is not defined in pageTheme.`);
- }
-
- return {
- palette,
- props: {
- MuiGrid: {
- spacing: 2,
- },
- MuiSwitch: {
- color: 'primary',
- },
- },
- typography: {
- htmlFontSize,
- fontFamily,
- h6: {
- fontWeight: 700,
- fontSize: 20,
- marginBottom: 2,
- },
- h5: {
- fontWeight: 700,
- fontSize: 24,
- marginBottom: 4,
- },
- h4: {
- fontWeight: 700,
- fontSize: 28,
- marginBottom: 6,
- },
- h3: {
- fontSize: 32,
- fontWeight: 700,
- marginBottom: 6,
- },
- h2: {
- fontSize: 40,
- fontWeight: 700,
- marginBottom: 8,
- },
- h1: {
- fontSize: 54,
- fontWeight: 700,
- marginBottom: 10,
- },
- },
- page: pageTheme[defaultPageTheme],
- getPageTheme: ({ themeId }) =>
- pageTheme[themeId] ?? pageTheme[defaultPageTheme],
- };
-}
+import { darken, lighten, ThemeOptions } from '@mui/material/styles';
/**
* A helper for creating theme overrides.
*
* @public
*/
-export function createThemeOverrides(theme: BackstageTheme): Overrides {
- return {
- MuiCssBaseline: {
- '@global': {
- html: {
- height: '100%',
- fontFamily: theme.typography.fontFamily,
- },
- body: {
- height: '100%',
- fontFamily: theme.typography.fontFamily,
- 'overscroll-behavior-y': 'none',
- },
- a: {
- color: 'inherit',
- textDecoration: 'none',
- },
+export const defaultComponentThemes: ThemeOptions['components'] = {
+ MuiCssBaseline: {
+ styleOverrides: theme => ({
+ html: {
+ height: '100%',
+ fontFamily: theme.typography.fontFamily,
},
+ body: {
+ height: '100%',
+ fontFamily: theme.typography.fontFamily,
+ overscrollBehaviorY: 'none',
+ fontSize: '0.875rem',
+ lineHeight: 1.43,
+ },
+ a: {
+ color: 'inherit',
+ textDecoration: 'none',
+ },
+ }),
+ },
+ MuiGrid: {
+ defaultProps: {
+ spacing: 2,
},
- MuiTableRow: {
+ },
+ MuiSwitch: {
+ defaultProps: {
+ color: 'primary',
+ },
+ },
+ MuiTableRow: {
+ styleOverrides: {
// Alternating row backgrounds
- root: {
+ root: ({ theme }) => ({
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.background.default,
},
- },
+ }),
// Use pointer for hoverable rows
hover: {
'&:hover': {
@@ -136,15 +66,17 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
},
},
// Alternating head backgrounds
- head: {
+ head: ({ theme }) => ({
'&:nth-of-type(odd)': {
backgroundColor: theme.palette.background.paper,
},
- },
+ }),
},
- // Tables are more dense than default mui tables
- MuiTableCell: {
- root: {
+ },
+ // Tables are more dense than default mui tables
+ MuiTableCell: {
+ styleOverrides: {
+ root: ({ theme }) => ({
wordBreak: 'break-word',
overflow: 'hidden',
verticalAlign: 'middle',
@@ -152,27 +84,31 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
margin: 0,
padding: theme.spacing(3, 2, 3, 2.5),
borderBottom: 0,
- },
- sizeSmall: {
+ }),
+ sizeSmall: ({ theme }) => ({
padding: theme.spacing(1.5, 2, 1.5, 2.5),
- },
- head: {
+ }),
+ head: ({ theme }) => ({
wordBreak: 'break-word',
overflow: 'hidden',
color: theme.palette.textSubtle,
fontWeight: 'normal',
lineHeight: '1',
- },
+ }),
},
- MuiTabs: {
+ },
+ MuiTabs: {
+ styleOverrides: {
// Tabs are smaller than default mui tab rows
root: {
minHeight: 24,
},
},
- MuiTab: {
+ },
+ MuiTab: {
+ styleOverrides: {
// Tabs are smaller and have a hover background
- root: {
+ root: ({ theme }) => ({
color: theme.palette.link,
minHeight: 24,
textTransform: 'initial',
@@ -186,12 +122,14 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
fontSize: theme.typography.pxToRem(14),
fontWeight: 500,
},
- },
- textColorPrimary: {
+ }),
+ textColorPrimary: ({ theme }) => ({
color: theme.palette.link,
- },
+ }),
},
- MuiTableSortLabel: {
+ },
+ MuiTableSortLabel: {
+ styleOverrides: {
// No color change on hover, just rely on the arrow showing up instead.
root: {
color: 'inherit',
@@ -201,14 +139,16 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
'&:focus': {
color: 'inherit',
},
- },
- // Bold font for highlighting selected column
- active: {
- fontWeight: 'bold',
- color: 'inherit',
+ // Bold font for highlighting selected column
+ '&.Mui-active': {
+ fontWeight: 'bold',
+ color: 'inherit',
+ },
},
},
- MuiListItemText: {
+ },
+ MuiListItemText: {
+ styleOverrides: {
dense: {
// Default dense list items to adding ellipsis for really long str...
whiteSpace: 'nowrap',
@@ -216,44 +156,50 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
textOverflow: 'ellipsis',
},
},
- MuiButton: {
+ },
+ MuiButton: {
+ styleOverrides: {
text: {
// Text buttons have less padding by default, but we want to keep the original padding
padding: undefined,
},
},
- MuiChip: {
- root: {
+ },
+ MuiChip: {
+ styleOverrides: {
+ root: ({ theme }) => ({
backgroundColor: '#D9D9D9',
// By default there's no margin, but it's usually wanted, so we add some trailing margin
marginRight: theme.spacing(1),
marginBottom: theme.spacing(1),
color: theme.palette.grey[900],
- },
- outlined: {
+ }),
+ outlined: ({ theme }) => ({
color: theme.palette.text.primary,
- },
- label: {
- lineHeight: `${theme.spacing(2.5)}px`,
+ }),
+ label: ({ theme }) => ({
+ lineHeight: theme.spacing(2.5),
fontWeight: theme.typography.fontWeightMedium,
- fontSize: `${theme.spacing(1.75)}px`,
- },
- labelSmall: {
- fontSize: `${theme.spacing(1.5)}px`,
- },
- deleteIcon: {
+ fontSize: theme.spacing(1.75),
+ }),
+ labelSmall: ({ theme }) => ({
+ fontSize: theme.spacing(1.5),
+ }),
+ deleteIcon: ({ theme }) => ({
color: theme.palette.grey[500],
- width: `${theme.spacing(3)}px`,
- height: `${theme.spacing(3)}px`,
- margin: `0 ${theme.spacing(0.75)}px 0 -${theme.spacing(0.75)}px`,
- },
- deleteIconSmall: {
- width: `${theme.spacing(2)}px`,
- height: `${theme.spacing(2)}px`,
- margin: `0 ${theme.spacing(0.5)}px 0 -${theme.spacing(0.5)}px`,
- },
+ width: theme.spacing(3),
+ height: theme.spacing(3),
+ margin: `0 ${theme.spacing(0.75)} 0 -${theme.spacing(0.75)}`,
+ }),
+ deleteIconSmall: ({ theme }) => ({
+ width: theme.spacing(2),
+ height: theme.spacing(2),
+ margin: `0 ${theme.spacing(0.5)} 0 -${theme.spacing(0.5)}`,
+ }),
},
- MuiCard: {
+ },
+ MuiCard: {
+ styleOverrides: {
root: {
// When cards have a forced size, such as when they are arranged in a
// CSS grid, the content needs to flex such that the actions (buttons
@@ -263,13 +209,17 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
flexDirection: 'column',
},
},
- MuiCardHeader: {
+ },
+ MuiCardHeader: {
+ styleOverrides: {
root: {
// Reduce padding between header and content
paddingBottom: 0,
},
},
- MuiCardContent: {
+ },
+ MuiCardContent: {
+ styleOverrides: {
root: {
// When cards have a forced size, such as when they are arranged in a
// CSS grid, the content needs to flex such that the actions (buttons
@@ -281,25 +231,13 @@ export function createThemeOverrides(theme: BackstageTheme): Overrides {
},
},
},
- MuiCardActions: {
+ },
+ MuiCardActions: {
+ styleOverrides: {
root: {
// We default to putting the card actions at the end
justifyContent: 'flex-end',
},
},
- };
-}
-
-/**
- * Creates a Backstage MUI theme using a palette. The theme is created with the
- * common Backstage options and component styles.
- *
- * @public
- */
-export function createTheme(options: SimpleThemeOptions): BackstageTheme {
- const themeOptions = createThemeOptions(options);
- const baseTheme = createMuiTheme(themeOptions) as BackstageTheme;
- const overrides = createThemeOverrides(baseTheme);
- const theme = { ...baseTheme, overrides };
- return theme;
-}
+ },
+};
diff --git a/packages/theme/src/v5/index.ts b/packages/theme/src/v5/index.ts
new file mode 100644
index 0000000000..eee5d852b1
--- /dev/null
+++ b/packages/theme/src/v5/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './types';
+export { defaultComponentThemes } from './defaultComponentThemes';
diff --git a/packages/theme/src/v5/types.ts b/packages/theme/src/v5/types.ts
new file mode 100644
index 0000000000..31fe72b5b9
--- /dev/null
+++ b/packages/theme/src/v5/types.ts
@@ -0,0 +1,37 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Theme } from '@mui/material/styles';
+import {
+ BackstagePaletteAdditions,
+ BackstageThemeAdditions,
+} from '../base/types';
+
+declare module '@mui/material/styles' {
+ interface Palette extends BackstagePaletteAdditions {}
+
+ interface PaletteOptions extends BackstagePaletteAdditions {}
+}
+
+declare module '@mui/material/styles' {
+ interface Theme extends BackstageThemeAdditions {}
+
+ interface ThemeOptions extends BackstageThemeAdditions {}
+}
+
+declare module '@mui/private-theming/defaultTheme' {
+ interface DefaultTheme extends Theme {}
+}
diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md
index 39e8bf7e4c..e4e23e4b66 100644
--- a/plugins/adr-backend/CHANGELOG.md
+++ b/plugins/adr-backend/CHANGELOG.md
@@ -1,5 +1,33 @@
# @backstage/plugin-adr-backend
+## 0.3.4-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-adr-common@0.2.9
+ - @backstage/plugin-search-common@1.2.3
+
+## 0.3.3
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-adr-common@0.2.9
+ - @backstage/plugin-search-common@1.2.3
+
## 0.3.3-next.1
### Patch Changes
diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json
index a1f6a859a0..b693d3e8c2 100644
--- a/plugins/adr-backend/package.json
+++ b/plugins/adr-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr-backend",
- "version": "0.3.3-next.1",
+ "version": "0.3.4-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr-common/CHANGELOG.md b/plugins/adr-common/CHANGELOG.md
index c3b8806c80..9fddae2588 100644
--- a/plugins/adr-common/CHANGELOG.md
+++ b/plugins/adr-common/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-adr-common
+## 0.2.9
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/integration@1.4.5
+ - @backstage/catalog-model@1.3.0
+ - @backstage/plugin-search-common@1.2.3
+
## 0.2.9-next.0
### Patch Changes
diff --git a/plugins/adr-common/package.json b/plugins/adr-common/package.json
index 0fdf94003a..adbb6bea58 100644
--- a/plugins/adr-common/package.json
+++ b/plugins/adr-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-adr-common",
"description": "Common functionalities for the adr plugin",
- "version": "0.2.9-next.0",
+ "version": "0.2.9",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md
index b0ce02fa19..5a16413811 100644
--- a/plugins/adr/CHANGELOG.md
+++ b/plugins/adr/CHANGELOG.md
@@ -1,5 +1,51 @@
# @backstage/plugin-adr
+## 0.6.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/integration-react@1.1.14-next.0
+ - @backstage/plugin-search-react@1.6.1-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/plugin-adr-common@0.2.9
+ - @backstage/plugin-search-common@1.2.3
+
+## 0.6.0
+
+### Minor Changes
+
+- b12cd5dc221: render SupportButton only if config is set
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/plugin-search-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/integration-react@1.1.13
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-adr-common@0.2.9
+ - @backstage/plugin-search-common@1.2.3
+
+## 0.5.1-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.5.1-next.1
### Patch Changes
diff --git a/plugins/adr/package.json b/plugins/adr/package.json
index 3202a8c14e..41653dc1dc 100644
--- a/plugins/adr/package.json
+++ b/plugins/adr/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr",
- "version": "0.5.1-next.1",
+ "version": "0.6.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx
index d136de4717..da575d884f 100644
--- a/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx
+++ b/plugins/adr/src/components/EntityAdrContent/EntityAdrContent.tsx
@@ -29,7 +29,7 @@ import {
SupportButton,
WarningPanel,
} from '@backstage/core-components';
-import { useApi, useRouteRef } from '@backstage/core-plugin-api';
+import { configApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import {
AdrFilePathFilterFn,
@@ -170,6 +170,9 @@ export const EntityAdrContent = (props: {
const adrApi = useApi(adrApiRef);
const entityHasAdrs = isAdrAvailable(entity);
+ const config = useApi(configApiRef);
+ const appSupportConfigured = config?.getOptionalConfig('app.support');
+
const { value, loading, error } = useAsync(async () => {
const url = getAdrLocationUrl(entity, scmIntegrations);
return adrApi.listAdrs(url);
@@ -212,7 +215,7 @@ export const EntityAdrContent = (props: {
return (
-
+ {appSupportConfigured && }
{!entityHasAdrs && (
diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md
index 8407f5822e..99bc787d74 100644
--- a/plugins/airbrake-backend/CHANGELOG.md
+++ b/plugins/airbrake-backend/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/plugin-airbrake-backend
+## 0.2.19-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+
+## 0.2.18
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/config@1.0.7
+
## 0.2.18-next.1
### Patch Changes
diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json
index c57ffa8be9..6754446993 100644
--- a/plugins/airbrake-backend/package.json
+++ b/plugins/airbrake-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake-backend",
- "version": "0.2.18-next.1",
+ "version": "0.2.19-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/airbrake/CHANGELOG.md b/plugins/airbrake/CHANGELOG.md
index 191207e591..dbc923f75c 100644
--- a/plugins/airbrake/CHANGELOG.md
+++ b/plugins/airbrake/CHANGELOG.md
@@ -1,5 +1,43 @@
# @backstage/plugin-airbrake
+## 0.3.19-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/test-utils@1.4.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/dev-utils@1.0.16-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.3.18
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/dev-utils@1.0.15
+ - @backstage/test-utils@1.3.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/dev-utils@1.0.15-next.2
+ - @backstage/test-utils@1.3.1-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.18-next.1
### Patch Changes
diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json
index 6f7f404742..f7034ced40 100644
--- a/plugins/airbrake/package.json
+++ b/plugins/airbrake/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake",
- "version": "0.3.18-next.1",
+ "version": "0.3.19-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md
index a20bd80961..f7462e5bc3 100644
--- a/plugins/allure/CHANGELOG.md
+++ b/plugins/allure/CHANGELOG.md
@@ -1,5 +1,37 @@
# @backstage/plugin-allure
+## 0.1.35-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.1.34
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.34-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.34-next.1
### Patch Changes
diff --git a/plugins/allure/package.json b/plugins/allure/package.json
index 2a98ae1faf..af095a2901 100644
--- a/plugins/allure/package.json
+++ b/plugins/allure/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-allure",
"description": "A Backstage plugin that integrates with Allure",
- "version": "0.1.34-next.1",
+ "version": "0.1.35-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md
index 4195e02248..7ead93c55e 100644
--- a/plugins/analytics-module-ga/CHANGELOG.md
+++ b/plugins/analytics-module-ga/CHANGELOG.md
@@ -1,5 +1,35 @@
# @backstage/plugin-analytics-module-ga
+## 0.1.30-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.29
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/core-components@0.13.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.29-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.29-next.0
### Patch Changes
diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json
index 31215bd84f..5851edf627 100644
--- a/plugins/analytics-module-ga/package.json
+++ b/plugins/analytics-module-ga/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga",
- "version": "0.1.29-next.0",
+ "version": "0.1.30-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/analytics-module-ga4/CHANGELOG.md b/plugins/analytics-module-ga4/CHANGELOG.md
index 59c03fec0f..dd9d87b2c4 100644
--- a/plugins/analytics-module-ga4/CHANGELOG.md
+++ b/plugins/analytics-module-ga4/CHANGELOG.md
@@ -1,5 +1,40 @@
# @backstage/plugin-analytics-module-ga4
+## 0.1.1-next.0
+
+### Patch Changes
+
+- 3d11596a72b5: Update plugin installation docs to be more consistent across documentations
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.0
+
+### Minor Changes
+
+- 22b46f7f562: Plugin provides Backstage Analytics API for Google Analytics 4. Once installed and configured, analytics events will be sent to GA4 as your users navigate and use your Backstage instance
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/core-components@0.13.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.0-next.1
### Patch Changes
diff --git a/plugins/analytics-module-ga4/README.md b/plugins/analytics-module-ga4/README.md
index d060c9d9b1..97bb751ca8 100644
--- a/plugins/analytics-module-ga4/README.md
+++ b/plugins/analytics-module-ga4/README.md
@@ -9,7 +9,12 @@ This plugin contains no other functionality.
## Installation
1. Install the plugin package in your Backstage app:
- `cd packages/app && yarn add @backstage/plugin-analytics-module-ga4`
+
+```sh
+# From your Backstage root directory
+yarn add --cwd packages/app @backstage/plugin-analytics-module-ga4
+```
+
2. Wire up the API implementation to your App:
```tsx
diff --git a/plugins/analytics-module-ga4/package.json b/plugins/analytics-module-ga4/package.json
index 5e27228534..80dd77e11b 100644
--- a/plugins/analytics-module-ga4/package.json
+++ b/plugins/analytics-module-ga4/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-analytics-module-ga4",
- "version": "0.1.0-next.1",
+ "version": "0.1.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md
index 368995a369..410286ecf1 100644
--- a/plugins/apache-airflow/CHANGELOG.md
+++ b/plugins/apache-airflow/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/plugin-apache-airflow
+## 0.2.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.2.11
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.2.11-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.11-next.0
### Patch Changes
diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json
index 6d503e972a..569f256318 100644
--- a/plugins/apache-airflow/package.json
+++ b/plugins/apache-airflow/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apache-airflow",
- "version": "0.2.11-next.0",
+ "version": "0.2.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md
index 635b1e2dca..afad944485 100644
--- a/plugins/api-docs/CHANGELOG.md
+++ b/plugins/api-docs/CHANGELOG.md
@@ -1,5 +1,40 @@
# @backstage/plugin-api-docs
+## 0.9.4-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/plugin-catalog@1.11.1-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.9.3
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/plugin-catalog@1.11.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.9.3-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.9.3-next.1
### Patch Changes
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index 89e65dfcc3..be31cea662 100644
--- a/plugins/api-docs/package.json
+++ b/plugins/api-docs/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-api-docs",
"description": "A Backstage plugin that helps represent API entities in the frontend",
- "version": "0.9.3-next.1",
+ "version": "0.9.4-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md
index 174a862d61..15e38c890f 100644
--- a/plugins/apollo-explorer/CHANGELOG.md
+++ b/plugins/apollo-explorer/CHANGELOG.md
@@ -1,5 +1,32 @@
# @backstage/plugin-apollo-explorer
+## 0.1.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.11
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/core-components@0.13.1
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.11-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.11-next.0
### Patch Changes
diff --git a/plugins/apollo-explorer/README.md b/plugins/apollo-explorer/README.md
index 98f3116966..23ea71e934 100644
--- a/plugins/apollo-explorer/README.md
+++ b/plugins/apollo-explorer/README.md
@@ -38,17 +38,22 @@ yarn --cwd packages/app add @backstage/plugin-apollo-explorer
Then, in `packages/app/src/App.tsx` add the plugin as a `Route`
```typescript
-
+ {/* other routes... */}
+
+ }
/>
- }
-/>
```
Then, in `packages/app/src/components/Root/Root.tsx` add a sidebar item so users can find your beautiful plugin!
diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json
index c1164cf1d6..59325f4dc4 100644
--- a/plugins/apollo-explorer/package.json
+++ b/plugins/apollo-explorer/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-apollo-explorer",
- "version": "0.1.11-next.0",
+ "version": "0.1.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md
index 1e98f25c64..ef2230a370 100644
--- a/plugins/app-backend/CHANGELOG.md
+++ b/plugins/app-backend/CHANGELOG.md
@@ -1,5 +1,27 @@
# @backstage/plugin-app-backend
+## 0.3.46-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config-loader@1.3.1-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/types@1.0.2
+
+## 0.3.45
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/config-loader@1.3.0
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/config@1.0.7
+ - @backstage/types@1.0.2
+
## 0.3.45-next.1
### Patch Changes
diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json
index 85602a06e4..3f983e66e0 100644
--- a/plugins/app-backend/package.json
+++ b/plugins/app-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-app-backend",
"description": "A Backstage backend plugin that serves the Backstage frontend app",
- "version": "0.3.45-next.1",
+ "version": "0.3.46-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/auth-backend/CHANGELOG.md b/plugins/auth-backend/CHANGELOG.md
index c6ff454a7d..93b2d31e56 100644
--- a/plugins/auth-backend/CHANGELOG.md
+++ b/plugins/auth-backend/CHANGELOG.md
@@ -1,5 +1,42 @@
# @backstage/plugin-auth-backend
+## 0.18.4-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.15-next.0
+
+## 0.18.3
+
+### Patch Changes
+
+- 7c116bcac7f: Fixed the way that some request errors are thrown
+- 473db605a4f: Fix config schema definition.
+- 3ffcdac7d07: Added a persistent session store through the database
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 0.18.3-next.2
+
+### Patch Changes
+
+- 3ffcdac7d07: Added a persistent session store through the database
+- Updated dependencies
+ - @backstage/config@1.0.7
+
## 0.18.3-next.1
### Patch Changes
diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts
index 8d76ca8bf5..c6579e08f8 100644
--- a/plugins/auth-backend/config.d.ts
+++ b/plugins/auth-backend/config.d.ts
@@ -59,6 +59,7 @@ export interface Config {
/**
* The available auth-provider options and attributes
+ * @additionalProperties true
*/
providers?: {
google?: {
diff --git a/plugins/auth-backend/migrations/20230428155633_sessions.js b/plugins/auth-backend/migrations/20230428155633_sessions.js
new file mode 100644
index 0000000000..49462ac503
--- /dev/null
+++ b/plugins/auth-backend/migrations/20230428155633_sessions.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+// @ts-check
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.up = async function up(knex) {
+ // See https://github.com/gx0r/connect-session-knex
+ // Modeled loosely after https://github.com/gx0r/connect-session-knex/blob/4e0e36a9afbb13c3000a89f5e341f2d2d4339a02/lib/index.js#L114
+ // For simplicity we always make the session a string
+ // Do NOT change around this table or column names; the connect-session-knex library makes assumptions about them
+ await knex.schema.createTable('sessions', table => {
+ table.comment('Session data');
+ table.string('sid').primary().notNullable().comment('ID of the session');
+ table
+ .text('sess', 'longtext')
+ .notNullable()
+ .comment('Session data, JSON serialized');
+ table
+ .timestamp('expired')
+ .notNullable()
+ .comment('The point in time when the session expires');
+ table.index('sid', 'sessions_sid_idx');
+ table.index('expired', 'sessions_expired_idx');
+ });
+};
+
+/**
+ * @param {import('knex').Knex} knex
+ */
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('sessions', table => {
+ table.dropIndex([], 'sessions_sid_idx');
+ table.dropIndex([], 'sessions_expired_idx');
+ });
+ await knex.schema.dropTable('sessions');
+};
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 61c3fe9a8d..3425a17831 100644
--- a/plugins/auth-backend/package.json
+++ b/plugins/auth-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-auth-backend",
"description": "A Backstage backend plugin that handles authentication",
- "version": "0.18.3-next.1",
+ "version": "0.18.4-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -44,6 +44,7 @@
"@types/express": "^4.17.6",
"@types/passport": "^1.0.3",
"compression": "^1.7.4",
+ "connect-session-knex": "^3.0.1",
"cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"express": "^4.17.1",
diff --git a/plugins/auth-backend/src/database/AuthDatabase.ts b/plugins/auth-backend/src/database/AuthDatabase.ts
new file mode 100644
index 0000000000..97e4236754
--- /dev/null
+++ b/plugins/auth-backend/src/database/AuthDatabase.ts
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ DatabaseManager,
+ PluginDatabaseManager,
+ resolvePackagePath,
+} from '@backstage/backend-common';
+import { ConfigReader } from '@backstage/config';
+import { Knex } from 'knex';
+
+const migrationsDir = resolvePackagePath(
+ '@backstage/plugin-auth-backend',
+ 'migrations',
+);
+
+/**
+ * Ensures that a database connection is established exactly once and only when
+ * asked for, and runs migrations.
+ */
+export class AuthDatabase {
+ readonly #database: PluginDatabaseManager;
+ #promise: Promise | undefined;
+
+ static create(database: PluginDatabaseManager): AuthDatabase {
+ return new AuthDatabase(database);
+ }
+
+ /** @internal */
+ static forTesting(): AuthDatabase {
+ const config = new ConfigReader({
+ backend: {
+ database: {
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ useNullAsDefault: true,
+ },
+ },
+ });
+ const database = DatabaseManager.fromConfig(config).forPlugin('auth');
+ return new AuthDatabase(database);
+ }
+
+ static async runMigrations(knex: Knex): Promise {
+ await knex.migrate.latest({
+ directory: migrationsDir,
+ });
+ }
+
+ private constructor(database: PluginDatabaseManager) {
+ this.#database = database;
+ }
+
+ get(): Promise {
+ this.#promise ??= this.#database.getClient().then(async client => {
+ if (!this.#database.migrations?.skip) {
+ await AuthDatabase.runMigrations(client);
+ }
+ return client;
+ });
+
+ return this.#promise;
+ }
+}
diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts
index da67a2f88f..cf415c7e13 100644
--- a/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts
+++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.test.ts
@@ -14,33 +14,10 @@
* limitations under the License.
*/
-import Knex, { Knex as KnexType } from 'knex';
-import { DatabaseKeyStore } from './DatabaseKeyStore';
import { DateTime } from 'luxon';
-
-function createDatabaseManager(
- client: KnexType,
- skipMigrations: boolean = false,
-) {
- return {
- getClient: async () => client,
- migrations: {
- skip: skipMigrations,
- },
- };
-}
-
-function createDB() {
- const knex = Knex({
- client: 'better-sqlite3',
- connection: ':memory:',
- useNullAsDefault: true,
- });
- knex.client.pool.on('createSuccess', (_eventId: any, resource: any) => {
- resource.run('PRAGMA foreign_keys = ON', () => {});
- });
- return knex;
-}
+import { AuthDatabase } from '../database/AuthDatabase';
+import { DatabaseKeyStore } from './DatabaseKeyStore';
+import { TestDatabases } from '@backstage/backend-test-utils';
const keyBase = {
use: 'sig',
@@ -48,95 +25,107 @@ const keyBase = {
alg: 'Base64',
} as const;
+jest.setTimeout(60_000);
+
describe('DatabaseKeyStore', () => {
- it('should store a key', async () => {
- const client = createDB();
- const store = await DatabaseKeyStore.create({
- database: createDatabaseManager(client),
- });
-
- const key = {
- kid: '123',
- ...keyBase,
- };
-
- await expect(store.listKeys()).resolves.toEqual({ items: [] });
- await store.addKey(key);
-
- const { items } = await store.listKeys();
- expect(items).toEqual([{ createdAt: expect.anything(), key }]);
- expect(
- Math.abs(
- DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds,
- ),
- ).toBeLessThan(10);
+ const databases = TestDatabases.create({
+ ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
});
- it('should remove stored keys', async () => {
- const client = createDB();
- const store = await DatabaseKeyStore.create({
- database: createDatabaseManager(client),
- });
+ it.each(databases.eachSupportedId())(
+ 'should store a key, %p',
+ async databaseId => {
+ const knex = await databases.init(databaseId);
+ await AuthDatabase.runMigrations(knex);
- const key1 = { kid: '1', ...keyBase };
- const key2 = { kid: '2', ...keyBase };
- const key3 = { kid: '3', ...keyBase };
+ const store = new DatabaseKeyStore(knex);
- await store.addKey(key1);
- await store.addKey(key2);
- await store.addKey(key3);
+ const key = {
+ kid: '123',
+ ...keyBase,
+ };
- await expect(store.listKeys()).resolves.toEqual({
- items: [
- { key: key1, createdAt: expect.anything() },
- { key: key2, createdAt: expect.anything() },
- { key: key3, createdAt: expect.anything() },
- ],
- });
+ await expect(store.listKeys()).resolves.toEqual({ items: [] });
+ await store.addKey(key);
- store.removeKeys(['1']);
+ const { items } = await store.listKeys();
+ expect(items).toEqual([{ createdAt: expect.anything(), key }]);
+ expect(
+ Math.abs(
+ DateTime.fromJSDate(items[0].createdAt).diffNow('seconds').seconds,
+ ),
+ ).toBeLessThan(10);
+ },
+ );
- await expect(store.listKeys()).resolves.toEqual({
- items: [
- { key: key2, createdAt: expect.anything() },
- { key: key3, createdAt: expect.anything() },
- ],
- });
+ it.each(databases.eachSupportedId())(
+ 'should remove stored keys, %p',
+ async databaseId => {
+ const knex = await databases.init(databaseId);
+ await AuthDatabase.runMigrations(knex);
- store.removeKeys(['1', '2']);
+ const store = new DatabaseKeyStore(knex);
- await expect(store.listKeys()).resolves.toEqual({
- items: [{ key: key3, createdAt: expect.anything() }],
- });
+ const key1 = { kid: '1', ...keyBase };
+ const key2 = { kid: '2', ...keyBase };
+ const key3 = { kid: '3', ...keyBase };
- store.removeKeys([]);
+ await store.addKey(key1);
+ await store.addKey(key2);
+ await store.addKey(key3);
- await expect(store.listKeys()).resolves.toEqual({
- items: [{ key: key3, createdAt: expect.anything() }],
- });
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [
+ { key: key1, createdAt: expect.anything() },
+ { key: key2, createdAt: expect.anything() },
+ { key: key3, createdAt: expect.anything() },
+ ],
+ });
- store.removeKeys(['3', '4']);
+ await store.removeKeys(['1']);
- await expect(store.listKeys()).resolves.toEqual({
- items: [],
- });
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [
+ { key: key2, createdAt: expect.anything() },
+ { key: key3, createdAt: expect.anything() },
+ ],
+ });
- await store.addKey(key1);
- await store.addKey(key2);
- await store.addKey(key3);
+ await store.removeKeys(['1', '2']);
- await expect(store.listKeys()).resolves.toEqual({
- items: [
- { key: key1, createdAt: expect.anything() },
- { key: key2, createdAt: expect.anything() },
- { key: key3, createdAt: expect.anything() },
- ],
- });
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [{ key: key3, createdAt: expect.anything() }],
+ });
- store.removeKeys(['1', '2', '3']);
+ await store.removeKeys([]);
- await expect(store.listKeys()).resolves.toEqual({
- items: [],
- });
- });
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [{ key: key3, createdAt: expect.anything() }],
+ });
+
+ await store.removeKeys(['3', '4']);
+
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [],
+ });
+
+ await store.addKey(key1);
+ await store.addKey(key2);
+ await store.addKey(key3);
+
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [
+ { key: key1, createdAt: expect.anything() },
+ { key: key2, createdAt: expect.anything() },
+ { key: key3, createdAt: expect.anything() },
+ ],
+ });
+
+ await store.removeKeys(['1', '2', '3']);
+
+ await expect(store.listKeys()).resolves.toEqual({
+ items: [],
+ });
+ },
+ );
});
diff --git a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
index 8039837469..0559b698e9 100644
--- a/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
+++ b/plugins/auth-backend/src/identity/DatabaseKeyStore.ts
@@ -14,19 +14,10 @@
* limitations under the License.
*/
-import {
- PluginDatabaseManager,
- resolvePackagePath,
-} from '@backstage/backend-common';
import { Knex } from 'knex';
import { DateTime } from 'luxon';
import { AnyJWK, KeyStore, StoredKey } from './types';
-const migrationsDir = resolvePackagePath(
- '@backstage/plugin-auth-backend',
- 'migrations',
-);
-
const TABLE = 'signing_keys';
type Row = {
@@ -35,10 +26,6 @@ type Row = {
key: string;
};
-type Options = {
- database: PluginDatabaseManager;
-};
-
const parseDate = (date: string | Date) => {
const parsedDate =
typeof date === 'string'
@@ -55,24 +42,7 @@ const parseDate = (date: string | Date) => {
};
export class DatabaseKeyStore implements KeyStore {
- static async create(options: Options): Promise {
- const { database } = options;
- const client = await database.getClient();
-
- if (!database.migrations?.skip) {
- await client.migrate.latest({
- directory: migrationsDir,
- });
- }
-
- return new DatabaseKeyStore(client);
- }
-
- private readonly client: Knex;
-
- private constructor(client: Knex) {
- this.client = client;
- }
+ constructor(private readonly client: Knex) {}
async addKey(key: AnyJWK): Promise {
await this.client(TABLE).insert({
diff --git a/plugins/auth-backend/src/identity/KeyStores.test.ts b/plugins/auth-backend/src/identity/KeyStores.test.ts
index 375db4698b..6c034eb64b 100644
--- a/plugins/auth-backend/src/identity/KeyStores.test.ts
+++ b/plugins/auth-backend/src/identity/KeyStores.test.ts
@@ -14,13 +14,13 @@
* limitations under the License.
*/
-import { DatabaseManager } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
-
-import { MemoryKeyStore } from './MemoryKeyStore';
+import { AuthDatabase } from '../database/AuthDatabase';
import { DatabaseKeyStore } from './DatabaseKeyStore';
import { FirestoreKeyStore } from './FirestoreKeyStore';
import { KeyStores } from './KeyStores';
+import { MemoryKeyStore } from './MemoryKeyStore';
+import { getVoidLogger } from '@backstage/backend-common';
describe('KeyStores', () => {
const defaultConfigOptions = {
@@ -34,7 +34,10 @@ describe('KeyStores', () => {
it('reads auth section from config', async () => {
const configSpy = jest.spyOn(defaultConfig, 'getOptionalConfig');
- const keyStore = await KeyStores.fromConfig(defaultConfig);
+ const keyStore = await KeyStores.fromConfig(defaultConfig, {
+ logger: getVoidLogger(),
+ database: AuthDatabase.forTesting(),
+ });
expect(keyStore).toBeInstanceOf(MemoryKeyStore);
expect(configSpy).toHaveBeenCalledWith('auth.keyStore');
@@ -46,18 +49,10 @@ describe('KeyStores', () => {
});
it('can handle without auth config', async () => {
- const config = new ConfigReader({
- backend: {
- database: {
- client: 'better-sqlite3',
- connection: ':memory:',
- },
- },
+ const keyStore = await KeyStores.fromConfig(new ConfigReader({}), {
+ logger: getVoidLogger(),
+ database: AuthDatabase.forTesting(),
});
- const database =
- DatabaseManager.fromConfig(config).forPlugin('auth-backend');
- const keyStore = await KeyStores.fromConfig(config, { database });
-
expect(keyStore).toBeInstanceOf(DatabaseKeyStore);
});
@@ -82,7 +77,10 @@ describe('KeyStores', () => {
},
};
const config = new ConfigReader(configOptions);
- const keyStore = await KeyStores.fromConfig(config);
+ const keyStore = await KeyStores.fromConfig(config, {
+ logger: getVoidLogger(),
+ database: AuthDatabase.forTesting(),
+ });
expect(keyStore).toBeInstanceOf(FirestoreKeyStore);
expect(createSpy).toHaveBeenCalledWith(
diff --git a/plugins/auth-backend/src/identity/KeyStores.ts b/plugins/auth-backend/src/identity/KeyStores.ts
index e29b4b6fc4..4cd8f91842 100644
--- a/plugins/auth-backend/src/identity/KeyStores.ts
+++ b/plugins/auth-backend/src/identity/KeyStores.ts
@@ -14,20 +14,20 @@
* limitations under the License.
*/
-import { Logger } from 'winston';
import { pickBy } from 'lodash';
+import { Logger } from 'winston';
-import { PluginDatabaseManager } from '@backstage/backend-common';
import { Config } from '@backstage/config';
+import { AuthDatabase } from '../database/AuthDatabase';
import { DatabaseKeyStore } from './DatabaseKeyStore';
-import { MemoryKeyStore } from './MemoryKeyStore';
import { FirestoreKeyStore } from './FirestoreKeyStore';
+import { MemoryKeyStore } from './MemoryKeyStore';
import { KeyStore } from './types';
type Options = {
- logger?: Logger;
- database?: PluginDatabaseManager;
+ logger: Logger;
+ database: AuthDatabase;
};
export class KeyStores {
@@ -37,23 +37,16 @@ export class KeyStores {
*
* @returns a KeyStore store
*/
- static async fromConfig(
- config: Config,
- options?: Options,
- ): Promise {
- const { logger, database } = options ?? {};
+ static async fromConfig(config: Config, options: Options): Promise {
+ const { logger, database } = options;
const ks = config.getOptionalConfig('auth.keyStore');
const provider = ks?.getOptionalString('provider') ?? 'database';
- logger?.info(`Configuring "${provider}" as KeyStore provider`);
+ logger.info(`Configuring "${provider}" as KeyStore provider`);
if (provider === 'database') {
- if (!database) {
- throw new Error('This KeyStore provider requires a database');
- }
-
- return await DatabaseKeyStore.create({ database });
+ return new DatabaseKeyStore(await database.get());
}
if (provider === 'memory') {
diff --git a/plugins/auth-backend/src/migrations.test.ts b/plugins/auth-backend/src/migrations.test.ts
new file mode 100644
index 0000000000..194bbbf44c
--- /dev/null
+++ b/plugins/auth-backend/src/migrations.test.ts
@@ -0,0 +1,76 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Knex } from 'knex';
+import { TestDatabases } from '@backstage/backend-test-utils';
+import fs from 'fs';
+
+const migrationsDir = `${__dirname}/../migrations`;
+const migrationsFiles = fs.readdirSync(migrationsDir).sort();
+
+async function migrateUpOnce(knex: Knex): Promise {
+ await knex.migrate.up({ directory: migrationsDir });
+}
+
+async function migrateDownOnce(knex: Knex): Promise {
+ await knex.migrate.down({ directory: migrationsDir });
+}
+
+async function migrateUntilBefore(knex: Knex, target: string): Promise {
+ const index = migrationsFiles.indexOf(target);
+ if (index === -1) {
+ throw new Error(`Migration ${target} not found`);
+ }
+ for (let i = 0; i < index; i++) {
+ await migrateUpOnce(knex);
+ }
+}
+
+jest.setTimeout(60_000);
+
+describe('migrations', () => {
+ const databases = TestDatabases.create({
+ ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
+ });
+
+ it.each(databases.eachSupportedId())(
+ '20230428155633_sessions.js, %p',
+ async databaseId => {
+ const knex = await databases.init(databaseId);
+
+ await migrateUntilBefore(knex, '20230428155633_sessions.js');
+ await migrateUpOnce(knex);
+
+ // Ensure that large cookies are supported
+ const data = `{"cookie":"${'a'.repeat(100_000)}"}`;
+ await knex
+ .insert({ sid: 'abc', expired: knex.fn.now(), sess: data })
+ .into('sessions');
+ await knex
+ .insert({ sid: 'def', expired: knex.fn.now(), sess: data })
+ .into('sessions');
+
+ await expect(knex('sessions').orderBy('sid', 'asc')).resolves.toEqual([
+ { sid: 'abc', expired: expect.anything(), sess: data },
+ { sid: 'def', expired: expect.anything(), sess: data },
+ ]);
+
+ await migrateDownOnce(knex);
+
+ await knex.destroy();
+ },
+ );
+});
diff --git a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts
index 0d8f736550..6313276508 100644
--- a/plugins/auth-backend/src/providers/cloudflare-access/provider.ts
+++ b/plugins/auth-backend/src/providers/cloudflare-access/provider.ts
@@ -227,7 +227,7 @@ export class CloudflareAccessAuthProvider implements AuthProviderRouteHandlers {
{ headers },
);
if (!res.ok) {
- throw ResponseError.fromResponse(res);
+ throw await ResponseError.fromResponse(res);
}
const cfIdentity = await res.json();
return cfIdentity as unknown as CloudflareAccessIdentityProfile;
diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts
index 8013777415..2996ff68be 100644
--- a/plugins/auth-backend/src/service/router.ts
+++ b/plugins/auth-backend/src/service/router.ts
@@ -32,9 +32,11 @@ import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { createOidcRouter, TokenFactory, KeyStores } from '../identity';
import session from 'express-session';
+import connectSessionKnex from 'connect-session-knex';
import passport from 'passport';
import { Minimatch } from 'minimatch';
import { CatalogAuthResolverContext } from '../lib/resolvers';
+import { AuthDatabase } from '../database/AuthDatabase';
/** @public */
export type ProviderFactories = { [s: string]: AuthProviderFactory };
@@ -70,7 +72,11 @@ export async function createRouter(
const appUrl = config.getString('app.baseUrl');
const authUrl = await discovery.getExternalBaseUrl('auth');
- const keyStore = await KeyStores.fromConfig(config, { logger, database });
+ const authDb = AuthDatabase.create(database);
+ const keyStore = await KeyStores.fromConfig(config, {
+ logger,
+ database: authDb,
+ });
const keyDurationSeconds = 3600;
const tokenIssuer = new TokenFactory({
@@ -84,14 +90,18 @@ export async function createRouter(
const secret = config.getOptionalString('auth.session.secret');
if (secret) {
router.use(cookieParser(secret));
- // TODO: Configure the server-side session storage. The default MemoryStore is not designed for production
const enforceCookieSSL = authUrl.startsWith('https');
+ const KnexSessionStore = connectSessionKnex(session);
router.use(
session({
secret,
saveUninitialized: false,
resave: false,
cookie: { secure: enforceCookieSSL ? 'auto' : false },
+ store: new KnexSessionStore({
+ createtable: false,
+ knex: await authDb.get(),
+ }),
}),
);
router.use(passport.initialize());
diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md
index 7cb9f4cc64..3dacefa8d0 100644
--- a/plugins/auth-node/CHANGELOG.md
+++ b/plugins/auth-node/CHANGELOG.md
@@ -1,5 +1,23 @@
# @backstage/plugin-auth-node
+## 0.2.15-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
+## 0.2.14
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 0.2.14-next.1
### Patch Changes
diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json
index 1d8e71a4c0..155c107525 100644
--- a/plugins/auth-node/package.json
+++ b/plugins/auth-node/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-auth-node",
- "version": "0.2.14-next.1",
+ "version": "0.2.15-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/azure-devops-backend/CHANGELOG.md b/plugins/azure-devops-backend/CHANGELOG.md
index 3dd206ae66..cf6e17985c 100644
--- a/plugins/azure-devops-backend/CHANGELOG.md
+++ b/plugins/azure-devops-backend/CHANGELOG.md
@@ -1,5 +1,23 @@
# @backstage/plugin-azure-devops-backend
+## 0.3.25-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-azure-devops-common@0.3.0
+
+## 0.3.24
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/config@1.0.7
+ - @backstage/plugin-azure-devops-common@0.3.0
+
## 0.3.24-next.1
### Patch Changes
diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json
index f28bfa5cb4..af4e285bc7 100644
--- a/plugins/azure-devops-backend/package.json
+++ b/plugins/azure-devops-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-devops-backend",
- "version": "0.3.24-next.1",
+ "version": "0.3.25-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/azure-devops/CHANGELOG.md b/plugins/azure-devops/CHANGELOG.md
index a0328bcb22..7bbf34f381 100644
--- a/plugins/azure-devops/CHANGELOG.md
+++ b/plugins/azure-devops/CHANGELOG.md
@@ -1,5 +1,49 @@
# @backstage/plugin-azure-devops
+## 0.3.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-azure-devops-common@0.3.0
+
+## 0.3.0
+
+### Minor Changes
+
+- 877df261085: The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-azure-devops-common@0.3.0
+
+## 0.3.0-next.2
+
+### Minor Changes
+
+- 877df261085: The getBuildRuns function now checks contains multiple comma-separated builds and splits them to send multiple requests for each and concatenates the results.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.9-next.1
### Patch Changes
diff --git a/plugins/azure-devops/README.md b/plugins/azure-devops/README.md
index 5079fa4f16..7eeae09813 100644
--- a/plugins/azure-devops/README.md
+++ b/plugins/azure-devops/README.md
@@ -70,7 +70,7 @@ dev.azure.com/project:
dev.azure.com/build-definition:
```
-In this case `` will be the name of your Team Project and `` will be the name of the Build Definition you would like to see Builds for. If the Build Definition name has spaces in it make sure to put quotes around it
+In this case `` will be the name of your Team Project and `` will be the name of the Build Definition you would like to see Builds for, and it's possible to add more Builds separated by a comma. If the Build Definition name has spaces in it make sure to put quotes around it.
### Azure Pipelines Component
diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json
index 914cc9a4ba..0e9b38dfe7 100644
--- a/plugins/azure-devops/package.json
+++ b/plugins/azure-devops/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-devops",
- "version": "0.2.9-next.1",
+ "version": "0.3.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/azure-devops/src/api/AzureDevOpsClient.ts b/plugins/azure-devops/src/api/AzureDevOpsClient.ts
index 68d8896538..a202008798 100644
--- a/plugins/azure-devops/src/api/AzureDevOpsClient.ts
+++ b/plugins/azure-devops/src/api/AzureDevOpsClient.ts
@@ -120,7 +120,23 @@ export class AzureDevOpsClient implements AzureDevOpsApi {
queryString.append('repoName', repoName);
}
if (definitionName) {
- queryString.append('definitionName', definitionName);
+ const definitionNames = definitionName.split(',');
+ if (definitionNames.length > 1) {
+ const buildRuns: BuildRun[] = [];
+ for (const name of definitionNames) {
+ queryString.set('definitionName', name.trim());
+ if (options?.top) {
+ queryString.set('top', options.top.toString());
+ }
+ const urlSegment = `builds/${encodeURIComponent(
+ projectName,
+ )}?${queryString}`;
+ const items = await this.get(urlSegment);
+ buildRuns.push(...items);
+ }
+ return { items: buildRuns };
+ }
+ queryString.append('definitionName', definitionName.trim());
}
if (options?.top) {
queryString.append('top', options.top.toString());
diff --git a/plugins/azure-sites-backend/CHANGELOG.md b/plugins/azure-sites-backend/CHANGELOG.md
index 9c25a70214..bb29d92f51 100644
--- a/plugins/azure-sites-backend/CHANGELOG.md
+++ b/plugins/azure-sites-backend/CHANGELOG.md
@@ -1,5 +1,24 @@
# @backstage/plugin-azure-sites-backend
+## 0.1.8-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-azure-sites-common@0.1.0
+
+## 0.1.7
+
+### Patch Changes
+
+- d66d4f916aa: Updated URL to `/health` and corrected typos in the `README.md`
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/config@1.0.7
+ - @backstage/plugin-azure-sites-common@0.1.0
+
## 0.1.7-next.1
### Patch Changes
diff --git a/plugins/azure-sites-backend/README.md b/plugins/azure-sites-backend/README.md
index 0d44b9a4e4..23152f9760 100644
--- a/plugins/azure-sites-backend/README.md
+++ b/plugins/azure-sites-backend/README.md
@@ -2,7 +2,7 @@
Simple plugin that proxies requests to the Azure Portal API through Azure SDK JavaScript libraries.
-_Inspired by [roadie.io AWS Lamda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_
+_Inspired by [roadie.io AWS Lambda plugin](https://roadie.io/backstage/plugins/aws-lambda/)_
## Setup
@@ -65,7 +65,7 @@ Here's how to get the backend plugin up and running:
```ts
import azure from './plugins/azure';
- // Removed for clairty...
+ // Removed for clarity...
async function main() {
// ...
@@ -82,4 +82,4 @@ Here's how to get the backend plugin up and running:
4. Now run `yarn start-backend` from the repo root.
-5. Finally, open `http://localhost:7007/api/azure/health` in a browser, it should return `{"status":"ok"}`.
+5. Finally, open `http://localhost:7007/api/azure-sites/health` in a browser, it should return `{"status":"ok"}`.
diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json
index a58459d2aa..84f5fcb6a1 100644
--- a/plugins/azure-sites-backend/package.json
+++ b/plugins/azure-sites-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-sites-backend",
- "version": "0.1.7-next.1",
+ "version": "0.1.8-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/azure-sites/CHANGELOG.md b/plugins/azure-sites/CHANGELOG.md
index 1eea015834..3412e100c1 100644
--- a/plugins/azure-sites/CHANGELOG.md
+++ b/plugins/azure-sites/CHANGELOG.md
@@ -1,5 +1,39 @@
# @backstage/plugin-azure-sites
+## 0.1.8-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/plugin-azure-sites-common@0.1.0
+
+## 0.1.7
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-azure-sites-common@0.1.0
+
+## 0.1.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.7-next.1
### Patch Changes
diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json
index 8863e76dae..59bfaf839a 100644
--- a/plugins/azure-sites/package.json
+++ b/plugins/azure-sites/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-azure-sites",
- "version": "0.1.7-next.1",
+ "version": "0.1.8-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md
index 50bf55c918..57e5d63270 100644
--- a/plugins/badges-backend/CHANGELOG.md
+++ b/plugins/badges-backend/CHANGELOG.md
@@ -1,5 +1,53 @@
# @backstage/plugin-badges-backend
+## 0.2.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-auth-node@0.2.15-next.0
+
+## 0.2.0
+
+### Minor Changes
+
+- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+ Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+ **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
+
+### Patch Changes
+
+- 0cd552c28d8: Removed some unused dependencies
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
+## 0.2.0-next.2
+
+### Minor Changes
+
+- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+ Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+ **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+
## 0.1.39-next.1
### Patch Changes
diff --git a/plugins/badges-backend/README.md b/plugins/badges-backend/README.md
index 63e43c6692..ddbb30bc4e 100644
--- a/plugins/badges-backend/README.md
+++ b/plugins/badges-backend/README.md
@@ -36,6 +36,9 @@ export default async function createPlugin(
config: env.config,
discovery: env.discovery,
badgeFactories: createDefaultBadgeFactories(),
+ tokenManager: env.tokenManager,
+ logger: env.logger,
+ identity: env.identity,
});
}
```
@@ -112,11 +115,29 @@ export const createMyCustomBadgeFactories = (): BadgeFactories => ({
});
```
+### Badge obfuscation
+
+When you enable the obfuscation feature, the badges backend will obfuscate the entity names in the badge link. It's useful when you want your badges to be visible to the public, but you don't want to expose the entity names and also to protect your entity names from being enumerated.
+
+To enable the obfuscation you need to activate the `obfuscation` feature in the `app-config.yaml`:
+
+```yaml
+app:
+ badges:
+ obfuscate: true
+```
+
+:warning: **Warning**: The only endpoint to be publicly available is the `/entity/:entityUuid/:badgeId` endpoint. The other endpoints are meant for trusted internal users and should not be publicly exposed.
+
+> Note that you cannot use env vars to set the `obfuscate` value. It must be a boolean value and env vars are always strings.
+
## API
The badges backend api exposes two main endpoints for entity badges. The
`/badges` prefix is arbitrary, and the default for the example backend.
+### If obfuscation is disabled (default or apps.badges.obfuscate: false)
+
- `/badges/entity/:namespace/:kind/:name/badge-specs` List all defined badges
for a particular entity, in json format. See
[BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts)
@@ -126,6 +147,21 @@ The badges backend api exposes two main endpoints for entity badges. The
an SVG image. If the `accept` request header prefers `application/json` the
badge spec as JSON will be returned instead of the image.
+### If obfuscation is enabled (apps.badges.obfuscate: true)
+
+- `/badges/entity/:namespace/:kind/:name/obfuscated` Get the obfuscated `entity url`.
+
+> Note that endpoint have a embedded authMiddleware to authenticate the user requesting this endpoint. _It meant to be called from the frontend plugin._
+
+- `/badges/entity/:entityUuid/:badgeId` Get the entity badge as an SVG image. If
+ the `accept` request header prefers `application/json` the badge spec as JSON
+ will be returned instead of the image.
+
+- `/badge/entity/:entityUuid/badge-specs` List all defined badges for a
+ particular entity, in json format. See
+ [BadgeSpec](https://github.com/backstage/backstage/tree/master/plugins/badges/src/api/types.ts)
+ from the frontend plugin for a type declaration.
+
## Links
- [Frontend part of the plugin](https://github.com/backstage/backstage/tree/master/plugins/badges)
diff --git a/plugins/badges-backend/api-report.md b/plugins/badges-backend/api-report.md
index 8ca4b561a2..09a02ba44d 100644
--- a/plugins/badges-backend/api-report.md
+++ b/plugins/badges-backend/api-report.md
@@ -7,7 +7,10 @@ import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
+import { IdentityApi } from '@backstage/plugin-auth-node';
+import { Logger } from 'winston';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { TokenManager } from '@backstage/backend-common';
// @public (undocumented)
export interface Badge {
@@ -78,6 +81,27 @@ export type BadgeSpec = {
markdown: string;
};
+// @public
+export interface BadgesStore {
+ // (undocumented)
+ getBadgeFromUuid(uuid: string): Promise<
+ | {
+ name: string;
+ namespace: string;
+ kind: string;
+ }
+ | undefined
+ >;
+ // (undocumented)
+ getBadgeUuid(
+ name: string,
+ namespace: string,
+ kind: string,
+ ): Promise<{
+ uuid: string;
+ }>;
+}
+
// @public (undocumented)
export type BadgeStyle = (typeof BADGE_STYLES)[number];
@@ -107,10 +131,18 @@ export interface RouterOptions {
// (undocumented)
badgeFactories?: BadgeFactories;
// (undocumented)
+ badgeStore?: BadgesStore;
+ // (undocumented)
catalog?: CatalogApi;
// (undocumented)
config: Config;
// (undocumented)
discovery: PluginEndpointDiscovery;
+ // (undocumented)
+ identity: IdentityApi;
+ // (undocumented)
+ logger: Logger;
+ // (undocumented)
+ tokenManager: TokenManager;
}
```
diff --git a/plugins/badges-backend/migrations/20230404_init.js b/plugins/badges-backend/migrations/20230404_init.js
new file mode 100644
index 0000000000..3de6bfc248
--- /dev/null
+++ b/plugins/badges-backend/migrations/20230404_init.js
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+exports.up = async function up(knex) {
+ await knex.schema.createTable('badges', table => {
+ table.string('kind').notNullable();
+ table.string('namespace').notNullable();
+ table.string('name').notNullable();
+ table.string('uuid').unique().notNullable();
+ table.index(['uuid'], 'badges_uuid_index');
+ table.primary(['kind', 'namespace', 'name']);
+ });
+};
+
+exports.down = async function down(knex) {
+ await knex.schema.alterTable('badges', table => {
+ table.dropIndex('', 'badges_uuid_index');
+ });
+ await knex.schema.dropTable('badges');
+};
diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json
index 5f9b5b51a3..e29d3f9e8f 100644
--- a/plugins/badges-backend/package.json
+++ b/plugins/badges-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-badges-backend",
"description": "A Backstage backend plugin that generates README badges for your entities",
- "version": "0.1.39-next.1",
+ "version": "0.2.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -38,18 +38,25 @@
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
"@types/express": "^4.17.6",
"badge-maker": "^3.3.0",
"cors": "^2.8.5",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
+ "knex": "^2.4.2",
+ "lodash": "^4.17.21",
+ "supertest": "^6.3.3",
+ "uuid": "^9.0.0",
"winston": "^3.2.1",
"yn": "^4.0.0"
},
"devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/catalog-client": "workspace:^",
"@backstage/cli": "workspace:^",
- "@types/supertest": "^2.0.8",
- "supertest": "^6.1.3"
+ "@types/node": "*",
+ "cross-fetch": "^3.1.5"
},
"files": [
"dist"
diff --git a/plugins/badges-backend/src/database/badgeStore.test.ts b/plugins/badges-backend/src/database/badgeStore.test.ts
new file mode 100644
index 0000000000..5a7a82d4b7
--- /dev/null
+++ b/plugins/badges-backend/src/database/badgeStore.test.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { DatabaseBadgesStore } from './badgesStore';
+import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils';
+import { Knex } from 'knex';
+import { Entity } from '@backstage/catalog-model';
+
+describe('DatabaseBadgesStore', () => {
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'test',
+ },
+ };
+
+ const databases = TestDatabases.create();
+
+ async function createDatabaseBadgesStore(databaseId: TestDatabaseId) {
+ const knex = await databases.init(databaseId);
+ return {
+ knex,
+ badgeStore: await DatabaseBadgesStore.create({
+ database: { getClient: async () => knex },
+ }),
+ };
+ }
+
+ describe.each(databases.eachSupportedId())('%p', databaseId => {
+ let knex: Knex;
+ let badgeStore: DatabaseBadgesStore;
+
+ beforeEach(async () => {
+ ({ knex, badgeStore } = await createDatabaseBadgesStore(databaseId));
+ });
+
+ it('createABadge if not existing in DB', async () => {
+ const uuid = await badgeStore.getBadgeUuid(
+ entity.metadata.name,
+ entity.metadata.namespace || 'default',
+ entity.kind,
+ );
+
+ expect(uuid.uuid).toMatch(
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/,
+ );
+
+ const storedBadge = await badgeStore.getBadgeFromUuid(uuid.uuid);
+ expect(storedBadge?.kind).toEqual(entity.kind);
+ expect(storedBadge?.name).toEqual(entity.metadata.name);
+ expect(storedBadge?.namespace).toEqual(
+ entity.metadata.namespace || 'default',
+ );
+ });
+
+ it('getBadge if badge already exist in DB', async () => {
+ await knex('badges').truncate();
+ await knex('badges').insert([
+ {
+ uuid: 'uuid1',
+ name: 'test',
+ namespace: 'default',
+ kind: 'component',
+ },
+ ]);
+
+ const storedEntity = await badgeStore.getBadgeFromUuid('uuid1');
+
+ expect(storedEntity).toEqual({
+ name: 'test',
+ namespace: 'default',
+ kind: 'component',
+ });
+ });
+
+ it('getBadgeUuid if badge exist in DB', async () => {
+ await knex('badges').truncate();
+ await knex('badges').insert([
+ {
+ uuid: 'uuid1',
+ name: 'test',
+ namespace: 'default',
+ kind: 'component',
+ },
+ ]);
+
+ const storedUuid = await badgeStore.getBadgeUuid(
+ 'test',
+ 'default',
+ 'component',
+ );
+
+ expect(storedUuid).toEqual({ uuid: 'uuid1' });
+ });
+ });
+});
diff --git a/plugins/badges-backend/src/database/badgesStore.ts b/plugins/badges-backend/src/database/badgesStore.ts
new file mode 100644
index 0000000000..e6c6a9e2ad
--- /dev/null
+++ b/plugins/badges-backend/src/database/badgesStore.ts
@@ -0,0 +1,110 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ PluginDatabaseManager,
+ resolvePackagePath,
+} from '@backstage/backend-common';
+import { Knex } from 'knex';
+import { isNil } from 'lodash';
+import { v4 as uuidv4 } from 'uuid';
+
+/**
+ * internal
+ * @public
+ */
+export interface BadgesStore {
+ getBadgeUuid(
+ name: string,
+ namespace: string,
+ kind: string,
+ ): Promise<{ uuid: string }>;
+
+ getBadgeFromUuid(
+ uuid: string,
+ ): Promise<{ name: string; namespace: string; kind: string } | undefined>;
+}
+
+const migrationsDir = resolvePackagePath(
+ '@backstage/plugin-badges-backend', // Package name
+ 'migrations', // Migrations directory
+);
+
+/**
+ * DatabaseBadgesStore
+ * @internal
+ */
+export class DatabaseBadgesStore implements BadgesStore {
+ private constructor(private readonly db: Knex) {}
+
+ static async create({
+ database,
+ skipMigrations,
+ }: {
+ database: PluginDatabaseManager;
+ skipMigrations?: boolean;
+ }): Promise {
+ const client = await database.getClient();
+
+ if (!database.migrations?.skip && !skipMigrations) {
+ await client.migrate.latest({
+ directory: migrationsDir,
+ });
+ }
+
+ return new DatabaseBadgesStore(client);
+ }
+
+ async getBadgeFromUuid(
+ uuid: string,
+ ): Promise<{ name: string; namespace: string; kind: string } | undefined> {
+ const result = await this.db('badges')
+ .select('namespace', 'name', 'kind')
+ .where({ uuid: uuid })
+ .first();
+
+ return result;
+ }
+
+ async getBadgeUuid(
+ name: string,
+ namespace: string,
+ kind: string,
+ ): Promise<{ uuid: string }> {
+ const result = await this.db('badges')
+ .select('uuid')
+ .where({ name: name, namespace: namespace, kind: kind })
+ .first();
+
+ let uuid = result?.uuid;
+
+ if (isNil(uuid)) {
+ uuid = uuidv4();
+
+ await this.db('badges')
+ .insert({
+ uuid: uuid,
+ name: name,
+ namespace: namespace,
+ kind: kind,
+ })
+ .onConflict(['name', 'namespace', 'kind'])
+ .ignore();
+ }
+
+ return { uuid };
+ }
+}
diff --git a/plugins/badges-backend/src/index.ts b/plugins/badges-backend/src/index.ts
index a45714cb81..35a061c516 100644
--- a/plugins/badges-backend/src/index.ts
+++ b/plugins/badges-backend/src/index.ts
@@ -24,3 +24,4 @@ export * from './badges';
export * from './lib';
export * from './service/router';
export * from './types';
+export * from './database/badgesStore';
diff --git a/plugins/badges-backend/src/service/router-obfuscated.test.ts b/plugins/badges-backend/src/service/router-obfuscated.test.ts
new file mode 100644
index 0000000000..3c9ec0d2b6
--- /dev/null
+++ b/plugins/badges-backend/src/service/router-obfuscated.test.ts
@@ -0,0 +1,302 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import express from 'express';
+import request from 'supertest';
+import {
+ getVoidLogger,
+ PluginEndpointDiscovery,
+ ServerTokenManager,
+ SingleHostDiscovery,
+} from '@backstage/backend-common';
+import { CatalogApi } from '@backstage/catalog-client';
+import type { Entity } from '@backstage/catalog-model';
+import { Config, ConfigReader } from '@backstage/config';
+import { createRouter } from './router';
+import { BadgeBuilder } from '../lib';
+import {
+ BackstageIdentityResponse,
+ IdentityApiGetIdentityRequest,
+} from '@backstage/plugin-auth-node';
+import { BadgesStore } from '../database/badgesStore';
+
+describe('createRouter', () => {
+ let app: express.Express;
+ const badgeBuilder: jest.Mocked = {
+ getBadges: jest.fn(),
+ createBadgeJson: jest.fn(),
+ createBadgeSvg: jest.fn(),
+ };
+
+ const catalog = {
+ addLocation: jest.fn(),
+ getEntities: jest.fn(),
+ getEntityByRef: jest.fn(),
+ getLocationByRef: jest.fn(),
+ getLocationById: jest.fn(),
+ removeLocationById: jest.fn(),
+ removeEntityByUid: jest.fn(),
+ refreshEntity: jest.fn(),
+ getEntityAncestors: jest.fn(),
+ getEntityFacets: jest.fn(),
+ validateEntity: jest.fn(),
+ };
+ const getIdentity = jest
+ .fn()
+ .mockImplementation(
+ async ({
+ request: _request,
+ }: IdentityApiGetIdentityRequest): Promise<
+ BackstageIdentityResponse | undefined
+ > => {
+ return {
+ identity: {
+ userEntityRef: 'user:default/guest',
+ ownershipEntityRefs: [],
+ type: 'user',
+ },
+ token: 'token',
+ };
+ },
+ );
+
+ const config: Config = new ConfigReader({
+ backend: {
+ baseUrl: 'http://127.0.0.1',
+ listen: {
+ port: 7007,
+ },
+ database: {
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ },
+ },
+ app: {
+ badges: {
+ obfuscate: true,
+ },
+ },
+ });
+
+ let discovery: PluginEndpointDiscovery;
+
+ const entity: Entity = {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'test',
+ },
+ };
+
+ const entities: Entity[] = [
+ entity,
+ {
+ apiVersion: 'v1',
+ kind: 'Component',
+ metadata: {
+ name: 'test-2',
+ },
+ },
+ ];
+
+ const badge = {
+ id: 'test-badge',
+ badge: {
+ label: 'test',
+ message: 'badge',
+ },
+ url: '/...',
+ markdown: '[]',
+ };
+
+ const badgeEntity = {
+ name: 'test',
+ namespace: 'default',
+ kind: 'component',
+ };
+
+ const badgeStore: jest.Mocked = {
+ getBadgeUuid: jest.fn().mockImplementation(async () => {
+ return { uuid: 'uuid1' };
+ }),
+ getBadgeFromUuid: jest.fn().mockImplementation(async () => {
+ return badgeEntity;
+ }),
+ };
+
+ beforeAll(async () => {
+ discovery = SingleHostDiscovery.fromConfig(config);
+ const tokenManager = ServerTokenManager.noop();
+ const router = await createRouter({
+ badgeBuilder,
+ catalog: catalog as Partial as CatalogApi,
+ config,
+ discovery,
+ tokenManager,
+ logger: getVoidLogger(),
+ identity: { getIdentity },
+ });
+ app = express().use(router);
+ });
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('works with provided badgeStore', async () => {
+ const tokenManager = ServerTokenManager.noop();
+ const router = await createRouter({
+ badgeBuilder,
+ catalog: catalog as Partial as CatalogApi,
+ config,
+ discovery,
+ tokenManager,
+ logger: getVoidLogger(),
+ identity: { getIdentity },
+ badgeStore: badgeStore,
+ });
+ expect(router).toBeDefined();
+ });
+
+ describe('GET /entity/:namespace/:kind/:name/badge-specs', () => {
+ it('does not returns all badge specs for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+
+ badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ const response = await request(app).get(
+ '/entity/default/component/test/badge-specs',
+ );
+
+ expect(response.status).toEqual(404);
+ });
+ });
+
+ describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => {
+ it('does not returns badge for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+
+ const image = '';
+ badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
+
+ const response = await request(app).get(
+ '/entity/default/component/test/badge/test-badge',
+ );
+
+ expect(response.status).toEqual(404);
+ });
+
+ it('does not returns badge spec for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ const url = '/entity/default/component/test/badge/test-badge?format=json';
+ const response = await request(app).get(url);
+
+ expect(response.status).toEqual(404);
+ });
+ });
+
+ describe('GET /entity/:namespace/:kind/:name/obfuscated', () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ catalog.getEntities.mockResolvedValueOnce({ items: entities });
+
+ it('returns obfuscated 401 if no auth', async () => {
+ const obfuscatedEntity = await request(app).get(
+ '/entity/default/component/test/obfuscated',
+ );
+ expect(obfuscatedEntity.status).toEqual(401);
+ });
+
+ it('returns obfuscated entity and badges', async () => {
+ const obfuscatedEntity = await request(app)
+ .get('/entity/default/component/test/obfuscated')
+ .set('Authorization', 'Bearer fakeToken');
+ expect(obfuscatedEntity.status).toEqual(200);
+ expect(obfuscatedEntity.body.uuid).toMatch(
+ new RegExp(
+ '^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$',
+ ),
+ );
+
+ const uuid = obfuscatedEntity.body.uuid;
+ const url = `/entity/${uuid}/test-badge?format=json`;
+ let response = await request(app).get(url);
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(badge);
+
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ catalog.getEntities.mockResolvedValueOnce({ items: entities });
+
+ const image = '';
+ badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
+
+ response = await request(app).get(`/entity/${uuid}/test-badge`);
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(Buffer.from(image));
+
+ catalog.getEntities.mockResolvedValueOnce({ items: entities });
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+
+ badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ response = await request(app).get(`/entity/${uuid}/badge-specs`);
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual([badge]);
+
+ expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1);
+ expect(badgeBuilder.createBadgeJson).toHaveBeenCalledTimes(2);
+ });
+
+ describe('Errors', () => {
+ it('returns 404 for unknown entity uuid', async () => {
+ badgeStore.getBadgeFromUuid.mockResolvedValue(undefined);
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ catalog.getEntities.mockResolvedValueOnce({ items: entities });
+ badgeBuilder.getBadges.mockResolvedValueOnce([{ id: badge.id }]);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ async function testUrl(url: string) {
+ const response = await request(app).get(url);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({
+ error: {
+ message: expect.any(String),
+ name: 'NotFoundError',
+ },
+ request: {
+ method: 'GET',
+ url,
+ },
+ response: {
+ statusCode: 404,
+ },
+ });
+ }
+ await testUrl(
+ '/entity/3a5f91c1e66519be5394c37a8ba69cfsf3087b7c322c600e7497dc9d517353e5bed/badge-specs',
+ );
+ await testUrl(
+ '/entity/3a5f91c1e66519be5394c37a8ba69c3087b7csfsf322c600e7497dc9d517353e5bed/test-badge',
+ );
+ });
+ });
+ });
+});
diff --git a/plugins/badges-backend/src/service/router.test.ts b/plugins/badges-backend/src/service/router.test.ts
index 109b64231d..2ecae6b452 100644
--- a/plugins/badges-backend/src/service/router.test.ts
+++ b/plugins/badges-backend/src/service/router.test.ts
@@ -17,7 +17,9 @@
import express from 'express';
import request from 'supertest';
import {
+ getVoidLogger,
PluginEndpointDiscovery,
+ ServerTokenManager,
SingleHostDiscovery,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
@@ -25,10 +27,16 @@ import type { Entity } from '@backstage/catalog-model';
import { Config, ConfigReader } from '@backstage/config';
import { createRouter } from './router';
import { BadgeBuilder } from '../lib';
+import {
+ BackstageIdentityResponse,
+ IdentityApiGetIdentityRequest,
+} from '@backstage/plugin-auth-node';
+import { BadgesStore } from '../database/badgesStore';
describe('createRouter', () => {
let app: express.Express;
let badgeBuilder: jest.Mocked;
+
const catalog = {
addLocation: jest.fn(),
getEntities: jest.fn(),
@@ -42,12 +50,15 @@ describe('createRouter', () => {
getEntityFacets: jest.fn(),
validateEntity: jest.fn(),
};
+
+ const getIdentity = jest.fn();
+
let config: Config;
let discovery: PluginEndpointDiscovery;
const entity: Entity = {
apiVersion: 'v1',
- kind: 'service',
+ kind: 'component',
metadata: {
name: 'test',
},
@@ -63,6 +74,11 @@ describe('createRouter', () => {
markdown: '[]',
};
+ const badgeStore: jest.Mocked = {
+ getBadgeFromUuid: jest.fn(),
+ getBadgeUuid: jest.fn(),
+ };
+
beforeAll(async () => {
badgeBuilder = {
getBadges: jest.fn(),
@@ -73,15 +89,40 @@ describe('createRouter', () => {
backend: {
baseUrl: 'http://127.0.0.1',
listen: { port: 7007 },
+ database: {
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ },
},
});
- discovery = SingleHostDiscovery.fromConfig(config);
+ getIdentity.mockImplementation(
+ async ({
+ request: _request,
+ }: IdentityApiGetIdentityRequest): Promise<
+ BackstageIdentityResponse | undefined
+ > => {
+ return {
+ identity: {
+ userEntityRef: 'user:default/guest',
+ ownershipEntityRefs: [],
+ type: 'user',
+ },
+ token: 'token',
+ };
+ },
+ );
+
+ discovery = SingleHostDiscovery.fromConfig(config);
+ const tokenManager = ServerTokenManager.noop();
const router = await createRouter({
badgeBuilder,
catalog: catalog as Partial as CatalogApi,
config,
discovery,
+ tokenManager,
+ logger: getVoidLogger(),
+ identity: { getIdentity },
});
app = express().use(router);
});
@@ -90,12 +131,17 @@ describe('createRouter', () => {
jest.resetAllMocks();
});
- it('works', async () => {
+ it('works with badgeStore', async () => {
+ const tokenManager = ServerTokenManager.noop();
const router = await createRouter({
badgeBuilder,
catalog: catalog as Partial as CatalogApi,
config,
discovery,
+ tokenManager,
+ logger: getVoidLogger(),
+ identity: { getIdentity },
+ badgeStore: badgeStore,
});
expect(router).toBeDefined();
});
@@ -108,7 +154,7 @@ describe('createRouter', () => {
badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
const response = await request(app).get(
- '/entity/default/service/test/badge-specs',
+ '/entity/default/component/test/badge-specs',
);
expect(response.status).toEqual(200);
@@ -118,10 +164,10 @@ describe('createRouter', () => {
expect(catalog.getEntityByRef).toHaveBeenCalledWith(
{
namespace: 'default',
- kind: 'service',
+ kind: 'component',
name: 'test',
},
- { token: undefined },
+ { token: '' },
);
expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(1);
@@ -130,46 +176,7 @@ describe('createRouter', () => {
badgeInfo: { id: badge.id },
context: {
badgeUrl: expect.stringMatching(
- /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/,
- ),
- config,
- entity,
- },
- });
- });
- });
-
- describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => {
- it('returns badge for entity', async () => {
- catalog.getEntityByRef.mockResolvedValueOnce(entity);
-
- const image = '';
- badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
-
- const response = await request(app).get(
- '/entity/default/service/test/badge/test-badge',
- );
-
- expect(response.status).toEqual(200);
- expect(response.body).toEqual(Buffer.from(image));
-
- expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
- expect(catalog.getEntityByRef).toHaveBeenCalledWith(
- {
- namespace: 'default',
- kind: 'service',
- name: 'test',
- },
- { token: undefined },
- );
-
- expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0);
- expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1);
- expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({
- badgeInfo: { id: badge.id },
- context: {
- badgeUrl: expect.stringMatching(
- /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/service\/test\/badge\/test-badge/,
+ /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/component\/test\/badge\/test-badge/,
),
config,
entity,
@@ -177,27 +184,90 @@ describe('createRouter', () => {
});
});
- it('returns badge spec for entity', async () => {
- catalog.getEntityByRef.mockResolvedValueOnce(entity);
- badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+ describe('GET /entity/:namespace/:kind/:name/badge/test-badge', () => {
+ it('returns badge for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
- const url = '/entity/default/service/test/badge/test-badge?format=json';
- const response = await request(app).get(url);
+ const image = '';
+ badgeBuilder.createBadgeSvg.mockResolvedValueOnce(image);
- expect(response.status).toEqual(200);
- expect(response.body).toEqual(badge);
+ const response = await request(app).get(
+ '/entity/default/component/test/badge/test-badge',
+ );
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(Buffer.from(image));
+
+ expect(catalog.getEntityByRef).toHaveBeenCalledTimes(1);
+ expect(catalog.getEntityByRef).toHaveBeenCalledWith(
+ {
+ namespace: 'default',
+ kind: 'component',
+ name: 'test',
+ },
+ { token: '' },
+ );
+
+ expect(badgeBuilder.getBadges).toHaveBeenCalledTimes(0);
+ expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledTimes(1);
+ expect(badgeBuilder.createBadgeSvg).toHaveBeenCalledWith({
+ badgeInfo: { id: badge.id },
+ context: {
+ badgeUrl: expect.stringMatching(
+ /http:\/\/127.0.0.1\/api\/badges\/entity\/default\/component\/test\/badge\/test-badge/,
+ ),
+ config,
+ entity,
+ },
+ });
+ });
+
+ it('returns badge spec for entity', async () => {
+ catalog.getEntityByRef.mockResolvedValueOnce(entity);
+ badgeBuilder.createBadgeJson.mockResolvedValueOnce(badge);
+
+ const url =
+ '/entity/default/component/test/badge/test-badge?format=json';
+ const response = await request(app).get(url);
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual(badge);
+ });
});
- });
- describe('Errors', () => {
- it('returns 404 for unknown entities', async () => {
+ describe('Errors', () => {
+ it('returns 404 for unknown entities', async () => {
+ catalog.getEntityByRef.mockResolvedValue(undefined);
+ async function testUrl(url: string) {
+ const response = await request(app).get(url);
+ expect(response.status).toEqual(404);
+ expect(response.body).toEqual({
+ error: {
+ message: 'No component entity in default named "missing"',
+ name: 'NotFoundError',
+ },
+ request: {
+ method: 'GET',
+ url,
+ },
+ response: {
+ statusCode: 404,
+ },
+ });
+ }
+ await testUrl('/entity/default/component/missing/badge-specs');
+ await testUrl('/entity/default/component/missing/badge/test-badge');
+ });
+ });
+
+ it('returns 404 for uuid entities', async () => {
catalog.getEntityByRef.mockResolvedValue(undefined);
async function testUrl(url: string) {
const response = await request(app).get(url);
expect(response.status).toEqual(404);
expect(response.body).toEqual({
error: {
- message: 'No service entity in default named "missing"',
+ message: 'No component entity in default named "missing"',
name: 'NotFoundError',
},
request: {
@@ -209,8 +279,8 @@ describe('createRouter', () => {
},
});
}
- await testUrl('/entity/default/service/missing/badge-specs');
- await testUrl('/entity/default/service/missing/badge/test-badge');
+ await testUrl('/entity/default/component/missing/badge-specs');
+ await testUrl('/entity/default/component/missing/badge/test-badge');
});
});
});
diff --git a/plugins/badges-backend/src/service/router.ts b/plugins/badges-backend/src/service/router.ts
index e5caa5cd2a..308865b73a 100644
--- a/plugins/badges-backend/src/service/router.ts
+++ b/plugins/badges-backend/src/service/router.ts
@@ -17,14 +17,21 @@
import express from 'express';
import Router from 'express-promise-router';
import {
+ DatabaseManager,
errorHandler,
PluginEndpointDiscovery,
+ TokenManager,
} from '@backstage/backend-common';
import { CatalogApi, CatalogClient } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
-import { NotFoundError } from '@backstage/errors';
+import { AuthenticationError, NotFoundError } from '@backstage/errors';
import { BadgeBuilder, DefaultBadgeBuilder } from '../lib/BadgeBuilder';
import { BadgeContext, BadgeFactories } from '../types';
+import { isNil } from 'lodash';
+import { Logger } from 'winston';
+import { IdentityApi } from '@backstage/plugin-auth-node';
+import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
+import { BadgesStore, DatabaseBadgesStore } from '../database/badgesStore';
/** @public */
export interface RouterOptions {
@@ -33,6 +40,10 @@ export interface RouterOptions {
catalog?: CatalogApi;
config: Config;
discovery: PluginEndpointDiscovery;
+ tokenManager: TokenManager;
+ logger: Logger;
+ identity: IdentityApi;
+ badgeStore?: BadgesStore;
}
/** @public */
@@ -46,13 +57,214 @@ export async function createRouter(
new DefaultBadgeBuilder(options.badgeFactories || {});
const router = Router();
+ const { config, logger, tokenManager, discovery, identity } = options;
+ const baseUrl = await discovery.getExternalBaseUrl('badges');
+
+ if (config.getOptionalBoolean('app.badges.obfuscate')) {
+ return obfuscatedRoute(
+ router,
+ catalog,
+ badgeBuilder,
+ tokenManager,
+ logger,
+ options,
+ config,
+ identity,
+ baseUrl,
+ );
+ }
+ return nonObfuscatedRoute(
+ router,
+ catalog,
+ badgeBuilder,
+ tokenManager,
+ config,
+ baseUrl,
+ );
+}
+
+async function obfuscatedRoute(
+ router: express.Router,
+ catalog: CatalogApi,
+ badgeBuilder: BadgeBuilder,
+ tokenManager: TokenManager,
+ logger: Logger,
+ options: RouterOptions,
+ config: Config,
+ identity: IdentityApi,
+ baseUrl: string,
+) {
+ logger.info('Badges obfuscation is enabled');
+
+ const store = options.badgeStore
+ ? options.badgeStore
+ : await DatabaseBadgesStore.create({
+ database: await DatabaseManager.fromConfig(config).forPlugin('badges'),
+ });
+
+ router.get('/entity/:entityUuid/badge-specs', async (req, res) => {
+ const { entityUuid } = req.params;
+
+ // Retrieve the badge info from the database
+ const badgeInfos = await store.getBadgeFromUuid(entityUuid);
+
+ if (isNil(badgeInfos)) {
+ throw new NotFoundError(`No badge found for entity uuid "${entityUuid}"`);
+ }
+
+ // If a mapping is found, map name, namespace and kind
+ const name = badgeInfos.name;
+ const namespace = badgeInfos.namespace;
+ const kind = badgeInfos.kind;
+ const token = await tokenManager.getToken();
+
+ // Query the catalog with the name, namespace, kind to get the entity informations
+ const entity = await catalog.getEntityByRef(
+ {
+ namespace,
+ kind,
+ name,
+ },
+ token,
+ );
+ if (isNil(entity)) {
+ throw new NotFoundError(
+ `No ${kind} entity in ${namespace} named "${name}"`,
+ );
+ }
+
+ // Create the badge specs
+ const specs = [];
+ for (const badgeInfo of await badgeBuilder.getBadges()) {
+ const context: BadgeContext = {
+ badgeUrl: `${baseUrl}/entity/${entityUuid}/${badgeInfo.id}`,
+ config: config,
+ entity,
+ };
+
+ const badge = await badgeBuilder.createBadgeJson({
+ badgeInfo,
+ context,
+ });
+ specs.push(badge);
+ }
+
+ res.status(200).json(specs);
+ });
+
+ router.get('/entity/:entityUuid/:badgeId', async (req, res) => {
+ const { entityUuid, badgeId } = req.params;
+
+ // Retrieve the badge info from the database
+ const badgeInfo = await store.getBadgeFromUuid(entityUuid);
+
+ if (isNil(badgeInfo)) {
+ throw new NotFoundError(`No badge found for entity uuid "${entityUuid}"`);
+ }
+
+ // If a mapping is found, map name, namespace and kind
+ const name = badgeInfo.name;
+ const namespace = badgeInfo.namespace;
+ const kind = badgeInfo.kind;
+ const token = await tokenManager.getToken();
+ const entity = await catalog.getEntityByRef(
+ {
+ namespace,
+ kind,
+ name,
+ },
+ token,
+ );
+ if (isNil(entity)) {
+ throw new NotFoundError(
+ `No ${kind} entity in ${namespace} named "${name}"`,
+ res.sendStatus(404),
+ );
+ }
+
+ let format =
+ req.accepts(['image/svg+xml', 'application/json']) || 'image/svg+xml';
+ if (req.query.format === 'json') {
+ format = 'application/json';
+ }
+
+ const badgeOptions = {
+ badgeInfo: { id: badgeId },
+ context: {
+ badgeUrl: `${baseUrl}/entity/${entityUuid}/${badgeId}`,
+ config: config,
+ entity,
+ },
+ };
+
+ let data: string;
+ if (format === 'application/json') {
+ data = JSON.stringify(
+ await badgeBuilder.createBadgeJson(badgeOptions),
+ null,
+ 2,
+ );
+ } else {
+ data = await badgeBuilder.createBadgeSvg(badgeOptions);
+ }
+
+ res.setHeader('Content-Type', format);
+ res.status(200).send(data);
+ });
+
+ router.get(
+ '/entity/:namespace/:kind/:name/obfuscated',
+ function authenticate(req, _res, next) {
+ const token =
+ getBearerTokenFromAuthorizationHeader(req.headers.authorization) ||
+ (req.cookies?.token as string | undefined);
+
+ if (!token) {
+ throw new AuthenticationError('Unauthorized');
+ }
+
+ try {
+ req.user = identity.getIdentity({ request: req });
+ next();
+ } catch (error) {
+ tokenManager.authenticate(token.toString());
+ next(error);
+ }
+ },
+ async (req, res) => {
+ const { namespace, kind, name } = req.params;
+ const storedEntityUuid: { uuid: string } | undefined =
+ await store.getBadgeUuid(name, namespace, kind);
+
+ if (isNil(storedEntityUuid)) {
+ throw new NotFoundError(
+ `No uuid found for entity "${namespace}/${kind}/${name}"`,
+ );
+ }
+
+ return res.status(200).json(storedEntityUuid);
+ },
+ );
+
+ router.use(errorHandler());
+
+ return router;
+}
+
+async function nonObfuscatedRoute(
+ router: express.Router,
+ catalog: CatalogApi,
+ badgeBuilder: BadgeBuilder,
+ tokenManager: TokenManager,
+ config: Config,
+ baseUrl: string,
+) {
router.get('/entity/:namespace/:kind/:name/badge-specs', async (req, res) => {
+ const token = await tokenManager.getToken();
const { namespace, kind, name } = req.params;
const entity = await catalog.getEntityByRef(
{ namespace, kind, name },
- {
- token: getBearerToken(req.headers.authorization),
- },
+ token,
);
if (!entity) {
throw new NotFoundError(
@@ -62,19 +274,17 @@ export async function createRouter(
const specs = [];
for (const badgeInfo of await badgeBuilder.getBadges()) {
+ const badgeId = badgeInfo.id;
const context: BadgeContext = {
- badgeUrl: await getBadgeUrl(
- namespace,
- kind,
- name,
- badgeInfo.id,
- options,
- ),
- config: options.config,
+ badgeUrl: `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`,
+ config: config,
entity,
};
- const badge = await badgeBuilder.createBadgeJson({ badgeInfo, context });
+ const badge = await badgeBuilder.createBadgeJson({
+ badgeInfo,
+ context,
+ });
specs.push(badge);
}
@@ -85,11 +295,10 @@ export async function createRouter(
'/entity/:namespace/:kind/:name/badge/:badgeId',
async (req, res) => {
const { namespace, kind, name, badgeId } = req.params;
+ const token = await tokenManager.getToken();
const entity = await catalog.getEntityByRef(
{ namespace, kind, name },
- {
- token: getBearerToken(req.headers.authorization),
- },
+ token,
);
if (!entity) {
throw new NotFoundError(
@@ -106,8 +315,8 @@ export async function createRouter(
const badgeOptions = {
badgeInfo: { id: badgeId },
context: {
- badgeUrl: await getBadgeUrl(namespace, kind, name, badgeId, options),
- config: options.config,
+ badgeUrl: `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`,
+ config: config,
entity,
},
};
@@ -132,18 +341,3 @@ export async function createRouter(
return router;
}
-
-async function getBadgeUrl(
- namespace: string,
- kind: string,
- name: string,
- badgeId: string,
- options: RouterOptions,
-): Promise {
- const baseUrl = await options.discovery.getExternalBaseUrl('badges');
- return `${baseUrl}/entity/${namespace}/${kind}/${name}/badge/${badgeId}`;
-}
-
-function getBearerToken(header?: string): string | undefined {
- return header?.match(/Bearer\s+(\S+)/i)?.[1];
-}
diff --git a/plugins/badges-backend/src/service/standaloneServer.ts b/plugins/badges-backend/src/service/standaloneServer.ts
index 0800585634..b07e1fa3b1 100644
--- a/plugins/badges-backend/src/service/standaloneServer.ts
+++ b/plugins/badges-backend/src/service/standaloneServer.ts
@@ -19,9 +19,14 @@ import { Logger } from 'winston';
import {
createServiceBuilder,
loadBackendConfig,
+ ServerTokenManager,
SingleHostDiscovery,
+ useHotMemoize,
} from '@backstage/backend-common';
import { createRouter } from './router';
+import { IdentityApi } from '@backstage/plugin-auth-node';
+import { DatabaseBadgesStore } from '../database/badgesStore';
+import Knex from 'knex';
export interface ServerOptions {
port: number;
@@ -35,10 +40,40 @@ export async function startStandaloneServer(
const logger = options.logger.child({ service: 'badges-backend' });
const config = await loadBackendConfig({ logger, argv: process.argv });
const discovery = SingleHostDiscovery.fromConfig(config);
+ const database = useHotMemoize(module, () => {
+ return Knex({
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ useNullAsDefault: true,
+ });
+ });
logger.debug('Creating application...');
- const router = await createRouter({ config, discovery });
+ const tokenManager = ServerTokenManager.noop();
+ const identity: IdentityApi = {
+ async getIdentity({ request }) {
+ const token = request.headers.authorization?.split(' ')[1];
+ return {
+ identity: {
+ type: 'user',
+ ownershipEntityRefs: [],
+ userEntityRef: token || 'user:default/john_doe',
+ },
+ token: token || 'no-token',
+ };
+ },
+ };
+ const router = await createRouter({
+ config,
+ discovery,
+ tokenManager,
+ logger,
+ identity,
+ badgeStore: await DatabaseBadgesStore.create({
+ database: { getClient: async () => database },
+ }),
+ });
let service = createServiceBuilder(module)
.setPort(options.port)
diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md
index b4a0e6aec6..656745c506 100644
--- a/plugins/badges/CHANGELOG.md
+++ b/plugins/badges/CHANGELOG.md
@@ -1,5 +1,51 @@
# @backstage/plugin-badges
+## 0.2.43-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+
+## 0.2.42
+
+### Patch Changes
+
+- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+ Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+ **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+
+## 0.2.42-next.2
+
+### Patch Changes
+
+- a0108c49774: Fixing badges-backend plugin to get a token from the TokenManager instead of parsing the request header. Hence, it's now possible to disable the authMiddleware for the badges-backend plugin to expose publicly the badges.
+
+ Implementing an obfuscation feature to protect an open badges endpoint from being enumerated. The feature is disabled by default and the change is compatible with the previous version.
+
+ **BREAKING**: `createRouter` now require that `tokenManager`, `logger`, and `identityApi`, are passed in as options.
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.42-next.1
### Patch Changes
diff --git a/plugins/badges/README.md b/plugins/badges/README.md
index 65ace6226e..4b8ff38d47 100644
--- a/plugins/badges/README.md
+++ b/plugins/badges/README.md
@@ -18,6 +18,51 @@ This will popup a badges dialog showing all available badges for that entity lik

+## Badge obfuscation
+
+The badges plugin supports obfuscating the badge URL to prevent it from being enumerated if the badges are used in a public context (like in Github repositories).
+
+To enable obfuscation, set the `obfuscate` option to `true` in the `app.badges` section of your `app-config.yaml`:
+
+```yaml
+app:
+ badges:
+ obfuscate: true
+```
+
+Please note that if you have already set badges in your repositories and you activate the obfuscation you will need to update the badges in your repositories to use the new obfuscated URLs.
+
+Please note that the backend part needs to be configured to support obfuscation. See the [backend plugin documentation](../badges-backend/README.md) for more details.
+
+Also, you need to allow your frontend to access the configuration see :
+
+Example implementation would be in : `packages/app/src/config.d.ts`
+
+```typescript
+export interface Config {
+ app: {
+ ... some code
+ badges: {
+ /**
+ * badges obfuscate
+ * @visibility frontend
+ */
+ obfuscate?: string;
+ };
+ };
+}
+```
+
+then include in the `packages/app/package.json` :
+
+```json
+"files": [
+ "dist",
+ "config.d.ts"
+ ],
+"configSchema": "config.d.ts",
+```
+
## Sample Badges
Here are some samples of badges for the `artists-lookup` service in the Demo Backstage site:
diff --git a/plugins/badges/package.json b/plugins/badges/package.json
index 0e736d1cbc..2c3d72f234 100644
--- a/plugins/badges/package.json
+++ b/plugins/badges/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-badges",
"description": "A Backstage plugin that generates README badges for your entities",
- "version": "0.2.42-next.1",
+ "version": "0.2.43-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -50,14 +50,9 @@
"@backstage/core-app-api": "workspace:^",
"@backstage/dev-utils": "workspace:^",
"@backstage/test-utils": "workspace:^",
- "@testing-library/dom": "^8.0.0",
- "@testing-library/jest-dom": "^5.10.1",
- "@testing-library/react": "^12.1.3",
- "@testing-library/user-event": "^14.0.0",
+ "@testing-library/jest-dom": "^5.16.5",
"@types/node": "^16.11.26",
- "@types/react": "^16.13.1 || ^17.0.0",
- "cross-fetch": "^3.1.5",
- "msw": "^1.0.0"
+ "cross-fetch": "^3.1.5"
},
"files": [
"dist"
diff --git a/plugins/badges/src/api/BadgesClient.ts b/plugins/badges/src/api/BadgesClient.ts
index 9a89806d56..9a08db882a 100644
--- a/plugins/badges/src/api/BadgesClient.ts
+++ b/plugins/badges/src/api/BadgesClient.ts
@@ -18,31 +18,55 @@ import { generatePath } from 'react-router-dom';
import { ResponseError } from '@backstage/errors';
import { Entity, DEFAULT_NAMESPACE } from '@backstage/catalog-model';
import { BadgesApi, BadgeSpec } from './types';
-import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
+import { ConfigApi, DiscoveryApi, FetchApi } from '@backstage/core-plugin-api';
export class BadgesClient implements BadgesApi {
private readonly discoveryApi: DiscoveryApi;
- private readonly identityApi: IdentityApi;
+ private readonly fetchApi: FetchApi;
+ private readonly configApi: ConfigApi;
constructor(options: {
discoveryApi: DiscoveryApi;
- identityApi: IdentityApi;
+ fetchApi: FetchApi;
+ configApi: ConfigApi;
}) {
this.discoveryApi = options.discoveryApi;
- this.identityApi = options.identityApi;
+ this.fetchApi = options.fetchApi;
+ this.configApi = options.configApi;
+ }
+
+ static fromConfig(options: {
+ fetchApi: FetchApi;
+ discoveryApi: DiscoveryApi;
+ configApi: ConfigApi;
+ }) {
+ return new BadgesClient(options);
}
public async getEntityBadgeSpecs(entity: Entity): Promise {
- const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
- const { token } = await this.identityApi.getCredentials();
- const response = await fetch(entityBadgeSpecsUrl, {
- headers: token
- ? {
- Authorization: `Bearer ${token}`,
- }
- : undefined,
- });
+ // Check if obfuscation is enabled in the configuration
+ const obfuscate = this.configApi.getOptionalBoolean('app.badges.obfuscate');
+ if (obfuscate) {
+ const entityUuidUrl = await this.getEntityUuidUrl(entity);
+ const entityUuid = await this.getEntityUuid(entityUuidUrl).then(data => {
+ return data.uuid;
+ });
+ const entityUuidBadgeSpecsUrl = await this.getEntityUuidBadgeSpecsUrl(
+ entityUuid,
+ );
+
+ const response = await this.fetchApi.fetch(entityUuidBadgeSpecsUrl);
+ if (!response.ok) {
+ throw await ResponseError.fromResponse(response);
+ }
+
+ return await response.json();
+ }
+
+ // If obfuscation is disabled, get the badge specs directly as the previous implementation
+ const entityBadgeSpecsUrl = await this.getEntityBadgeSpecsUrl(entity);
+ const response = await this.fetchApi.fetch(entityBadgeSpecsUrl);
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
@@ -50,14 +74,39 @@ export class BadgesClient implements BadgesApi {
return await response.json();
}
+ private async getEntityUuidUrl(entity: Entity): Promise {
+ const routeParams = this.getEntityRouteParams(entity);
+ const path = generatePath(`:namespace/:kind/:name`, routeParams);
+ const baseUrl = await this.discoveryApi.getBaseUrl('badges');
+ const obfuscatedEntityUrl = `${baseUrl}/entity/${path}/obfuscated`;
+
+ return obfuscatedEntityUrl;
+ }
+
+ private async getEntityUuid(entityUuidUrl: string): Promise {
+ const responseEntityUuid = await this.fetchApi.fetch(entityUuidUrl);
+
+ if (!responseEntityUuid.ok) {
+ throw await ResponseError.fromResponse(responseEntityUuid);
+ }
+ return await responseEntityUuid.json();
+ }
+
+ private async getEntityUuidBadgeSpecsUrl(entityUuid: {
+ uuid: string;
+ }): Promise {
+ const baseUrl = await this.discoveryApi.getBaseUrl('badges');
+ return `${baseUrl}/entity/${entityUuid}/badge-specs`;
+ }
+
private async getEntityBadgeSpecsUrl(entity: Entity): Promise {
const routeParams = this.getEntityRouteParams(entity);
const path = generatePath(`:namespace/:kind/:name`, routeParams);
- return `${await this.discoveryApi.getBaseUrl(
- 'badges',
- )}/entity/${path}/badge-specs`;
+ const baseUrl = await this.discoveryApi.getBaseUrl('badges');
+ return `${baseUrl}/entity/${path}/badge-specs`;
}
+ // This function is used to generate the route parameters using the entity kind, namespace and name
private getEntityRouteParams(entity: Entity) {
return {
kind: entity.kind.toLocaleLowerCase('en-US'),
diff --git a/plugins/badges/src/plugin.ts b/plugins/badges/src/plugin.ts
index 7ef3025458..771e3ed893 100644
--- a/plugins/badges/src/plugin.ts
+++ b/plugins/badges/src/plugin.ts
@@ -15,11 +15,12 @@
*/
import { badgesApiRef, BadgesClient } from './api';
import {
+ configApiRef,
createApiFactory,
createComponentExtension,
createPlugin,
+ fetchApiRef,
discoveryApiRef,
- identityApiRef,
} from '@backstage/core-plugin-api';
/** @public */
@@ -28,9 +29,17 @@ export const badgesPlugin = createPlugin({
apis: [
createApiFactory({
api: badgesApiRef,
- deps: { discoveryApi: discoveryApiRef, identityApi: identityApiRef },
- factory: ({ discoveryApi, identityApi }) =>
- new BadgesClient({ discoveryApi, identityApi }),
+ deps: {
+ fetchApi: fetchApiRef,
+ discoveryApi: discoveryApiRef,
+ configApi: configApiRef,
+ },
+ factory: ({ fetchApi, discoveryApi, configApi }) =>
+ new BadgesClient({
+ fetchApi,
+ discoveryApi,
+ configApi,
+ }),
}),
],
});
diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md
index 11d820a334..0861db530f 100644
--- a/plugins/bazaar-backend/CHANGELOG.md
+++ b/plugins/bazaar-backend/CHANGELOG.md
@@ -1,5 +1,27 @@
# @backstage/plugin-bazaar-backend
+## 0.2.9-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-auth-node@0.2.15-next.0
+
+## 0.2.8
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 0.2.8-next.1
### Patch Changes
diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json
index 60e549655e..be621f5d5f 100644
--- a/plugins/bazaar-backend/package.json
+++ b/plugins/bazaar-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-bazaar-backend",
- "version": "0.2.8-next.1",
+ "version": "0.2.9-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/bazaar/CHANGELOG.md b/plugins/bazaar/CHANGELOG.md
index 298c684d3e..97fc659f03 100644
--- a/plugins/bazaar/CHANGELOG.md
+++ b/plugins/bazaar/CHANGELOG.md
@@ -1,5 +1,49 @@
# @backstage/plugin-bazaar
+## 0.2.9-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/cli@0.22.8-next.0
+ - @backstage/plugin-catalog@1.11.1-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+
+## 0.2.8
+
+### Patch Changes
+
+- 900880ab7c3: Fixed `validateDOMNesting` warnings
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/cli@0.22.7
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/plugin-catalog@1.11.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+
+## 0.2.8-next.2
+
+### Patch Changes
+
+- 900880ab7c3: Fixed `validateDOMNesting` warnings
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/cli@0.22.7-next.0
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.8-next.1
### Patch Changes
diff --git a/plugins/bazaar/dev/__fixtures__/get-projects-response.json b/plugins/bazaar/dev/__fixtures__/get-projects-response.json
new file mode 100644
index 0000000000..7aee019a6e
--- /dev/null
+++ b/plugins/bazaar/dev/__fixtures__/get-projects-response.json
@@ -0,0 +1,35 @@
+{
+ "status": "ok",
+ "data": [
+ {
+ "id": 10,
+ "entity_ref": null,
+ "title": "Lorem ipsum 1",
+ "description": "Lorem ipsum dolor sit amet, consectetur adipiscing elit.",
+ "status": "proposed",
+ "updated_at": "2023-03-10T12:41:41.941Z",
+ "community": "",
+ "size": "medium",
+ "start_date": null,
+ "end_date": null,
+ "responsible": "Responsible Adult 1",
+ "docs": null,
+ "members_count": "3"
+ },
+ {
+ "id": 11,
+ "entity_ref": null,
+ "title": "Lorem ipsum 2",
+ "description": "sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud",
+ "status": "proposed",
+ "updated_at": "2023-03-09T12:41:41.941Z",
+ "community": "",
+ "size": "medium",
+ "start_date": null,
+ "end_date": null,
+ "responsible": "Responsible Adult 2",
+ "docs": null,
+ "members_count": "5"
+ }
+ ]
+}
diff --git a/plugins/bazaar/dev/index.tsx b/plugins/bazaar/dev/index.tsx
index a4509bc1fc..f2c40df8e1 100644
--- a/plugins/bazaar/dev/index.tsx
+++ b/plugins/bazaar/dev/index.tsx
@@ -16,10 +16,30 @@
import React from 'react';
import { createDevApp } from '@backstage/dev-utils';
+import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { bazaarPlugin, BazaarPage } from '../src/plugin';
+import { bazaarApiRef } from '../src/api';
+
+import getProjectsData from './__fixtures__/get-projects-response.json';
createDevApp()
.registerPlugin(bazaarPlugin)
+ .registerApi({
+ api: bazaarApiRef,
+ deps: {},
+ factory: () =>
+ ({
+ getProjects: async () => getProjectsData,
+ } as any),
+ })
+ .registerApi({
+ api: catalogApiRef,
+ deps: {},
+ factory: () =>
+ ({
+ getEntities: () => ({}),
+ } as CatalogApi),
+ })
.addPage({
element: ,
title: 'Root Page',
diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json
index c61db47231..11f8dce0dc 100644
--- a/plugins/bazaar/package.json
+++ b/plugins/bazaar/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-bazaar",
- "version": "0.2.8-next.1",
+ "version": "0.2.9-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx
index 97b247e289..61d1dfebb9 100644
--- a/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx
+++ b/plugins/bazaar/src/components/ProjectCard/ProjectCard.tsx
@@ -94,11 +94,7 @@ export const ProjectCard = ({
setOpenCard(true)}>
- {title}
-
- }
+ title={title}
subtitle={`updated ${DateTime.fromISO(
new Date(updatedAt!).toISOString(),
).toRelative({
diff --git a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx
index 8f7160600a..39319b6353 100644
--- a/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx
+++ b/plugins/bazaar/src/components/ProjectPreview/ProjectPreview.tsx
@@ -15,9 +15,14 @@
*/
import React, { ChangeEvent, useState } from 'react';
-import { Content } from '@backstage/core-components';
import { ProjectCard } from '../ProjectCard/ProjectCard';
-import { makeStyles, Grid, TablePagination, GridSize } from '@material-ui/core';
+import {
+ Box,
+ makeStyles,
+ Grid,
+ TablePagination,
+ GridSize,
+} from '@material-ui/core';
import { BazaarProject } from '../../types';
import { Entity } from '@backstage/catalog-model';
@@ -80,7 +85,7 @@ export const ProjectPreview = ({
}
return (
-
+
{bazaarProjects
.slice((page - 1) * rows, rows * page)
@@ -101,6 +106,7 @@ export const ProjectPreview = ({
{useTablePagination && (
)}
-
+
);
};
diff --git a/plugins/bitbucket-cloud-common/CHANGELOG.md b/plugins/bitbucket-cloud-common/CHANGELOG.md
index c3c31ed392..04b2cc787b 100644
--- a/plugins/bitbucket-cloud-common/CHANGELOG.md
+++ b/plugins/bitbucket-cloud-common/CHANGELOG.md
@@ -1,5 +1,12 @@
# @backstage/plugin-bitbucket-cloud-common
+## 0.2.6
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/integration@1.4.5
+
## 0.2.6-next.0
### Patch Changes
diff --git a/plugins/bitbucket-cloud-common/package.json b/plugins/bitbucket-cloud-common/package.json
index 7bf2c5791e..a63b9588fb 100644
--- a/plugins/bitbucket-cloud-common/package.json
+++ b/plugins/bitbucket-cloud-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-bitbucket-cloud-common",
"description": "Common functionalities for bitbucket-cloud plugins",
- "version": "0.2.6-next.0",
+ "version": "0.2.6",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md
index 1ef42a6bbe..cb9f5b1a5e 100644
--- a/plugins/bitrise/CHANGELOG.md
+++ b/plugins/bitrise/CHANGELOG.md
@@ -1,5 +1,37 @@
# @backstage/plugin-bitrise
+## 0.1.46-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.1.45
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.45-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.45-next.1
### Patch Changes
diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json
index 1b3740a058..db4769d6f1 100644
--- a/plugins/bitrise/package.json
+++ b/plugins/bitrise/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-bitrise",
"description": "A Backstage plugin that integrates towards Bitrise",
- "version": "0.1.45-next.1",
+ "version": "0.1.46-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md
index f7ffc5e63c..e4b6e3d63a 100644
--- a/plugins/catalog-backend-module-aws/CHANGELOG.md
+++ b/plugins/catalog-backend-module-aws/CHANGELOG.md
@@ -1,5 +1,53 @@
# @backstage/plugin-catalog-backend-module-aws
+## 0.2.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/integration-aws-node@0.1.3
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-kubernetes-common@0.6.3
+
+## 0.2.0
+
+### Minor Changes
+
+- 1a3b5f1e390: **BREAKING**: AwsOrganizationCloudAccountProcessor.fromConfig now returns a promise instead of the instance directly.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/integration-aws-node@0.1.3
+ - @backstage/plugin-kubernetes-common@0.6.3
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 0.1.19-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-kubernetes-common@0.6.3-next.0
+ - @backstage/config@1.0.7
+
## 0.1.19-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-aws/api-report.md b/plugins/catalog-backend-module-aws/api-report.md
index 491dbf497e..83b072061b 100644
--- a/plugins/catalog-backend-module-aws/api-report.md
+++ b/plugins/catalog-backend-module-aws/api-report.md
@@ -3,11 +3,12 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+import { AwsCredentialIdentity } from '@aws-sdk/types';
+import { AwsCredentialsManager } from '@backstage/integration-aws-node';
import { CatalogProcessor } from '@backstage/plugin-catalog-node';
import { CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
import { CatalogProcessorParser } from '@backstage/plugin-catalog-node';
import { Config } from '@backstage/config';
-import { Credentials } from 'aws-sdk';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
@@ -19,11 +20,16 @@ import { UrlReader } from '@backstage/backend-common';
// @public
export type AWSCredentialFactory = (
awsAccountId: string,
-) => Promise;
+) => Promise;
// @public
export class AwsEKSClusterProcessor implements CatalogProcessor {
- constructor(options: { credentialsFactory?: AWSCredentialFactory });
+ constructor(options: {
+ credentialsFactory?: AWSCredentialFactory;
+ credentialsManager?: AwsCredentialsManager;
+ });
+ // (undocumented)
+ static fromConfig(configRoot: Config): AwsEKSClusterProcessor;
// (undocumented)
getProcessorName(): string;
// (undocumented)
@@ -44,7 +50,7 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
options: {
logger: Logger;
},
- ): AwsOrganizationCloudAccountProcessor;
+ ): Promise;
// (undocumented)
getProcessorName(): string;
// (undocumented)
diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json
index 73bd5123d2..07309f81cc 100644
--- a/plugins/catalog-backend-module-aws/package.json
+++ b/plugins/catalog-backend-module-aws/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-aws",
"description": "A Backstage catalog backend module that helps integrate towards AWS",
- "version": "0.1.19-next.1",
+ "version": "0.2.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -45,7 +45,12 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
- "@aws-sdk/client-s3": "^3.208.0",
+ "@aws-sdk/client-eks": "^3.310.0",
+ "@aws-sdk/client-organizations": "^3.310.0",
+ "@aws-sdk/client-s3": "^3.310.0",
+ "@aws-sdk/credential-providers": "^3.310.0",
+ "@aws-sdk/middleware-endpoint": "^3.310.0",
+ "@aws-sdk/types": "^3.310.0",
"@backstage/backend-common": "workspace:^",
"@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
@@ -53,23 +58,23 @@
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/integration": "workspace:^",
+ "@backstage/integration-aws-node": "workspace:^",
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/plugin-kubernetes-common": "workspace:^",
"@backstage/types": "workspace:^",
- "aws-sdk": "^2.840.0",
"lodash": "^4.17.21",
"p-limit": "^3.0.2",
"uuid": "^8.0.0",
"winston": "^3.2.1"
},
"devDependencies": {
- "@aws-sdk/util-stream-node": "^3.208.0",
+ "@aws-sdk/util-stream-node": "^3.310.0",
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/lodash": "^4.14.151",
"aws-sdk-client-mock": "^2.0.0",
- "aws-sdk-mock": "^5.2.1",
+ "aws-sdk-client-mock-jest": "^2.0.0",
"luxon": "^3.0.0",
"yaml": "^2.0.0"
},
diff --git a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts b/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts
deleted file mode 100644
index fded78aed4..0000000000
--- a/plugins/catalog-backend-module-aws/src/credentials/AwsCredentials.ts
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Copyright 2022 The Backstage Authors
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-import aws, { Credentials } from 'aws-sdk';
-import { CredentialsOptions } from 'aws-sdk/lib/credentials';
-
-export class AwsCredentials {
- /**
- * If accessKeyId and secretAccessKey are missing, the DefaultAWSCredentialsProviderChain will be used:
- * https://docs.aws.amazon.com/AWSJavaSDK/latest/javadoc/com/amazonaws/auth/DefaultAWSCredentialsProviderChain.html
- */
- static create(
- config: {
- accessKeyId?: string;
- secretAccessKey?: string;
- roleArn?: string;
- externalId?: string;
- },
- roleSessionName: string,
- ): Credentials | CredentialsOptions | undefined {
- if (!config) {
- return undefined;
- }
-
- const accessKeyId = config.accessKeyId;
- const secretAccessKey = config.secretAccessKey;
- let explicitCredentials: Credentials | undefined;
-
- if (accessKeyId && secretAccessKey) {
- explicitCredentials = new Credentials({
- accessKeyId,
- secretAccessKey,
- });
- }
-
- const roleArn = config.roleArn;
- if (roleArn) {
- return new aws.ChainableTemporaryCredentials({
- masterCredentials: explicitCredentials,
- params: {
- RoleArn: roleArn,
- RoleSessionName: roleSessionName,
- ExternalId: config.externalId,
- },
- });
- }
-
- return explicitCredentials;
- }
-}
diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts
index ecf3b1a961..6cec6ed66d 100644
--- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts
+++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.test.ts
@@ -15,24 +15,28 @@
*/
import { AwsEKSClusterProcessor } from './AwsEKSClusterProcessor';
-import AWSMock from 'aws-sdk-mock';
-import aws from 'aws-sdk';
+import { mockClient } from 'aws-sdk-client-mock';
+import {
+ ListClustersResponse,
+ DescribeClusterResponse,
+ ListClustersCommand,
+ DescribeClusterCommand,
+ EKSClient,
+} from '@aws-sdk/client-eks';
describe('AwsEKSClusterProcessor', () => {
- AWSMock.setSDKInstance(aws);
-
describe('readLocation', () => {
const processor = new (AwsEKSClusterProcessor as any)({});
const location = { type: 'aws-eks', target: '957140518395/us-west-2' };
const emit = jest.fn();
it('generates cluster correctly', async () => {
- const clusters: aws.EKS.Types.ListClustersResponse = {
+ const clusters: ListClustersResponse = {
clusters: ['backstage-test'],
nextToken: undefined,
};
- const cluster: aws.EKS.Types.DescribeClusterResponse = {
+ const cluster: DescribeClusterResponse = {
cluster: {
name: 'backstage-test',
arn: 'arn:aws:1',
@@ -42,9 +46,10 @@ describe('AwsEKSClusterProcessor', () => {
},
},
};
+ const mock = mockClient(EKSClient);
- AWSMock.mock('EKS', 'listClusters', clusters);
- AWSMock.mock('EKS', 'describeCluster', cluster);
+ mock.on(ListClustersCommand).resolves(clusters);
+ mock.on(DescribeClusterCommand).resolves(cluster);
await processor.readLocation(location, false, emit);
diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts
index 05c85c7426..4a19b9eb7f 100644
--- a/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts
+++ b/plugins/catalog-backend-module-aws/src/processors/AwsEKSClusterProcessor.ts
@@ -24,8 +24,14 @@ import {
ANNOTATION_KUBERNETES_API_SERVER_CA,
ANNOTATION_KUBERNETES_AUTH_PROVIDER,
} from '@backstage/plugin-kubernetes-common';
-import { Credentials, EKS } from 'aws-sdk';
+import { EKS } from '@aws-sdk/client-eks';
import { AWSCredentialFactory } from '../types';
+import { AwsCredentialIdentity, Provider } from '@aws-sdk/types';
+import {
+ AwsCredentialsManager,
+ DefaultAwsCredentialsManager,
+} from '@backstage/integration-aws-node';
+import { Config } from '@backstage/config';
const ACCOUNTID_ANNOTATION: string = 'amazonaws.com/account-id';
const ARN_ANNOTATION: string = 'amazonaws.com/arn';
@@ -39,9 +45,22 @@ const ARN_ANNOTATION: string = 'amazonaws.com/arn';
*/
export class AwsEKSClusterProcessor implements CatalogProcessor {
private credentialsFactory?: AWSCredentialFactory;
+ private credentialsManager?: AwsCredentialsManager;
- constructor(options: { credentialsFactory?: AWSCredentialFactory }) {
+ static fromConfig(configRoot: Config): AwsEKSClusterProcessor {
+ const awsCredentaislManager =
+ DefaultAwsCredentialsManager.fromConfig(configRoot);
+ return new AwsEKSClusterProcessor({
+ credentialsManager: awsCredentaislManager,
+ });
+ }
+
+ constructor(options: {
+ credentialsFactory?: AWSCredentialFactory;
+ credentialsManager?: AwsCredentialsManager;
+ }) {
this.credentialsFactory = options.credentialsFactory;
+ this.credentialsManager = options.credentialsManager;
}
getProcessorName(): string {
@@ -73,20 +92,30 @@ export class AwsEKSClusterProcessor implements CatalogProcessor {
);
}
- let credentials: Credentials | undefined;
+ let credentials: AwsCredentialIdentity | undefined;
if (this.credentialsFactory) {
credentials = await this.credentialsFactory(accountId);
}
- const eksClient = new EKS({ credentials, region });
- const clusters = await eksClient.listClusters({}).promise();
+ let providerFunction: (() => Provider) | undefined;
+ if (this.credentialsManager) {
+ const credentialsProvider =
+ await this.credentialsManager.getCredentialProvider({ accountId });
+ providerFunction = () => credentialsProvider.sdkCredentialProvider;
+ }
+
+ const eksClient = new EKS({
+ credentials,
+ credentialDefaultProvider: providerFunction,
+ });
+ const clusters = await eksClient.listClusters({});
if (clusters.clusters === undefined) {
return true;
}
const results = clusters.clusters
- .map(cluster => eksClient.describeCluster({ name: cluster }).promise())
+ .map(cluster => eksClient.describeCluster({ name: cluster }))
.map(async describedClusterPromise => {
const describedCluster = await describedClusterPromise;
if (describedCluster.cluster) {
diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts
index b9b8ba7741..458a1f3531 100644
--- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts
+++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.test.ts
@@ -14,7 +14,13 @@
* limitations under the License.
*/
+import { mockClient } from 'aws-sdk-client-mock';
+import 'aws-sdk-client-mock-jest';
import { AwsOrganizationCloudAccountProcessor } from './AwsOrganizationCloudAccountProcessor';
+import {
+ ListAccountsCommand,
+ OrganizationsClient,
+} from '@aws-sdk/client-organizations';
describe('AwsOrganizationCloudAccountProcessor', () => {
describe('readLocation', () => {
@@ -23,26 +29,21 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
});
const location = { type: 'aws-cloud-accounts', target: '' };
const emit = jest.fn();
- const listAccounts = jest.fn();
+ const mock = mockClient(OrganizationsClient);
- processor.organizations.listAccounts = listAccounts;
- afterEach(() => jest.resetAllMocks());
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
it('generates component entities for accounts', async () => {
- listAccounts.mockImplementation(() => {
- return {
- async promise() {
- return {
- Accounts: [
- {
- Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
- Name: 'testaccount',
- },
- ],
- NextToken: undefined,
- };
+ mock.on(ListAccountsCommand).resolves({
+ Accounts: [
+ {
+ Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
+ Name: 'testaccount',
},
- };
+ ],
+ NextToken: undefined,
});
await processor.readLocation(location, false, emit);
expect(emit).toHaveBeenCalledWith({
@@ -74,24 +75,18 @@ describe('AwsOrganizationCloudAccountProcessor', () => {
type: 'aws-cloud-accounts',
target: 'o-1vl18kc5a3',
};
- listAccounts.mockImplementation(() => {
- return {
- async promise() {
- return {
- Accounts: [
- {
- Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
- Name: 'testaccount',
- },
- {
- Arn: 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395',
- Name: 'testaccount2',
- },
- ],
- NextToken: undefined,
- };
+ mock.on(ListAccountsCommand).resolves({
+ Accounts: [
+ {
+ Arn: 'arn:aws:organizations::192594491037:account/o-1vl18kc5a3/957140518395',
+ Name: 'testaccount',
},
- };
+ {
+ Arn: 'arn:aws:organizations::192594491037:account/o-zzzzzzzzz/957140518395',
+ Name: 'testaccount2',
+ },
+ ],
+ NextToken: undefined,
});
await processor.readLocation(locationTest, false, emit);
expect(emit).toHaveBeenCalledTimes(1);
diff --git a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts
index c0ba1e3e98..264fa7a50d 100644
--- a/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts
+++ b/plugins/catalog-backend-module-aws/src/processors/AwsOrganizationCloudAccountProcessor.ts
@@ -22,13 +22,17 @@ import {
processingResult,
} from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
-import AWS, { Credentials, Organizations } from 'aws-sdk';
-import { Account, ListAccountsResponse } from 'aws-sdk/clients/organizations';
-import { Logger } from 'winston';
import {
- AwsOrganizationProviderConfig,
- readAwsOrganizationConfig,
-} from '../awsOrganization/config';
+ Account,
+ ListAccountsResponse,
+ Organizations,
+} from '@aws-sdk/client-organizations';
+import { Logger } from 'winston';
+import { readAwsOrganizationConfig } from '../awsOrganization/config';
+import {
+ AwsCredentialProvider,
+ DefaultAwsCredentialsManager,
+} from '@backstage/integration-aws-node';
const AWS_ORGANIZATION_REGION = 'us-east-1';
const LOCATION_TYPE = 'aws-cloud-accounts';
@@ -47,39 +51,31 @@ const ORGANIZATION_ANNOTATION = 'amazonaws.com/organization-id';
*/
export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
private readonly organizations: Organizations;
- private readonly provider: AwsOrganizationProviderConfig;
+ private readonly logger: Logger;
- static fromConfig(config: Config, options: { logger: Logger }) {
+ static async fromConfig(config: Config, options: { logger: Logger }) {
const c = config.getOptionalConfig('catalog.processors.awsOrganization');
- return new AwsOrganizationCloudAccountProcessor({
- ...options,
- provider: c ? readAwsOrganizationConfig(c) : {},
+ const orgConfig = c ? readAwsOrganizationConfig(c) : undefined;
+ const awsCredentialsManager =
+ DefaultAwsCredentialsManager.fromConfig(config);
+ const credProvider = await awsCredentialsManager.getCredentialProvider({
+ arn: orgConfig?.roleArn,
});
- }
-
- private static buildCredentials(
- config: AwsOrganizationProviderConfig,
- ): Credentials | undefined {
- const roleArn = config.roleArn;
- if (!roleArn) {
- return undefined;
- }
-
- return new AWS.ChainableTemporaryCredentials({
- params: {
- RoleSessionName: 'backstage-aws-organization-processor',
- RoleArn: roleArn,
- },
- });
- }
-
- private constructor(options: { provider: AwsOrganizationProviderConfig }) {
- this.provider = options.provider;
- const credentials = AwsOrganizationCloudAccountProcessor.buildCredentials(
- this.provider,
+ return new AwsOrganizationCloudAccountProcessor(
+ credProvider,
+ options.logger,
);
- this.organizations = new AWS.Organizations({
- credentials,
+ }
+
+ private constructor(
+ private readonly credProvider: AwsCredentialProvider,
+ logger: Logger,
+ ) {
+ this.logger = logger?.child({
+ target: this.getProcessorName(),
+ });
+ this.organizations = new Organizations({
+ credentialDefaultProvider: () => this.credProvider.sdkCredentialProvider,
region: AWS_ORGANIZATION_REGION,
}); // Only available in us-east-1
}
@@ -97,6 +93,8 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
return false;
}
+ this.logger?.info('Discovering AWS Organization Account objects');
+
(await this.getAwsAccounts())
.map(account => this.mapAccountToComponent(account))
.filter(entity => {
@@ -143,9 +141,8 @@ export class AwsOrganizationCloudAccountProcessor implements CatalogProcessor {
let nextToken = undefined;
while (isInitialAttempt || nextToken) {
isInitialAttempt = false;
- const orgAccounts: ListAccountsResponse = await this.organizations
- .listAccounts({ NextToken: nextToken })
- .promise();
+ const orgAccounts: ListAccountsResponse =
+ await this.organizations.listAccounts({ NextToken: nextToken });
if (orgAccounts.Accounts) {
awsAccounts = awsAccounts.concat(orgAccounts.Accounts);
}
diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts
index 821fb6d4f4..e9e17ad76a 100644
--- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts
+++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.test.ts
@@ -23,8 +23,9 @@ import {
import { ConfigReader } from '@backstage/config';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { AwsS3EntityProvider } from './AwsS3EntityProvider';
-import aws from 'aws-sdk';
-import AWSMock from 'aws-sdk-mock';
+import { mockClient } from 'aws-sdk-client-mock';
+import 'aws-sdk-client-mock-jest';
+import { ListObjectsV2Command, S3Client } from '@aws-sdk/client-s3';
class PersistingTaskRunner implements TaskRunner {
private tasks: TaskInvocationDefinition[] = [];
@@ -42,38 +43,39 @@ class PersistingTaskRunner implements TaskRunner {
const logger = getVoidLogger();
describe('AwsS3EntityProvider', () => {
- AWSMock.setSDKInstance(aws);
- const createObjectList = (keys: string[]): aws.S3.ObjectList => {
+ const createObjectList = (keys: string[]) => {
const objects = keys.map(key => {
return {
Key: key,
- } as aws.S3.Types.Object;
+ };
});
-
- return objects as aws.S3.ObjectList;
+ return objects;
};
const keys = ['key1.yaml', 'key2.yaml', 'key3.yaml', 'key 4.yaml'];
+ const mock = mockClient(S3Client);
- AWSMock.mock('S3', 'listObjectsV2', async req => {
- const prefix = req.Prefix ?? '';
+ beforeEach(() => {
+ mock.on(ListObjectsV2Command).callsFake(async req => {
+ const prefix = req.Prefix ?? '';
+
+ if (!req.ContinuationToken) {
+ return {
+ Contents: createObjectList(
+ keys
+ .slice(0, Math.ceil(keys.length / 2))
+ .map(key => `${prefix}${key}`),
+ ),
+ NextContinuationToken: 'next-token',
+ };
+ }
- if (!req.ContinuationToken) {
return {
Contents: createObjectList(
- keys
- .slice(0, Math.ceil(keys.length / 2))
- .map(key => `${prefix}${key}`),
+ keys.slice(Math.ceil(keys.length / 2)).map(key => `${prefix}${key}`),
),
- NextContinuationToken: 'next-token',
- } as aws.S3.Types.ListObjectsV2Output;
- }
-
- return {
- Contents: createObjectList(
- keys.slice(Math.ceil(keys.length / 2)).map(key => `${prefix}${key}`),
- ),
- } as aws.S3.Types.ListObjectsV2Output;
+ };
+ });
});
afterEach(() => {
@@ -103,7 +105,9 @@ describe('AwsS3EntityProvider', () => {
});
const schedulingConfig: Record = {};
-
+ const normalizedExpectedBaseUrl = expectedBaseUrl.endsWith('/')
+ ? expectedBaseUrl
+ : `${expectedBaseUrl}/`;
const schedule = new PersistingTaskRunner();
const entityProviderConnection: EntityProviderConnection = {
applyMutation: jest.fn(),
@@ -131,7 +135,7 @@ describe('AwsS3EntityProvider', () => {
await (taskDef.fn as () => Promise)();
const expectedEntities = keys.map(key => {
- const url = encodeURI(`${expectedBaseUrl}${key}`);
+ const url = encodeURI(`${normalizedExpectedBaseUrl}${key}`);
return {
entity: {
apiVersion: 'backstage.io/v1alpha1',
@@ -161,6 +165,25 @@ describe('AwsS3EntityProvider', () => {
// eslint-disable-next-line jest/expect-expect
it('apply full update on scheduled execution', async () => {
+ return expectMutation(
+ 'regionalStatic',
+ {
+ bucketName: 'bucket-1',
+ prefix: 'sub/dir/',
+ region: 'eu-west-1',
+ },
+ 'https://bucket-1.s3.eu-west-1.amazonaws.com/sub/dir/',
+ {
+ 'key1.yaml': 'generated-8ece85ad90200c6577b99f553dcbedde05fa34bb',
+ 'key2.yaml': 'generated-6b54c6aaa44696f5e91ce0f54fb27bf837549d11',
+ 'key3.yaml': 'generated-88c703cf1aa66913db4033b029adc0b174574646',
+ 'key 4.yaml': 'generated-2b7e068bb4ec818c14f179a1e721843fc2dbc5f9',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('apply full update on scheduled execution force path style', async () => {
return expectMutation(
'regionalStatic',
{
@@ -175,11 +198,72 @@ describe('AwsS3EntityProvider', () => {
'key3.yaml': 'generated-8d75f78ed9fa618ce433b226dc24eeab441f3a2d',
'key 4.yaml': 'generated-1e0249dcb5805fc2ce6ac2d3c4d2a3ef4f1270c0',
},
+ {
+ s3ForcePathStyle: true,
+ },
);
});
// eslint-disable-next-line jest/expect-expect
- it('us-east-1 has region-less URLs', async () => {
+ it('apply full update on scheduled execution no prefix', async () => {
+ return expectMutation(
+ 'regionalStatic',
+ {
+ bucketName: 'bucket-1',
+ region: 'eu-west-1',
+ },
+ 'https://bucket-1.s3.eu-west-1.amazonaws.com/',
+ {
+ 'key1.yaml': 'generated-ac6d99e56e0fbd6bf3719a5182f37bfa84b1cddc',
+ 'key2.yaml': 'generated-cc3e1145496cd95bf3898687bb5b7d2991a7b7fd',
+ 'key3.yaml': 'generated-a197557b0cb89d14ac1297231ed930bd4c40b664',
+ 'key 4.yaml': 'generated-a774e6c1d1c7ee511b7d79ebb99b08a39cf439c4',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('apply full update on scheduled execution force path style no prefix', async () => {
+ return expectMutation(
+ 'regionalStatic',
+ {
+ bucketName: 'bucket-1',
+ region: 'eu-west-1',
+ },
+ 'https://s3.eu-west-1.amazonaws.com/bucket-1',
+ {
+ 'key1.yaml': 'generated-b390cd4de7ff00f6d51426870fc0e685029fbf09',
+ 'key2.yaml': 'generated-15ebeb1e53ffe71ea93f037076aefcdc5cc5e617',
+ 'key3.yaml': 'generated-fd0a33e70bafd707a60139decbd0383361ab3b4e',
+ 'key 4.yaml': 'generated-ac574fb266d9c8bd983b25a208c538e4ad2c1d06',
+ },
+ {
+ s3ForcePathStyle: true,
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('apply full update on scheduled execution bucket with dot in it', async () => {
+ return expectMutation(
+ 'regionalStatic',
+ {
+ bucketName: 'bucket.1',
+ prefix: 'sub/dir/',
+ region: 'eu-west-1',
+ },
+ 'https://s3.eu-west-1.amazonaws.com/bucket.1/sub/dir/',
+ {
+ 'key1.yaml': 'generated-651b5851d4670b7d5bb9a319d8f6eb295092e4ad',
+ 'key2.yaml': 'generated-ccddcdc3ad814aea072883118f3a9d0ac39a161e',
+ 'key3.yaml': 'generated-b2b8548903aa764544ab79dc81ab5093e6e4ba83',
+ 'key 4.yaml': 'generated-aafa9cc0422f5e8ccce1616dc0c74fa94f692af3',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('us-east-1 does not have region-less URLs', async () => {
return expectMutation(
'usEast1',
{
@@ -187,12 +271,12 @@ describe('AwsS3EntityProvider', () => {
prefix: 'sub/dir/',
region: 'us-east-1',
},
- 'https://s3.amazonaws.com/bucket-1/sub/dir/',
+ 'https://bucket-1.s3.us-east-1.amazonaws.com/sub/dir/',
{
- 'key1.yaml': 'generated-f7e3f1c89f62cbf0d82db16452faaa7c040fc331',
- 'key2.yaml': 'generated-925173f7dba1acaa73cac5ef4d10c3ed7660aa25',
- 'key3.yaml': 'generated-d94cf017911ed7fb3be4a62a3dae1f5202879de3',
- 'key 4.yaml': 'generated-f917fca0cfacc2be478ca2a3cff94d00917a1cde',
+ 'key1.yaml': 'generated-980e6ad47fbfbfeead708a9c7c87331b7540296a',
+ 'key2.yaml': 'generated-266794d8e789089dddba2b42cd79e70b149aa61c',
+ 'key3.yaml': 'generated-96f0cdcd7e33aa687c19d160ec7d5b1975cb9ea1',
+ 'key 4.yaml': 'generated-4f6d3ffb232e4a5b82c3a52549e4c36dab1c2f8d',
},
);
});
@@ -209,18 +293,60 @@ describe('AwsS3EntityProvider', () => {
bucketName: 'bucket-1',
prefix: 'sub/dir/',
},
- 'https://s3.eu-central-1.amazonaws.com/bucket-1/sub/dir/',
+ 'https://bucket-1.s3.eu-central-1.amazonaws.com/sub/dir/',
{
- 'key1.yaml': 'generated-285d144b5c1e24e801b97f61f794d566dabc7236',
- 'key2.yaml': 'generated-36b78d7fa69690059797d4cc4f40c2a5eaed1d6d',
- 'key3.yaml': 'generated-a39ad804bf65993cbc0f3810ff128a3c91daf768',
- 'key 4.yaml': 'generated-bd7622ac86c313cb812d90e335b2f5f6bff14670',
+ 'key1.yaml': 'generated-bb17a9f9b420edd93137692f000b2d443a29239c',
+ 'key2.yaml': 'generated-b4559ecc8b615407e421dbaa3fcb9338e977b8ac',
+ 'key3.yaml': 'generated-bb3d500a6f8b351b0eed1aec77ebce65c1ab41fb',
+ 'key 4.yaml': 'generated-686de87f5882f6558b43280c5b6047e485199e88',
},
);
});
// eslint-disable-next-line jest/expect-expect
it('custom endpoint', async () => {
+ return expectMutation(
+ 'customEndpoint',
+ {
+ bucketName: 'bucket-1',
+ prefix: 'sub/dir/',
+ },
+ 'http://bucket-1.localhost:1234/sub/dir/',
+ {
+ 'key1.yaml': 'generated-79625971ead9c39c91b0bfef4d7c748325e31234',
+ 'key2.yaml': 'generated-eeae95f16605d3afdc615436c18c9fbd35781299',
+ 'key3.yaml': 'generated-692976561563c3fa55889ad090363716c7b7e915',
+ 'key 4.yaml': 'generated-c8f92ea0eb96c355b7e56cbc461cef29f830747e',
+ },
+ {
+ endpoint: 'http://localhost:1234/',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('custom endpoint bucket dot', async () => {
+ return expectMutation(
+ 'customEndpoint',
+ {
+ bucketName: 'bucket.1',
+ prefix: 'sub/dir/',
+ },
+ 'http://localhost:1234/bucket.1/sub/dir/',
+ {
+ 'key1.yaml': 'generated-07be77ac8626ac76a924414e0edf5ce66d3e0b27',
+ 'key2.yaml': 'generated-47295ada3c894cf9bac0d570a34d909244ea2f74',
+ 'key3.yaml': 'generated-30e98f3ebd350f3b618acd024199b5f31545495d',
+ 'key 4.yaml': 'generated-b71bc7661b92cef0db62c9652912fd5e4b245aa7',
+ },
+ {
+ endpoint: 'http://localhost:1234/',
+ },
+ );
+ });
+
+ // eslint-disable-next-line jest/expect-expect
+ it('custom endpoint force path style', async () => {
return expectMutation(
'customEndpoint',
{
@@ -235,7 +361,8 @@ describe('AwsS3EntityProvider', () => {
'key 4.yaml': 'generated-659f17a2429f107db04850f9081e3e9c56084aeb',
},
{
- endpoint: 'http://localhost:1234',
+ endpoint: 'http://localhost:1234/',
+ s3ForcePathStyle: true,
},
);
});
@@ -305,12 +432,12 @@ describe('AwsS3EntityProvider', () => {
},
},
},
- 'https://s3.eu-west-1.amazonaws.com/bucket-1/sub/dir/',
+ 'https://bucket-1.s3.eu-west-1.amazonaws.com/sub/dir/',
{
- 'key1.yaml': 'generated-7f6d5861b0b3401a38b5fe62e6c7ca11da5fd6d8',
- 'key2.yaml': 'generated-a290be145586042af7d80715626399c9d661718d',
- 'key3.yaml': 'generated-8d75f78ed9fa618ce433b226dc24eeab441f3a2d',
- 'key 4.yaml': 'generated-1e0249dcb5805fc2ce6ac2d3c4d2a3ef4f1270c0',
+ 'key1.yaml': 'generated-8ece85ad90200c6577b99f553dcbedde05fa34bb',
+ 'key2.yaml': 'generated-6b54c6aaa44696f5e91ce0f54fb27bf837549d11',
+ 'key3.yaml': 'generated-88c703cf1aa66913db4033b029adc0b174574646',
+ 'key 4.yaml': 'generated-2b7e068bb4ec818c14f179a1e721843fc2dbc5f9',
},
undefined,
true,
diff --git a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts
index 8212f9279a..4a87a42e75 100644
--- a/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts
+++ b/plugins/catalog-backend-module-aws/src/providers/AwsS3EntityProvider.ts
@@ -23,13 +23,20 @@ import {
locationSpecToLocationEntity,
} from '@backstage/plugin-catalog-node';
import { LocationSpec } from '@backstage/plugin-catalog-common';
-import { AwsCredentials } from '../credentials/AwsCredentials';
import { readAwsS3Configs } from './config';
import { AwsS3Config } from './types';
-import { S3 } from 'aws-sdk';
-import { ListObjectsV2Output } from 'aws-sdk/clients/s3';
+import {
+ ListObjectsV2Command,
+ ListObjectsV2Output,
+ S3,
+} from '@aws-sdk/client-s3';
import * as uuid from 'uuid';
import { Logger } from 'winston';
+import { getEndpointFromInstructions } from '@aws-sdk/middleware-endpoint';
+import {
+ AwsCredentialsManager,
+ DefaultAwsCredentialsManager,
+} from '@backstage/integration-aws-node';
// TODO: event-based updates using S3 events (+ queue like SQS)?
/**
@@ -41,9 +48,10 @@ import { Logger } from 'winston';
*/
export class AwsS3EntityProvider implements EntityProvider {
private readonly logger: Logger;
- private readonly s3: S3;
+ private s3?: S3;
private readonly scheduleFn: () => Promise;
private connection?: EntityProviderConnection;
+ private endpoint?: string;
static fromConfig(
configRoot: Config,
@@ -78,7 +86,8 @@ export class AwsS3EntityProvider implements EntityProvider {
`No schedule provided neither via code nor config for awsS3-provider:${providerConfig.id}.`,
);
}
-
+ const awsCredentialsManager =
+ DefaultAwsCredentialsManager.fromConfig(configRoot);
const taskRunner =
options.schedule ??
options.scheduler!.createScheduledTaskRunner(providerConfig.schedule!);
@@ -86,6 +95,7 @@ export class AwsS3EntityProvider implements EntityProvider {
return new AwsS3EntityProvider(
providerConfig,
integration,
+ awsCredentialsManager,
options.logger,
taskRunner,
);
@@ -94,7 +104,8 @@ export class AwsS3EntityProvider implements EntityProvider {
private constructor(
private readonly config: AwsS3Config,
- integration: AwsS3Integration,
+ private readonly integration: AwsS3Integration,
+ private readonly awsCredentialsManager: AwsCredentialsManager,
logger: Logger,
taskRunner: TaskRunner,
) {
@@ -102,17 +113,6 @@ export class AwsS3EntityProvider implements EntityProvider {
target: this.getProviderName(),
});
- this.s3 = new S3({
- apiVersion: '2006-03-01',
- credentials: AwsCredentials.create(
- integration.config,
- 'backstage-aws-s3-provider',
- ),
- endpoint: integration.config.endpoint,
- region: this.config.region,
- s3ForcePathStyle: integration.config.s3ForcePathStyle,
- });
-
this.scheduleFn = this.createScheduleFn(taskRunner);
}
@@ -146,6 +146,28 @@ export class AwsS3EntityProvider implements EntityProvider {
/** {@inheritdoc @backstage/plugin-catalog-backend#EntityProvider.connect} */
async connect(connection: EntityProviderConnection): Promise {
this.connection = connection;
+ const credProvider =
+ await this.awsCredentialsManager.getCredentialProvider();
+ this.s3 = new S3({
+ apiVersion: '2006-03-01',
+ credentialDefaultProvider: () => credProvider.sdkCredentialProvider,
+ endpoint: this.integration.config.endpoint,
+ region: this.config.region,
+ forcePathStyle: this.integration.config.s3ForcePathStyle,
+ });
+
+ // https://github.com/aws/aws-sdk-js-v3/issues/4122#issuecomment-1298968804
+ const endpoint = await getEndpointFromInstructions(
+ {
+ Bucket: this.config.bucketName,
+ },
+ ListObjectsV2Command,
+ this.s3.config as unknown as Record,
+ );
+ if (endpoint?.url)
+ this.endpoint = endpoint.url.href.endsWith('/')
+ ? endpoint.url.href
+ : `${endpoint.url.href}/`;
await this.scheduleFn();
}
@@ -175,18 +197,21 @@ export class AwsS3EntityProvider implements EntityProvider {
}
private async listAllObjectKeys(): Promise {
+ if (!this.s3) {
+ throw new Error('Not initialized');
+ }
+
const keys: string[] = [];
let continuationToken: string | undefined = undefined;
let output: ListObjectsV2Output;
do {
- const request = this.s3.listObjectsV2({
+ output = await this.s3.listObjectsV2({
Bucket: this.config.bucketName,
ContinuationToken: continuationToken,
Prefix: this.config.prefix,
});
- output = await request.promise();
if (output.Contents) {
output.Contents.forEach(item => {
if (item.Key && !item.Key.endsWith('/')) {
@@ -209,9 +234,6 @@ export class AwsS3EntityProvider implements EntityProvider {
}
private createObjectUrl(key: string): string {
- const bucketName = this.config.bucketName;
- const endpoint = this.s3.endpoint.href;
-
- return encodeURI(`${endpoint}${bucketName}/${key}`);
+ return new URL(key, this.endpoint).href;
}
}
diff --git a/plugins/catalog-backend-module-aws/src/types.ts b/plugins/catalog-backend-module-aws/src/types.ts
index 3d9df38181..23460c104e 100644
--- a/plugins/catalog-backend-module-aws/src/types.ts
+++ b/plugins/catalog-backend-module-aws/src/types.ts
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { Credentials } from 'aws-sdk';
+import { AwsCredentialIdentity } from '@aws-sdk/types';
/**
* A factory for providing user-specified AWS credentials for a given AWS account.
@@ -22,4 +22,4 @@ import { Credentials } from 'aws-sdk';
*/
export type AWSCredentialFactory = (
awsAccountId: string,
-) => Promise;
+) => Promise;
diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md
index e8458d82e7..1783335c0f 100644
--- a/plugins/catalog-backend-module-azure/CHANGELOG.md
+++ b/plugins/catalog-backend-module-azure/CHANGELOG.md
@@ -1,5 +1,37 @@
# @backstage/plugin-catalog-backend-module-azure
+## 0.1.17-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 0.1.16
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
## 0.1.16-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json
index 2e2ff96e7e..eb94c176f3 100644
--- a/plugins/catalog-backend-module-azure/package.json
+++ b/plugins/catalog-backend-module-azure/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-azure",
"description": "A Backstage catalog backend module that helps integrate towards Azure",
- "version": "0.1.16-next.1",
+ "version": "0.1.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md
index af6e6c3a7f..c11afef754 100644
--- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md
+++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md
@@ -1,5 +1,39 @@
# @backstage/plugin-catalog-backend-module-bitbucket-cloud
+## 0.1.13-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/plugin-bitbucket-cloud-common@0.2.6
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-events-node@0.2.7-next.0
+
+## 0.1.12
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-bitbucket-cloud-common@0.2.6
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-events-node@0.2.6
+
## 0.1.12-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json
index 77df5b12b4..946a50d6a5 100644
--- a/plugins/catalog-backend-module-bitbucket-cloud/package.json
+++ b/plugins/catalog-backend-module-bitbucket-cloud/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-bitbucket-cloud",
"description": "A Backstage catalog backend module that helps integrate towards Bitbucket Cloud",
- "version": "0.1.12-next.1",
+ "version": "0.1.13-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md
index 3c9e51b08e..5609b696c4 100644
--- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md
+++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md
@@ -1,5 +1,33 @@
# @backstage/plugin-catalog-backend-module-bitbucket-server
+## 0.1.11-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+
+## 0.1.10
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 0.1.10-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json
index 68960fbf81..cbb4c89e04 100644
--- a/plugins/catalog-backend-module-bitbucket-server/package.json
+++ b/plugins/catalog-backend-module-bitbucket-server/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend-module-bitbucket-server",
- "version": "0.1.10-next.1",
+ "version": "0.1.11-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md
index 0193d551a5..c4bf6ca2e2 100644
--- a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md
+++ b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md
@@ -1,5 +1,33 @@
# @backstage/plugin-catalog-backend-module-bitbucket
+## 0.2.13-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-bitbucket-cloud-common@0.2.6
+
+## 0.2.12
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-bitbucket-cloud-common@0.2.6
+
## 0.2.12-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-bitbucket/package.json b/plugins/catalog-backend-module-bitbucket/package.json
index 64269a9102..2efd5558ae 100644
--- a/plugins/catalog-backend-module-bitbucket/package.json
+++ b/plugins/catalog-backend-module-bitbucket/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-bitbucket",
"description": "A Backstage catalog backend module that helps integrate towards Bitbucket",
- "version": "0.2.12-next.1",
+ "version": "0.2.13-next.0",
"deprecated": true,
"main": "src/index.ts",
"types": "src/index.ts",
diff --git a/plugins/catalog-backend-module-gerrit/CHANGELOG.md b/plugins/catalog-backend-module-gerrit/CHANGELOG.md
index ea3d4af960..9f22598c98 100644
--- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md
+++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md
@@ -1,5 +1,33 @@
# @backstage/plugin-catalog-backend-module-gerrit
+## 0.1.14-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+
+## 0.1.13
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 0.1.13-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json
index 22bc3a95e5..f104b894f2 100644
--- a/plugins/catalog-backend-module-gerrit/package.json
+++ b/plugins/catalog-backend-module-gerrit/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-backend-module-gerrit",
- "version": "0.1.13-next.1",
+ "version": "0.1.14-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md
index f54a55f9e9..42ec45f0d1 100644
--- a/plugins/catalog-backend-module-github/CHANGELOG.md
+++ b/plugins/catalog-backend-module-github/CHANGELOG.md
@@ -1,5 +1,66 @@
# @backstage/plugin-catalog-backend-module-github
+## 0.3.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/plugin-catalog-backend@1.9.2-next.0
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-events-node@0.2.7-next.0
+
+## 0.3.0
+
+### Minor Changes
+
+- 970678adbe2: Implement events support for `GithubMultiOrgEntityProvider`
+
+ **BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior
+
+### Patch Changes
+
+- 78bb674a713: Fixed bug in queryWithPaging that caused secondary rate limit errors in GitHub with organizations having more than 1000 repositories. This change makes one request per second to avoid concurrency issues.
+- bd101cefd37: Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description.
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-events-node@0.2.6
+
+## 0.3.0-next.2
+
+### Minor Changes
+
+- 970678adbe2: Implement events support for `GithubMultiOrgEntityProvider`
+
+ **BREAKING:** Passing in a custom `teamTransformer` will now correctly completely override the default transformer behavior
+
+### Patch Changes
+
+- bd101cefd37: Updated the `team.edited` event emitted from `GithubOrgEntityProvider` to also include teams description.
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
## 0.2.8-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-github/api-report.md b/plugins/catalog-backend-module-github/api-report.md
index 0698fae8a0..7b944b33a9 100644
--- a/plugins/catalog-backend-module-github/api-report.md
+++ b/plugins/catalog-backend-module-github/api-report.md
@@ -10,6 +10,7 @@ import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
+import { EventBroker } from '@backstage/plugin-events-node';
import { EventParams } from '@backstage/plugin-events-node';
import { EventSubscriber } from '@backstage/plugin-events-node';
import { GithubCredentialsProvider } from '@backstage/integration';
@@ -158,6 +159,7 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
// @public
export interface GithubMultiOrgEntityProviderOptions {
+ eventBroker?: EventBroker;
githubCredentialsProvider?: GithubCredentialsProvider;
githubUrl: string;
id: string;
diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json
index 5b29baa05c..f4cd2d1e0b 100644
--- a/plugins/catalog-backend-module-github/package.json
+++ b/plugins/catalog-backend-module-github/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-github",
"description": "A Backstage catalog backend module that helps integrate towards GitHub",
- "version": "0.2.8-next.1",
+ "version": "0.3.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-github/src/lib/github.test.ts b/plugins/catalog-backend-module-github/src/lib/github.test.ts
index df4565a174..7a0a29e098 100644
--- a/plugins/catalog-backend-module-github/src/lib/github.test.ts
+++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts
@@ -21,6 +21,7 @@ import { graphql as graphqlMsw } from 'msw';
import { setupServer } from 'msw/node';
import { TeamTransformer, UserTransformer } from './defaultTransformers';
import {
+ getOrganizationsFromUser,
getOrganizationTeams,
getOrganizationUsers,
getTeamMembers,
@@ -448,6 +449,37 @@ describe('github', () => {
});
});
+ describe('getOrganizationsFromUser', () => {
+ it('reads orgs from user', async () => {
+ const input: QueryResponse = {
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'a',
+ },
+ {
+ login: 'b',
+ },
+ {
+ login: 'c',
+ },
+ ],
+ },
+ },
+ };
+
+ server.use(
+ graphqlMsw.query('orgs', (_req, res, ctx) => res(ctx.data(input))),
+ );
+
+ await expect(getOrganizationsFromUser(graphql, 'foo')).resolves.toEqual({
+ orgs: ['a', 'b', 'c'],
+ });
+ });
+ });
+
describe('getTeamMembers', () => {
it('reads team members', async () => {
const input: QueryResponse = {
diff --git a/plugins/catalog-backend-module-github/src/lib/github.ts b/plugins/catalog-backend-module-github/src/lib/github.ts
index d0b629092a..412b751c38 100644
--- a/plugins/catalog-backend-module-github/src/lib/github.ts
+++ b/plugins/catalog-backend-module-github/src/lib/github.ts
@@ -33,6 +33,7 @@ import { DeferredEntity } from '@backstage/plugin-catalog-node';
export type QueryResponse = {
organization?: OrganizationResponse;
repositoryOwner?: RepositoryOwnerResponse;
+ user?: UserResponse;
};
type RepositoryOwnerResponse = {
@@ -46,11 +47,19 @@ export type OrganizationResponse = {
repositories?: Connection;
};
+export type UserResponse = {
+ organizations?: Connection;
+};
+
export type PageInfo = {
hasNextPage: boolean;
endCursor?: string;
};
+export type GithubOrg = {
+ login: string;
+};
+
/**
* Github User
*
@@ -334,6 +343,34 @@ export async function getOrganizationTeamsFromUsers(
return { groups };
}
+export async function getOrganizationsFromUser(
+ client: typeof graphql,
+ user: string,
+): Promise<{
+ orgs: string[];
+}> {
+ const query = `
+ query orgs($user: String!) {
+ user(login: $user) {
+ organizations(first: 100) {
+ nodes { login }
+ pageInfo { hasNextPage, endCursor }
+ }
+ }
+ }`;
+
+ const orgs = await queryWithPaging(
+ client,
+ query,
+ '',
+ r => r.user?.organizations,
+ async o => o.login,
+ { user },
+ );
+
+ return { orgs };
+}
+
export async function getOrganizationTeam(
client: typeof graphql,
org: string,
diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts
index 76ab55552d..99bef6e074 100644
--- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts
+++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.test.ts
@@ -16,11 +16,13 @@
import { getVoidLogger } from '@backstage/backend-common';
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
+import { ConfigReader } from '@backstage/config';
import {
GithubCredentialsProvider,
GithubIntegrationConfig,
} from '@backstage/integration';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
+import { EventSubscriber } from '@backstage/plugin-events-node';
import { graphql } from '@octokit/graphql';
import {
GithubMultiOrgEntityProvider,
@@ -160,7 +162,7 @@ describe('GithubMultiOrgEntityProvider', () => {
const logger = getVoidLogger();
const gitHubConfig: GithubIntegrationConfig = {
- host: 'https://github.com',
+ host: 'github.com',
};
const mockGetCredentials = jest.fn().mockReturnValue({
@@ -201,9 +203,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/a',
+ 'url:https://github.com/a',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/a',
+ 'url:https://github.com/a',
'github.com/user-login': 'a',
},
description: 'c',
@@ -227,9 +229,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/x',
+ 'url:https://github.com/x',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/x',
+ 'url:https://github.com/x',
'github.com/user-login': 'x',
},
description: 'z',
@@ -253,9 +255,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/q',
+ 'url:https://github.com/q',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/q',
+ 'url:https://github.com/q',
'github.com/user-login': 'q',
},
description: 's',
@@ -279,9 +281,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/orgs/orgA/teams/team',
+ 'url:https://github.com/orgs/orgA/teams/team',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/orgs/orgA/teams/team',
+ 'url:https://github.com/orgs/orgA/teams/team',
'github.com/team-slug': 'orgA/team',
},
namespace: 'orga',
@@ -308,9 +310,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/orgs/orgB/teams/team',
+ 'url:https://github.com/orgs/orgB/teams/team',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/orgs/orgB/teams/team',
+ 'url:https://github.com/orgs/orgB/teams/team',
'github.com/team-slug': 'orgB/team',
},
namespace: 'orgb',
@@ -467,7 +469,7 @@ describe('GithubMultiOrgEntityProvider', () => {
const logger = getVoidLogger();
const gitHubConfig: GithubIntegrationConfig = {
- host: 'https://github.com',
+ host: 'github.com',
};
const mockGetCredentials = jest.fn().mockReturnValue({
@@ -507,9 +509,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/a',
+ 'url:https://github.com/a',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/a',
+ 'url:https://github.com/a',
'github.com/user-login': 'a',
},
description: 'c',
@@ -533,9 +535,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/x',
+ 'url:https://github.com/x',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/x',
+ 'url:https://github.com/x',
'github.com/user-login': 'x',
},
description: 'z',
@@ -559,9 +561,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/q',
+ 'url:https://github.com/q',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/q',
+ 'url:https://github.com/q',
'github.com/user-login': 'q',
},
description: 's',
@@ -585,9 +587,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/orgs/orgC/teams/team',
+ 'url:https://github.com/orgs/orgC/teams/team',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/orgs/orgC/teams/team',
+ 'url:https://github.com/orgs/orgC/teams/team',
'github.com/team-slug': 'orgC/team',
},
namespace: 'orgc',
@@ -614,9 +616,9 @@ describe('GithubMultiOrgEntityProvider', () => {
metadata: {
annotations: {
'backstage.io/managed-by-location':
- 'url:https://https://github.com/orgs/orgD/teams/team',
+ 'url:https://github.com/orgs/orgD/teams/team',
'backstage.io/managed-by-origin-location':
- 'url:https://https://github.com/orgs/orgD/teams/team',
+ 'url:https://github.com/orgs/orgD/teams/team',
'github.com/team-slug': 'orgD/team',
},
namespace: 'orgd',
@@ -713,4 +715,1223 @@ describe('GithubMultiOrgEntityProvider', () => {
});
});
});
+
+ describe('events', () => {
+ let onEvent: Function;
+
+ const entityProviderConnection: EntityProviderConnection = {
+ applyMutation: jest.fn(),
+ refresh: jest.fn(),
+ };
+
+ beforeEach(() => {
+ const logger = getVoidLogger();
+ const config = new ConfigReader({
+ integrations: {
+ github: [
+ {
+ host: 'github.com',
+ },
+ ],
+ },
+ });
+
+ const mockGetCredentials = jest.fn().mockReturnValue({
+ headers: { token: 'blah' },
+ type: 'app',
+ });
+
+ const githubCredentialsProvider: GithubCredentialsProvider = {
+ getCredentials: mockGetCredentials,
+ };
+
+ const mockEventBroker = {
+ publish: async () => {},
+ subscribe: (subscriber: EventSubscriber) => {
+ onEvent = subscriber.onEvent;
+ },
+ };
+
+ const entityProvider = GithubMultiOrgEntityProvider.fromConfig(config, {
+ id: 'my-id',
+ githubCredentialsProvider,
+ githubUrl: 'https://github.com',
+ logger,
+ orgs: ['orgA', 'orgB'],
+ eventBroker: mockEventBroker,
+ });
+
+ entityProvider.connect(entityProviderConnection);
+ });
+
+ afterEach(() => jest.resetAllMocks());
+
+ it('should ignore events from non-applicable orgs', async () => {
+ await onEvent({
+ topic: 'github.organization',
+ eventPayload: {
+ action: 'member_added',
+ membership: {
+ user: {
+ name: 'githubuser',
+ login: 'githubuser',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ },
+ },
+ organization: {
+ login: 'test-org',
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled();
+
+ await onEvent({
+ topic: 'github.installation',
+ eventPayload: {
+ action: 'created',
+ installation: {
+ account: {
+ login: 'test-org',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).not.toHaveBeenCalled();
+ });
+
+ describe('installation', () => {
+ it('adds users and groups from new org', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ organization: {
+ membersWithRole: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'a',
+ name: 'b',
+ bio: 'c',
+ email: 'd',
+ avatarUrl: 'e',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ parentTeam: {
+ slug: 'parent',
+ combinedSlug: '',
+ members: { pageInfo: { hasNextPage: false }, nodes: [] },
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ parentTeam: {
+ slug: 'parent',
+ combinedSlug: '',
+ members: { pageInfo: { hasNextPage: false }, nodes: [] },
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ parentTeam: {
+ slug: 'parent',
+ combinedSlug: '',
+ members: { pageInfo: { hasNextPage: false }, nodes: [] },
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.installation',
+ eventPayload: {
+ action: 'created',
+ installation: {
+ account: {
+ login: 'orgB',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ description: 'c',
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orga/team', 'orgb/team'],
+ profile: {
+ displayName: 'b',
+ email: 'd',
+ picture: 'e',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgB/teams/team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgB/teams/team',
+ 'github.com/team-slug': 'orgB/team',
+ },
+ namespace: 'orgb',
+ name: 'team',
+ description: 'The one and only team',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'TeamB',
+ picture: 'http://example.com/team.jpeg',
+ },
+ type: 'team',
+ members: ['default/a'],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+ });
+
+ describe('organization', () => {
+ it('should add a new user', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgA',
+ },
+ {
+ login: 'orgB',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.organization',
+ eventPayload: {
+ action: 'member_added',
+ organization: {
+ login: 'orgB',
+ },
+ membership: {
+ user: {
+ name: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ login: 'a',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orga/team'],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+
+ it('should remove a user', async () => {
+ const mockClient = jest.fn();
+
+ mockClient.mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgC',
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.organization',
+ eventPayload: {
+ action: 'member_removed',
+ organization: {
+ login: 'orgB',
+ },
+ membership: {
+ user: {
+ name: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ login: 'a',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [],
+ removed: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: [],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ });
+ });
+
+ it('should only update group memberships of a user that still belongs to an applicable org', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgA',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.organization',
+ eventPayload: {
+ action: 'member_removed',
+ organization: {
+ login: 'orgB',
+ },
+ membership: {
+ user: {
+ name: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ login: 'a',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orga/team'],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+ });
+
+ describe('team', () => {
+ it('should create a new group from a new team', async () => {
+ await onEvent({
+ topic: 'github.team',
+ eventPayload: {
+ action: 'created',
+ organization: {
+ login: 'orgB',
+ },
+ team: {
+ name: 'New Team',
+ slug: 'new-team',
+ description: 'description from the new team',
+ html_url: 'https://github.com/orgs/orgB/teams/new-team',
+ parent: {
+ slug: 'father-team',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'new-team',
+ namespace: 'orgb',
+ description: 'description from the new team',
+ annotations: {
+ 'backstage.io/edit-url':
+ 'https://github.com/orgs/orgB/teams/new-team/edit',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgB/teams/new-team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgB/teams/new-team',
+ 'github.com/team-slug': 'orgB/new-team',
+ },
+ },
+ spec: {
+ type: 'team',
+ children: [],
+ members: [],
+ parent: 'father-team',
+ profile: {
+ displayName: 'New Team',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+
+ it('should remove a group from a deleted team', async () => {
+ await onEvent({
+ topic: 'github.team',
+ eventPayload: {
+ action: 'deleted',
+ organization: {
+ login: 'orgB',
+ },
+ team: {
+ name: 'New Team',
+ slug: 'new-team',
+ description: 'description from the new team',
+ html_url: 'https://github.com/orgs/orgB/teams/new-team',
+ parent: {
+ slug: 'father-team',
+ },
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [],
+ removed: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'new-team',
+ namespace: 'orgb',
+ description: 'description from the new team',
+ annotations: {
+ 'backstage.io/edit-url':
+ 'https://github.com/orgs/orgB/teams/new-team/edit',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgB/teams/new-team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgB/teams/new-team',
+ 'github.com/team-slug': 'orgB/new-team',
+ },
+ },
+ spec: {
+ type: 'team',
+ children: [],
+ members: [],
+ parent: 'father-team',
+ profile: {
+ displayName: 'New Team',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ });
+ });
+
+ it('should update group and user entities when a team is edited', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ organization: {
+ team: {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ membersWithRole: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'a',
+ name: 'b',
+ bio: 'c',
+ email: 'd',
+ avatarUrl: 'e',
+ },
+ {
+ login: 'w',
+ name: 'x',
+ bio: 'y',
+ email: 'z',
+ avatarUrl: 'q',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.team',
+ eventPayload: {
+ action: 'edited',
+ changes: {
+ name: {
+ from: 'oldName',
+ },
+ description: {
+ from: 'oldDescription',
+ },
+ },
+ team: {
+ slug: 'team',
+ parent: {
+ slug: 'parent',
+ },
+ },
+ organization: {
+ login: 'orgA',
+ },
+ },
+ });
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ description: 'c',
+ },
+ spec: {
+ memberOf: ['orga/team', 'orgb/team'],
+ profile: {
+ displayName: 'b',
+ email: 'd',
+ picture: 'e',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/edit-url': 'https://example.com',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'github.com/team-slug': 'orgA/team',
+ },
+ namespace: 'orga',
+ name: 'team',
+ description: 'The one and only team',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'TeamA',
+ picture: 'http://example.com/team.jpeg',
+ },
+ type: 'team',
+ members: ['default/a'],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgA/teams/oldname',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgA/teams/oldname',
+ 'github.com/team-slug': 'orgA/oldname',
+ },
+ namespace: 'orga',
+ name: 'oldname',
+ description: 'oldDescription',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'oldName',
+ },
+ type: 'team',
+ members: [],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ });
+ });
+ });
+
+ describe('membership', () => {
+ it('should update group and user entities when a member is added', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ organization: {
+ team: {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgA',
+ },
+ {
+ login: 'orgB',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.membership',
+ eventPayload: {
+ action: 'added',
+ organization: {
+ login: 'orgA',
+ },
+ team: {
+ slug: 'teama',
+ },
+ member: {
+ name: 'a',
+ login: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ },
+ },
+ });
+ await new Promise(process.nextTick);
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orga/team', 'orgb/team'],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/edit-url': 'https://example.com',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'github.com/team-slug': 'orgA/team',
+ },
+ namespace: 'orga',
+ name: 'team',
+ description: 'The one and only team',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'TeamA',
+ picture: 'http://example.com/team.jpeg',
+ },
+ type: 'team',
+ members: ['default/a'],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+
+ it('should update group and user entities when a member is removed', async () => {
+ const mockClient = jest.fn();
+
+ mockClient
+ .mockResolvedValueOnce({
+ organization: {
+ team: {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [],
+ },
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ user: {
+ organizations: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ login: 'orgA',
+ },
+ {
+ login: 'orgB',
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgA/team',
+ name: 'TeamA',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [],
+ },
+ },
+ ],
+ },
+ },
+ })
+ .mockResolvedValueOnce({
+ organization: {
+ teams: {
+ pageInfo: { hasNextPage: false },
+ nodes: [
+ {
+ slug: 'team',
+ combinedSlug: 'orgB/team',
+ name: 'TeamB',
+ description: 'The one and only team',
+ avatarUrl: 'http://example.com/team.jpeg',
+ editTeamUrl: 'https://example.com',
+ parentTeam: {
+ slug: 'parent',
+ },
+ members: {
+ pageInfo: { hasNextPage: false },
+ nodes: [{ login: 'a', name: 'a' }],
+ },
+ },
+ ],
+ },
+ },
+ });
+
+ (graphql.defaults as jest.Mock).mockReturnValue(mockClient);
+
+ await onEvent({
+ topic: 'github.membership',
+ eventPayload: {
+ action: 'removed',
+ organization: {
+ login: 'orgA',
+ },
+ team: {
+ slug: 'teama',
+ },
+ member: {
+ name: 'a',
+ login: 'a',
+ avatar_url: 'https://avatars.githubusercontent.com/u/83820368',
+ email: 'user1@test.com',
+ },
+ },
+ });
+ await new Promise(process.nextTick);
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledTimes(1);
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'delta',
+ added: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'User',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/a',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/a',
+ 'github.com/user-login': 'a',
+ },
+ name: 'a',
+ },
+ spec: {
+ memberOf: ['orgb/team'],
+ profile: {
+ displayName: 'a',
+ email: 'user1@test.com',
+ picture: 'https://avatars.githubusercontent.com/u/83820368',
+ },
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ annotations: {
+ 'backstage.io/edit-url': 'https://example.com',
+ 'backstage.io/managed-by-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://github.com/orgs/orgA/teams/team',
+ 'github.com/team-slug': 'orgA/team',
+ },
+ namespace: 'orga',
+ name: 'team',
+ description: 'The one and only team',
+ },
+ spec: {
+ children: [],
+ parent: 'parent',
+ profile: {
+ displayName: 'TeamA',
+ picture: 'http://example.com/team.jpeg',
+ },
+ type: 'team',
+ members: [],
+ },
+ },
+ locationKey: 'github-multi-org-provider:my-id',
+ },
+ ],
+ removed: [],
+ });
+ });
+ });
+ });
});
diff --git a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts
index ce45f30d96..51796c2116 100644
--- a/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts
+++ b/plugins/catalog-backend-module-github/src/providers/GithubMultiOrgEntityProvider.ts
@@ -21,7 +21,9 @@ import {
DEFAULT_NAMESPACE,
Entity,
GroupEntity,
+ parseEntityRef,
stringifyEntityRef,
+ UserEntity,
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import {
@@ -32,10 +34,24 @@ import {
ScmIntegrations,
} from '@backstage/integration';
import {
+ DeferredEntity,
EntityProvider,
EntityProviderConnection,
} from '@backstage/plugin-catalog-node';
+import { EventBroker, EventParams } from '@backstage/plugin-events-node';
import { graphql } from '@octokit/graphql';
+import {
+ InstallationCreatedEvent,
+ InstallationEvent,
+ OrganizationEvent,
+ OrganizationMemberAddedEvent,
+ OrganizationMemberRemovedEvent,
+ MembershipEvent,
+ TeamCreatedEvent,
+ TeamDeletedEvent,
+ TeamEditedEvent,
+ TeamEvent,
+} from '@octokit/webhooks-types';
import { merge } from 'lodash';
import * as uuid from 'uuid';
import { Logger } from 'winston';
@@ -44,6 +60,7 @@ import {
assignGroupsToUsers,
buildOrgHierarchy,
defaultOrganizationTeamTransformer,
+ defaultUserTransformer,
getOrganizationTeams,
getOrganizationUsers,
GithubTeam,
@@ -55,6 +72,11 @@ import {
ANNOTATION_GITHUB_TEAM_SLUG,
ANNOTATION_GITHUB_USER_LOGIN,
} from '../lib/annotation';
+import {
+ getOrganizationsFromUser,
+ getOrganizationTeam,
+ getOrganizationTeamsFromUsers,
+} from '../lib/github';
import { splitTeamSlug } from '../lib/util';
/**
@@ -118,8 +140,18 @@ export interface GithubMultiOrgEntityProviderOptions {
* By default groups will be namespaced according to their GitHub org.
*/
teamTransformer?: TeamTransformer;
+
+ /**
+ * An EventBroker to subscribe this provider to GitHub events to trigger delta mutations
+ */
+ eventBroker?: EventBroker;
}
+type CreateDeltaOperation = (entities: Entity[]) => {
+ added: DeferredEntity[];
+ removed: DeferredEntity[];
+};
+
/**
* Ingests org data (users and groups) from GitHub.
*
@@ -161,6 +193,13 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
provider.schedule(options.schedule);
+ if (options.eventBroker) {
+ options.eventBroker.subscribe({
+ supportsEventTopics: provider.supportsEventTopics.bind(provider),
+ onEvent: provider.onEvent.bind(provider),
+ });
+ }
+
return provider;
}
@@ -271,6 +310,476 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
markCommitComplete();
}
+ private supportsEventTopics(): string[] {
+ return [
+ 'github.installation',
+ 'github.organization',
+ 'github.team',
+ 'github.membership',
+ ];
+ }
+
+ private async onEvent(params: EventParams): Promise {
+ const { logger } = this.options;
+ logger.debug(`Received event from ${params.topic}`);
+
+ const orgs = this.options.orgs?.length
+ ? this.options.orgs
+ : await this.getAllOrgs(this.options.gitHubConfig);
+
+ const eventPayload = params.eventPayload as
+ | InstallationEvent
+ | OrganizationEvent
+ | MembershipEvent
+ | TeamEvent;
+
+ if (
+ !orgs.includes(
+ (eventPayload as InstallationEvent).installation?.account?.login,
+ ) &&
+ !orgs.includes(
+ (eventPayload as OrganizationEvent | MembershipEvent | TeamEvent)
+ .organization?.login,
+ )
+ ) {
+ return;
+ }
+
+ // https://docs.github.com/webhooks-and-events/webhooks/webhook-events-and-payloads#installation
+ if (
+ params.topic.includes('installation') &&
+ eventPayload.action === 'created'
+ ) {
+ // We can only respond to installation.created events to add new users/groups since a
+ // installation.deleted event won't provide us info on what user/groups we should remove and
+ // we can't query the uninstalled org since we will no longer have access. This will need to be
+ // eventually resolved via occasional full mutation runs by calling read()
+ await this.onInstallationChange(
+ eventPayload as InstallationCreatedEvent,
+ orgs,
+ );
+ }
+
+ // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#organization
+ if (
+ params.topic.includes('organization') &&
+ (eventPayload.action === 'member_added' ||
+ eventPayload.action === 'member_removed')
+ ) {
+ await this.onMemberChangeInOrganization(eventPayload, orgs);
+ }
+
+ // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#team
+ if (params.topic.includes('team')) {
+ if (
+ eventPayload.action === 'created' ||
+ eventPayload.action === 'deleted'
+ ) {
+ await this.onTeamChangeInOrganization(
+ eventPayload as TeamCreatedEvent | TeamDeletedEvent,
+ );
+ } else if (eventPayload.action === 'edited') {
+ await this.onTeamEditedInOrganization(
+ eventPayload as TeamEditedEvent,
+ orgs,
+ );
+ }
+ }
+
+ // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
+ if (params.topic.includes('membership')) {
+ this.onMembershipChangedInTeam(eventPayload as MembershipEvent, orgs);
+ }
+
+ return;
+ }
+
+ private async onInstallationChange(
+ event: InstallationCreatedEvent,
+ applicableOrgs: string[],
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ const org = event.installation.account.login;
+ const { headers, type: tokenType } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const { users } = await getOrganizationUsers(
+ client,
+ org,
+ tokenType,
+ this.options.userTransformer,
+ );
+
+ const { groups } = await getOrganizationTeams(
+ client,
+ org,
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ // Fetch group memberships of users in case they already exist and
+ // have memberships in groups from other applicable orgs
+ for (const userOrg of applicableOrgs) {
+ const { headers: orgHeaders } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${userOrg}`,
+ });
+ const orgClient = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers: orgHeaders,
+ });
+
+ const { groups: userGroups } = await getOrganizationTeamsFromUsers(
+ orgClient,
+ userOrg,
+ users.map(
+ u =>
+ u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] ||
+ u.metadata.name,
+ ),
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ assignGroupsToUsers(users, userGroups);
+ }
+
+ const { added, removed } = this.createAddEntitiesOperation([
+ ...users,
+ ...groups,
+ ]);
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
+ private async onMemberChangeInOrganization(
+ event: OrganizationMemberAddedEvent | OrganizationMemberRemovedEvent,
+ applicableOrgs: string[],
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ const userTransformer =
+ this.options.userTransformer || defaultUserTransformer;
+ const { name, avatar_url: avatarUrl, email, login } = event.membership.user;
+ const org = event.organization.login;
+ const { headers } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const { orgs } = await getOrganizationsFromUser(client, login);
+ const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o));
+
+ let updateMemberships: boolean;
+ let createDeltaOperation: CreateDeltaOperation;
+ if (event.action === 'member_removed') {
+ if (userApplicableOrgs.length) {
+ // If the user is still associated with another applicable org then we don't want to remove
+ // them, just update the entity to remove any potential group memberships from the old org
+ createDeltaOperation = this.createAddEntitiesOperation.bind(this);
+ updateMemberships = true;
+ } else {
+ // User is no longer part of any applicable orgs so we can remove it,
+ // no need to take memberships into account
+ createDeltaOperation = this.createRemoveEntitiesOperation.bind(this);
+ updateMemberships = false;
+ }
+ } else {
+ // We're not sure if this user was already added as part of another applicable org
+ // so grab the latest memberships (potentially from teams of other orgs) to ensure
+ // we're not accidentally omitting them
+ createDeltaOperation = this.createAddEntitiesOperation.bind(this);
+ updateMemberships = true;
+ }
+
+ const user = (await userTransformer(
+ {
+ name,
+ avatarUrl,
+ login,
+ email: email ?? undefined,
+ },
+ {
+ org,
+ client,
+ query: '',
+ },
+ )) as UserEntity;
+
+ if (updateMemberships) {
+ for (const userOrg of userApplicableOrgs) {
+ const { headers: orgHeaders } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${userOrg}`,
+ });
+ const orgClient = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers: orgHeaders,
+ });
+
+ const { groups } = await getOrganizationTeamsFromUsers(
+ orgClient,
+ userOrg,
+ [login],
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ assignGroupsToUsers([user], groups);
+ }
+ }
+
+ const { added, removed } = createDeltaOperation([user]);
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
+ private async onTeamChangeInOrganization(
+ event: TeamCreatedEvent | TeamDeletedEvent,
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ const org = event.organization.login;
+ const { headers } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const { name, html_url: url, description, slug } = event.team;
+ const group = (await this.defaultMultiOrgTeamTransformer(
+ {
+ name,
+ slug,
+ editTeamUrl: `${url}/edit`,
+ combinedSlug: `${org}/${slug}`,
+ description: description ?? undefined,
+ parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam,
+ // entity will be removed or is new
+ members: [],
+ },
+ {
+ org,
+ client,
+ query: '',
+ },
+ )) as Entity;
+
+ const createDeltaOperation =
+ event.action === 'created'
+ ? this.createAddEntitiesOperation.bind(this)
+ : this.createRemoveEntitiesOperation.bind(this);
+ const { added, removed } = createDeltaOperation([group]);
+
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
+ private async onTeamEditedInOrganization(
+ event: TeamEditedEvent,
+ applicableOrgs: string[],
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ const org = event.organization.login;
+ const { headers, type: tokenType } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const teamSlug = event.team.slug;
+ const { group } = await getOrganizationTeam(
+ client,
+ org,
+ teamSlug,
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ const { users } = await getOrganizationUsers(
+ client,
+ org,
+ tokenType,
+ this.options.userTransformer,
+ );
+
+ const usersFromChangedGroup =
+ group.spec.members?.map(m =>
+ stringifyEntityRef(parseEntityRef(m, { defaultKind: 'user' })),
+ ) || [];
+ const usersToRebuild = users.filter(u =>
+ usersFromChangedGroup.includes(stringifyEntityRef(u)),
+ );
+
+ // Update memberships of associated members of this group in case the group entity ref changed
+ for (const userOrg of applicableOrgs) {
+ const { headers: orgHeaders } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${userOrg}`,
+ });
+ const orgClient = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers: orgHeaders,
+ });
+
+ const { groups } = await getOrganizationTeamsFromUsers(
+ orgClient,
+ userOrg,
+ usersToRebuild.map(
+ u =>
+ u.metadata.annotations?.[ANNOTATION_GITHUB_USER_LOGIN] ||
+ u.metadata.name,
+ ),
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ assignGroupsToUsers(usersToRebuild, groups);
+ }
+
+ const oldName = event.changes.name?.from || '';
+ const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-');
+ const oldGroup = (await this.defaultMultiOrgTeamTransformer(
+ {
+ name: event.changes.name?.from,
+ slug: oldSlug,
+ combinedSlug: `${org}/${oldSlug}`,
+ description: event.changes.description?.from,
+ parentTeam: { slug: event.team?.parent?.slug || '' } as GithubTeam,
+ // entity will be removed
+ members: [],
+ },
+ {
+ org,
+ client,
+ query: '',
+ },
+ )) as Entity;
+
+ // Remove the old group entity in case the entity ref is now different
+ const { removed } = this.createRemoveEntitiesOperation([oldGroup]);
+ const { added } = this.createAddEntitiesOperation([
+ ...usersToRebuild,
+ group,
+ ]);
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
+ private async onMembershipChangedInTeam(
+ event: MembershipEvent,
+ applicableOrgs: string[],
+ ) {
+ if (!this.connection) {
+ throw new Error('Not initialized');
+ }
+
+ // The docs are saying I will receive the slug for the removed event,
+ // but the types don't reflect that,
+ // so I will just check to be sure the slug is there
+ // https://docs.github.com/en/developers/webhooks-and-events/webhooks/webhook-events-and-payloads#membership
+ if (!('slug' in event.team)) {
+ return;
+ }
+
+ const org = event.organization.login;
+ const { headers } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${org}`,
+ });
+ const client = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers,
+ });
+
+ const teamSlug = event.team.slug;
+ const { group } = await getOrganizationTeam(
+ client,
+ org,
+ teamSlug,
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ const userTransformer =
+ this.options.userTransformer || defaultUserTransformer;
+ const { name, avatar_url: avatarUrl, email, login } = event.member;
+ const user = (await userTransformer(
+ {
+ name,
+ avatarUrl,
+ login,
+ email: email ?? undefined,
+ },
+ {
+ org,
+ client,
+ query: '',
+ },
+ )) as UserEntity;
+
+ const { orgs } = await getOrganizationsFromUser(client, login);
+ const userApplicableOrgs = orgs.filter(o => applicableOrgs.includes(o));
+ for (const userOrg of userApplicableOrgs) {
+ const { headers: orgHeaders } =
+ await this.options.githubCredentialsProvider.getCredentials({
+ url: `${this.options.githubUrl}/${userOrg}`,
+ });
+ const orgClient = graphql.defaults({
+ baseUrl: this.options.gitHubConfig.apiBaseUrl,
+ headers: orgHeaders,
+ });
+
+ const { groups } = await getOrganizationTeamsFromUsers(
+ orgClient,
+ userOrg,
+ [login],
+ this.defaultMultiOrgTeamTransformer.bind(this),
+ );
+
+ assignGroupsToUsers([user], groups);
+ }
+
+ const { added, removed } = this.createAddEntitiesOperation([user, group]);
+ await this.connection.applyMutation({
+ type: 'delta',
+ removed,
+ added,
+ });
+ }
+
private schedule(schedule: GithubMultiOrgEntityProviderOptions['schedule']) {
if (!schedule || schedule === 'manual') {
return;
@@ -301,9 +810,11 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
team: GithubTeam,
ctx: TransformerContext,
): Promise {
- const result = this.options.teamTransformer
- ? await this.options.teamTransformer(team, ctx)
- : await defaultOrganizationTeamTransformer(team, ctx);
+ if (this.options.teamTransformer) {
+ return await this.options.teamTransformer(team, ctx);
+ }
+
+ const result = await defaultOrganizationTeamTransformer(team, ctx);
if (result) {
result.metadata.namespace = ctx.org.toLocaleLowerCase('en-US');
@@ -333,6 +844,32 @@ export class GithubMultiOrgEntityProvider implements EntityProvider {
)
.filter(Boolean) as string[];
}
+
+ private createAddEntitiesOperation(entities: Entity[]) {
+ return {
+ removed: [],
+ added: entities.map(entity => ({
+ locationKey: `github-multi-org-provider:${this.options.id}`,
+ entity: withLocations(
+ `https://${this.options.gitHubConfig.host}`,
+ entity,
+ ),
+ })),
+ };
+ }
+
+ private createRemoveEntitiesOperation(entities: Entity[]) {
+ return {
+ added: [],
+ removed: entities.map(entity => ({
+ locationKey: `github-multi-org-provider:${this.options.id}`,
+ entity: withLocations(
+ `https://${this.options.gitHubConfig.host}`,
+ entity,
+ ),
+ })),
+ };
+ }
}
// Helps wrap the timing and logging behaviors
diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts
index ff42e42718..3e58c1473f 100644
--- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts
+++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.test.ts
@@ -833,6 +833,7 @@ describe('GithubOrgEntityProvider', () => {
'url:https://github.com/orgs/backstage/teams/mygroup-with-spaces',
},
name: 'mygroup-with-spaces',
+ description: 'description-from-the-new-team',
},
apiVersion: 'backstage.io/v1alpha1',
kind: 'Group',
diff --git a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts
index 02e225c938..c181510a67 100644
--- a/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts
+++ b/plugins/catalog-backend-module-github/src/providers/GithubOrgEntityProvider.ts
@@ -371,14 +371,21 @@ export class GithubOrgEntityProvider
assignGroupsToUsers(usersToRebuild, groups);
buildOrgHierarchy(groups);
- const oldName = event.changes.name?.from || '';
+ const oldName = event.changes.name?.from || event.team.name;
const oldSlug = oldName.toLowerCase().replaceAll(/\s/gi, '-');
+ const oldDescription =
+ event.changes.description?.from || event.team.description;
+ const oldDescriptionSlug = oldDescription
+ ?.toLowerCase()
+ .replaceAll(/\s/gi, '-');
+
const { removed } = createDeltaOperation(org, [
{
...group,
metadata: {
name: oldSlug,
+ description: oldDescriptionSlug,
},
},
]);
diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md
index 9882178a2b..c7fdb3e600 100644
--- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md
+++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md
@@ -1,5 +1,36 @@
# @backstage/plugin-catalog-backend-module-gitlab
+## 0.2.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 0.2.1
+
+### Patch Changes
+
+- b12c41fafc4: Fix a corner case where returned users are null for bots
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.2.1-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json
index 22237862aa..31f67c8b28 100644
--- a/plugins/catalog-backend-module-gitlab/package.json
+++ b/plugins/catalog-backend-module-gitlab/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-gitlab",
"description": "A Backstage catalog backend module that helps integrate towards GitLab",
- "version": "0.2.1-next.1",
+ "version": "0.2.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts
index 8f8c67895c..46932c95c6 100644
--- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts
+++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts
@@ -128,10 +128,11 @@ export class GitLabClient {
throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
}
memberIds.push(
- ...response.data.group.groupMembers.nodes.map(
- (node: { user: { id: string } }) =>
+ ...response.data.group.groupMembers.nodes
+ .filter(n => n.user)
+ .map(node =>
Number(node.user.id.replace(/^gid:\/\/gitlab\/User\//, '')),
- ),
+ ),
);
({ hasNextPage, endCursor } = response.data.group.groupMembers.pageInfo);
} while (hasNextPage);
diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
index 9921b0dabc..887a38f1fc 100644
--- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
+++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
@@ -1,5 +1,45 @@
# @backstage/plugin-catalog-backend-module-incremental-ingestion
+## 0.3.3-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.2-next.0
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-events-node@0.2.7-next.0
+ - @backstage/plugin-permission-common@0.7.5
+
+## 0.3.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1
+ - @backstage/backend-common@0.18.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-events-node@0.2.6
+ - @backstage/plugin-permission-common@0.7.5
+
+## 0.3.2-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
## 0.3.2-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json
index 686c89cd25..d34042afd4 100644
--- a/plugins/catalog-backend-module-incremental-ingestion/package.json
+++ b/plugins/catalog-backend-module-incremental-ingestion/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-incremental-ingestion",
"description": "An entity provider for streaming large asset sources into the catalog",
- "version": "0.3.2-next.1",
+ "version": "0.3.3-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md
index 104e3f1c0f..540143a4d6 100644
--- a/plugins/catalog-backend-module-ldap/CHANGELOG.md
+++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md
@@ -1,5 +1,31 @@
# @backstage/plugin-catalog-backend-module-ldap
+## 0.5.13-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 0.5.12
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
## 0.5.12-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json
index 120131d95a..b931f9b752 100644
--- a/plugins/catalog-backend-module-ldap/package.json
+++ b/plugins/catalog-backend-module-ldap/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-ldap",
"description": "A Backstage catalog backend module that helps integrate towards LDAP",
- "version": "0.5.12-next.1",
+ "version": "0.5.13-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md
index c0c0888f38..dfd79f3023 100644
--- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md
+++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md
@@ -1,5 +1,31 @@
# @backstage/plugin-catalog-backend-module-msgraph
+## 0.5.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 0.5.4
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-catalog-common@1.0.13
+
## 0.5.4-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json
index 026386457a..120c4a46b8 100644
--- a/plugins/catalog-backend-module-msgraph/package.json
+++ b/plugins/catalog-backend-module-msgraph/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-msgraph",
"description": "A Backstage catalog backend module that helps integrate towards Microsoft Graph",
- "version": "0.5.4-next.1",
+ "version": "0.5.5-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md
index 5fc587a4f9..f35874880d 100644
--- a/plugins/catalog-backend-module-openapi/CHANGELOG.md
+++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md
@@ -1,5 +1,43 @@
# @backstage/plugin-catalog-backend-module-openapi
+## 0.1.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.2-next.0
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.3.0
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 0.1.11
+
+### Patch Changes
+
+- accaceadffa: Fixed bug in `jsonSchemaRefPlaceholderResolver` where relative $ref files were resolved through file system instead of base URL of file
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 0.1.11-next.2
+
+### Patch Changes
+
+- accaceadffa: Fixed bug in `jsonSchemaRefPlaceholderResolver` where relative $ref files were resolved through file system instead of base URL of file
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.9.1-next.2
+ - @backstage/config@1.0.7
+
## 0.1.11-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json
index e02490ef59..66ac0df179 100644
--- a/plugins/catalog-backend-module-openapi/package.json
+++ b/plugins/catalog-backend-module-openapi/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-openapi",
"description": "A Backstage catalog backend module that helps with OpenAPI specifications",
- "version": "0.1.11-next.1",
+ "version": "0.1.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts
index 64c772b91e..59f7e5791f 100644
--- a/plugins/catalog-backend-module-openapi/src/lib/bundle.ts
+++ b/plugins/catalog-backend-module-openapi/src/lib/bundle.ts
@@ -65,6 +65,6 @@ export async function bundleFileWithRefs(
},
};
const fileObject = parse(fileWithRefs);
- const bundledObject = await $RefParser.bundle(fileObject, options);
+ const bundledObject = await $RefParser.bundle(baseUrl, fileObject, options);
return stringify(bundledObject);
}
diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
index 39bd1e3cdc..0ae5333d51 100644
--- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
+++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
@@ -1,5 +1,42 @@
# @backstage/plugin-catalog-backend-module-puppetdb
+## 0.1.3-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 0.1.2
+
+### Patch Changes
+
+- 95b2168d71b: Fixes import paths and updates documentation
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 0.1.2-next.2
+
+### Patch Changes
+
+- 95b2168d71b: Fixes import paths and updates documentation
+- Updated dependencies
+ - @backstage/config@1.0.7
+
## 0.1.2-next.1
### Patch Changes
diff --git a/plugins/catalog-backend-module-puppetdb/README.md b/plugins/catalog-backend-module-puppetdb/README.md
index d4782c6d18..9319299a06 100644
--- a/plugins/catalog-backend-module-puppetdb/README.md
+++ b/plugins/catalog-backend-module-puppetdb/README.md
@@ -31,9 +31,9 @@ Update the catalog plugin initialization in your backend to add the provider and
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 10 },
+ timeout: { minutes: 50 },
-+ initialDelay: { seconds: 15}
++ initialDelay: { seconds: 15 }
+ }),
-+ });
++ })
+ );
```
@@ -67,7 +67,7 @@ of overriding the default transformer.
export const customResourceTransformer: ResourceTransformer = async (
node,
config,
-): Promise => {
+): Promise => {
// Transformations may change namespace, owner, change entity naming pattern, add labels, annotations, etc.
// Create the Resource Entity on your own, or wrap the default transformer
@@ -77,14 +77,16 @@ export const customResourceTransformer: ResourceTransformer = async (
2. Configure the provider with the transformer:
-```ts
-const puppetDbEntityProvider = PuppetDbEntityProvider.fromConfig(env.config, {
- logger: env.logger,
- schedule: env.scheduler.createScheduledTaskRunner({
- frequency: { minutes: 10 },
- timeout: { minutes: 50 },
- initialDelay: { seconds: 15 },
- }),
- transformer: customResourceTransformer,
-});
+```diff
+ builder.addEntityProvider(
+ PuppetDbEntityProvider.fromConfig(env.config, {
+ logger: env.logger,
++ transformer: customResourceTransformer,
+ schedule: env.scheduler.createScheduledTaskRunner({
+ frequency: { minutes: 10 },
+ timeout: { minutes: 50 },
+ initialDelay: { seconds: 15 }
+ }),
+ })
+ );
```
diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json
index 65bda327dd..2a16f0c46d 100644
--- a/plugins/catalog-backend-module-puppetdb/package.json
+++ b/plugins/catalog-backend-module-puppetdb/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-puppetdb",
"description": "A Backstage catalog backend module that helps integrate towards PuppetDB",
- "version": "0.1.2-next.1",
+ "version": "0.1.3-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -65,7 +65,7 @@
"@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/lodash": "^4.14.151",
- "msw": "^0.49.0"
+ "msw": "^1.0.0"
},
"files": [
"dist",
diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts
index ad89e1fd9a..42091fd6d6 100644
--- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts
+++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.test.ts
@@ -28,7 +28,7 @@ import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
ResourceEntity,
-} from '@backstage/catalog-model/';
+} from '@backstage/catalog-model';
import { DEFAULT_ENTITY_OWNER, ENDPOINT_NODES } from '../puppet/constants';
jest.mock('../puppet/read', () => {
diff --git a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts
index 8c63baf467..a5286a9d70 100644
--- a/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts
+++ b/plugins/catalog-backend-module-puppetdb/src/providers/PuppetDbEntityProvider.ts
@@ -31,7 +31,7 @@ import {
ANNOTATION_LOCATION,
ANNOTATION_ORIGIN_LOCATION,
Entity,
-} from '@backstage/catalog-model/';
+} from '@backstage/catalog-model';
import { merge } from 'lodash';
import { readPuppetNodes } from '../puppet/read';
import { ENDPOINT_NODES } from '../puppet/constants';
diff --git a/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts b/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts
index 568ea7224e..ab3e85370a 100644
--- a/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts
+++ b/plugins/catalog-backend-module-puppetdb/src/puppet/read.ts
@@ -16,7 +16,7 @@
import { PuppetDbEntityProviderConfig } from '../providers';
import { PuppetNode, ResourceTransformer } from './types';
-import { ResourceEntity } from '@backstage/catalog-model/';
+import { ResourceEntity } from '@backstage/catalog-model';
import { defaultResourceTransformer } from './transformers';
import fetch from 'node-fetch';
import { ResponseError } from '@backstage/errors';
diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md
index 1a715e26bc..d97663c66f 100644
--- a/plugins/catalog-backend/CHANGELOG.md
+++ b/plugins/catalog-backend/CHANGELOG.md
@@ -1,5 +1,68 @@
# @backstage/plugin-catalog-backend
+## 1.9.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/plugin-catalog-node@1.3.7-next.0
+ - @backstage/plugin-search-backend-module-catalog@0.1.2-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/backend-tasks@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-permission-common@0.7.5
+ - @backstage/plugin-permission-node@0.7.9-next.0
+ - @backstage/plugin-scaffolder-common@1.3.0
+ - @backstage/plugin-search-common@1.2.3
+
+## 1.9.1
+
+### Patch Changes
+
+- ce8d203235b: Ensure that entity cache state is only written to the database when actually changed
+- 485a6c5f7b5: Internal refactoring for performance in the service handlers
+- 3587a968dcd: Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests.
+- ce335df9d1c: Improve the query for orphan pruning
+- 27956d78671: Adjusted the OpenAPI schema file name according to the new structure
+- 51064e6e5ee: Change orphan cleanup task to only log a message if it deleted entities.
+- 12a345317ab: Remove unnecessary join in the entity facets endpoint, exclude nulls
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/plugin-scaffolder-common@1.3.0
+ - @backstage/plugin-permission-node@0.7.8
+ - @backstage/backend-tasks@0.5.2
+ - @backstage/plugin-catalog-node@1.3.6
+ - @backstage/plugin-search-backend-module-catalog@0.1.1
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-permission-common@0.7.5
+ - @backstage/plugin-search-common@1.2.3
+
+## 1.9.1-next.2
+
+### Patch Changes
+
+- ce8d203235b: Ensure that entity cache state is only written to the database when actually changed
+- 485a6c5f7b5: Internal refactoring for performance in the service handlers
+- 3587a968dcd: Fixed a bug in the `queryEntities` endpoint that was causing filtered entities to be included in cursor requests.
+- 12a345317ab: Remove unnecessary join in the entity facets endpoint, exclude nulls
+- Updated dependencies
+ - @backstage/plugin-scaffolder-common@1.3.0-next.0
+ - @backstage/config@1.0.7
+
## 1.9.1-next.1
### Patch Changes
diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json
index 289a9a801d..b22541bda1 100644
--- a/plugins/catalog-backend/package.json
+++ b/plugins/catalog-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend",
"description": "The Backstage backend plugin that provides the Backstage catalog",
- "version": "1.9.1-next.1",
+ "version": "1.9.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts b/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts
index 920acae7bd..eeab85f59e 100644
--- a/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts
+++ b/plugins/catalog-backend/src/database/operations/util/deleteOrphanedEntities.ts
@@ -33,13 +33,16 @@ export async function deleteOrphanedEntities(options: {
// Limit iterations for sanity
for (let i = 0; i < 100; ++i) {
const candidates = await tx
- .with('orphans', orphans =>
+ .with('orphans', ['entity_id', 'entity_ref'], orphans =>
orphans
- .from('refresh_state')
- .select('entity_id', 'entity_ref')
- .whereNotIn('entity_ref', keep =>
- keep.distinct('target_entity_ref').from('refresh_state_references'),
- ),
+ .from('refresh_state')
+ .select('refresh_state.entity_id', 'refresh_state.entity_ref')
+ .leftOuterJoin(
+ 'refresh_state_references',
+ 'refresh_state_references.target_entity_ref',
+ 'refresh_state.entity_ref',
+ )
+ .whereNull('refresh_state_references.target_entity_ref'),
)
.select({
entityId: 'orphans.entity_id',
diff --git a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts
index e89d53f79f..8e2f8b2f6f 100644
--- a/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts
+++ b/plugins/catalog-backend/src/processing/DefaultCatalogProcessingEngine.ts
@@ -317,7 +317,9 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine {
try {
await this.processingDatabase.transaction(async tx => {
const n = await this.processingDatabase.deleteOrphanedEntities(tx);
- this.logger.info(`Deleted ${n} orphaned entities`);
+ if (n > 0) {
+ this.logger.info(`Deleted ${n} orphaned entities`);
+ }
});
} catch (error) {
this.logger.warn(`Failed to delete orphaned entities`, error);
diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
index 93a37fb04a..4f011813e6 100644
--- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
+++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
@@ -1729,6 +1729,17 @@ describe('DefaultEntitiesCatalog', () => {
],
},
});
+
+ await expect(
+ catalog.facets({
+ facets: ['kind'],
+ filter: { not: { key: 'metadata.name', values: ['two'] } },
+ }),
+ ).resolves.toEqual({
+ facets: {
+ kind: [{ value: 'k', count: 1 }],
+ },
+ });
},
);
diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
index 5fca950cac..c328c83823 100644
--- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
+++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
@@ -64,43 +64,47 @@ const defaultSortField: EntityOrder = {
const DEFAULT_LIMIT = 20;
-function parsePagination(input?: EntityPagination): {
- limit?: number;
- offset?: number;
-} {
+function parsePagination(input?: EntityPagination): EntityPagination {
if (!input) {
return {};
}
let { limit, offset } = input;
- if (input.after !== undefined) {
- let cursor;
- try {
- const json = Buffer.from(input.after, 'base64').toString('utf8');
- cursor = JSON.parse(json);
- } catch {
- throw new InputError('Malformed after cursor, could not be parsed');
+ if (input.after === undefined) {
+ return { limit, offset };
+ }
+
+ let cursor;
+ try {
+ const json = Buffer.from(input.after, 'base64').toString('utf8');
+ cursor = JSON.parse(json);
+ } catch {
+ throw new InputError('Malformed after cursor, could not be parsed');
+ }
+
+ if (cursor.limit !== undefined) {
+ if (!Number.isInteger(cursor.limit)) {
+ throw new InputError('Malformed after cursor, limit was not an number');
}
- if (cursor.limit !== undefined) {
- if (!Number.isInteger(cursor.limit)) {
- throw new InputError('Malformed after cursor, limit was not an number');
- }
- limit = cursor.limit;
- }
- if (cursor.offset !== undefined) {
- if (!Number.isInteger(cursor.offset)) {
- throw new InputError('Malformed after cursor, offset was not a number');
- }
- offset = cursor.offset;
+ limit = cursor.limit;
+ }
+
+ if (cursor.offset !== undefined) {
+ if (!Number.isInteger(cursor.offset)) {
+ throw new InputError('Malformed after cursor, offset was not a number');
}
+ offset = cursor.offset;
}
return { limit, offset };
}
-function stringifyPagination(input: { limit: number; offset: number }) {
- const json = JSON.stringify({ limit: input.limit, offset: input.offset });
+function stringifyPagination(
+ input: Required>,
+): string {
+ const { limit, offset } = input;
+ const json = JSON.stringify({ limit, offset });
const base64 = Buffer.from(json, 'utf8').toString('base64');
return base64;
}
@@ -111,24 +115,21 @@ function addCondition(
filter: EntitiesSearchFilter,
negate: boolean = false,
entityIdField = 'entity_id',
-) {
+): void {
+ const key = filter.key.toLowerCase();
+ const values = filter.values?.map(v => v.toLowerCase());
+
// NOTE(freben): This used to be a set of OUTER JOIN, which may seem to
// make a lot of sense. However, it had abysmal performance on sqlite
// when datasets grew large, so we're using IN instead.
const matchQuery = db('search')
.select('search.entity_id')
- .where({ key: filter.key.toLowerCase() })
+ .where({ key })
.andWhere(function keyFilter() {
- if (filter.values) {
- if (filter.values.length === 1) {
- this.where({ value: filter.values[0].toLowerCase() });
- } else {
- this.andWhere(
- 'value',
- 'in',
- filter.values.map(v => v.toLowerCase()),
- );
- }
+ if (values?.length === 1) {
+ this.where({ value: values.at(0) });
+ } else if (values) {
+ this.andWhere('value', 'in', values);
}
});
queryBuilder.andWhere(entityIdField, negate ? 'not in' : 'in', matchQuery);
@@ -159,16 +160,16 @@ function parseFilter(
negate: boolean = false,
entityIdField = 'entity_id',
): Knex.QueryBuilder {
+ if (isNegationEntityFilter(filter)) {
+ return parseFilter(filter.not, query, db, !negate, entityIdField);
+ }
+
if (isEntitiesSearchFilter(filter)) {
return query.andWhere(function filterFunction() {
addCondition(this, db, filter, negate, entityIdField);
});
}
- if (isNegationEntityFilter(filter)) {
- return parseFilter(filter.not, query, db, !negate, entityIdField);
- }
-
return query[negate ? 'andWhereNot' : 'andWhere'](function filterFunction() {
if (isOrEntityFilter(filter)) {
for (const subFilter of filter.anyOf ?? []) {
@@ -678,10 +679,9 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog {
for (const facet of request.facets) {
const dbQuery = db('search')
- .join('final_entities', 'search.entity_id', 'final_entities.entity_id')
.where('search.key', facet.toLocaleLowerCase('en-US'))
- .count('search.entity_id as count')
- .select({ value: 'search.original_value' })
+ .whereNotNull('search.original_value')
+ .select({ value: 'search.original_value', count: db.raw('count(*)') })
.groupBy('search.original_value');
if (request?.filter) {
diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md
index 138edaee90..7b6a839605 100644
--- a/plugins/catalog-customized/CHANGELOG.md
+++ b/plugins/catalog-customized/CHANGELOG.md
@@ -1,5 +1,29 @@
# @internal/plugin-catalog-customized
+## 0.0.11-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/plugin-catalog@1.11.1-next.0
+
+## 0.0.10
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/plugin-catalog@1.11.0
+
+## 0.0.10-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog@1.11.0-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+
## 0.0.10-next.1
### Patch Changes
diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json
index 808f7a40e4..eb5e1ded05 100644
--- a/plugins/catalog-customized/package.json
+++ b/plugins/catalog-customized/package.json
@@ -1,7 +1,7 @@
{
"name": "@internal/plugin-catalog-customized",
"description": "The internal Backstage Customizable plugin for browsing the Backstage catalog",
- "version": "0.0.10-next.1",
+ "version": "0.0.11-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-graph/CHANGELOG.md b/plugins/catalog-graph/CHANGELOG.md
index eabfdaff12..36398fd099 100644
--- a/plugins/catalog-graph/CHANGELOG.md
+++ b/plugins/catalog-graph/CHANGELOG.md
@@ -1,5 +1,40 @@
# @backstage/plugin-catalog-graph
+## 0.2.31-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.2.30
+
+### Patch Changes
+
+- d446f8fb0a8: Expose all `EntityRelationsGraphProps` to Catalog Graph Page
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.2.30-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.30-next.1
### Patch Changes
diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json
index b9ac5ea1c1..510bc13b9c 100644
--- a/plugins/catalog-graph/package.json
+++ b/plugins/catalog-graph/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-catalog-graph",
- "version": "0.2.30-next.1",
+ "version": "0.2.31-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx
index 85d9d97c7f..242bed6311 100644
--- a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx
+++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { render, waitFor, screen } from '@testing-library/react';
+import { render, waitFor, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { CurveFilter } from './CurveFilter';
@@ -33,7 +33,9 @@ describe('', () => {
expect(screen.getByText('Step Before')).toBeInTheDocument();
- await userEvent.click(screen.getByTestId('select'));
+ await userEvent.click(
+ within(screen.getByTestId('select')).getByRole('button'),
+ );
await userEvent.click(screen.getByText('Monotone X'));
await waitFor(() => {
diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx
index cb660b5a25..dc74218316 100644
--- a/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx
+++ b/plugins/catalog-graph/src/components/CatalogGraphPage/DirectionFilter.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { render, waitFor, screen } from '@testing-library/react';
+import { render, waitFor, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { Direction } from '../EntityRelationsGraph';
@@ -37,7 +37,9 @@ describe('', () => {
expect(screen.getByText('Right to left')).toBeInTheDocument();
- await userEvent.click(screen.getByTestId('select'));
+ await userEvent.click(
+ within(screen.getByTestId('select')).getByRole('button'),
+ );
await userEvent.click(screen.getByText('Top to bottom'));
await waitFor(() => {
diff --git a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx
index 6592c6195e..cc47c8850b 100644
--- a/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx
+++ b/plugins/catalog-graph/src/components/EntityRelationsGraph/EntityKindIcon.test.tsx
@@ -24,7 +24,7 @@ describe('', () => {
,
);
- expect(baseElement.querySelector('.MuiSvgIcon-root')).toBeInTheDocument();
+ expect(baseElement.querySelector('svg')).toBeInTheDocument();
});
it('renders without exploding for unknown kind', async () => {
@@ -32,6 +32,6 @@ describe('', () => {
,
);
- expect(baseElement.querySelector('.MuiSvgIcon-root')).toBeInTheDocument();
+ expect(baseElement.querySelector('svg')).toBeInTheDocument();
});
});
diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md
index 738a28f3af..7c9646a22e 100644
--- a/plugins/catalog-import/CHANGELOG.md
+++ b/plugins/catalog-import/CHANGELOG.md
@@ -1,5 +1,49 @@
# @backstage/plugin-catalog-import
+## 0.9.9-next.0
+
+### Patch Changes
+
+- 2ef84c05aee7: Added analytics event for import entity button
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/integration-react@1.1.14-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 0.9.8
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/integration@1.4.5
+ - @backstage/core-components@0.13.1
+ - @backstage/integration-react@1.1.13
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 0.9.8-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.9.8-next.1
### Patch Changes
diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json
index 7e7113a16c..8fc04f95f4 100644
--- a/plugins/catalog-import/package.json
+++ b/plugins/catalog-import/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-import",
"description": "A Backstage plugin the helps you import entities into your catalog",
- "version": "0.9.8-next.1",
+ "version": "0.9.9-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx
index 7943b6e3b8..74ebd973d0 100644
--- a/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx
+++ b/plugins/catalog-import/src/components/StepReviewLocation/StepReviewLocation.tsx
@@ -22,7 +22,7 @@ import { BackButton, NextButton } from '../Buttons';
import { EntityListComponent } from '../EntityListComponent';
import { PrepareResult, ReviewResult } from '../useImportState';
-import { configApiRef, useApi } from '@backstage/core-plugin-api';
+import { configApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api';
import { Link } from '@backstage/core-components';
import { stringifyEntityRef } from '@backstage/catalog-model';
import { assertError } from '@backstage/errors';
@@ -40,6 +40,7 @@ export const StepReviewLocation = ({
}: Props) => {
const catalogApi = useApi(catalogApiRef);
const configApi = useApi(configApiRef);
+ const analytics = useAnalytics();
const appTitle = configApi.getOptional('app.title') || 'Backstage';
@@ -52,6 +53,7 @@ export const StepReviewLocation = ({
: false;
const handleClick = useCallback(async () => {
setSubmitted(true);
+ analytics.captureEvent('click', 'import entity');
try {
let refreshed = new Array<{ target: string }>();
if (prepareResult.type === 'locations') {
@@ -108,7 +110,7 @@ export const StepReviewLocation = ({
setSubmitted(false);
}
}
- }, [prepareResult, onReview, catalogApi]);
+ }, [prepareResult, onReview, catalogApi, analytics]);
return (
<>
diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md
index a1ca2c37c6..903088747e 100644
--- a/plugins/catalog-node/CHANGELOG.md
+++ b/plugins/catalog-node/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/plugin-catalog-node
+## 1.3.7-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/backend-plugin-api@0.5.3-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
+## 1.3.6
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.5.2
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+
## 1.3.6-next.1
### Patch Changes
diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json
index 95136c2da1..ec47249a1c 100644
--- a/plugins/catalog-node/package.json
+++ b/plugins/catalog-node/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-node",
"description": "The plugin-catalog-node module for @backstage/plugin-catalog-backend",
- "version": "1.3.6-next.1",
+ "version": "1.3.7-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-react/CHANGELOG.md b/plugins/catalog-react/CHANGELOG.md
index bd9d13ace5..2a267bfd1d 100644
--- a/plugins/catalog-react/CHANGELOG.md
+++ b/plugins/catalog-react/CHANGELOG.md
@@ -1,5 +1,81 @@
# @backstage/plugin-catalog-react
+## 1.7.0-next.0
+
+### Minor Changes
+
+- cb4c15989b6b: The `EntityOwnerPicker` component has undergone improvements to enhance its performance.
+
+ The component now loads entities asynchronously, resulting in improved performance and responsiveness. Instead of loading all entities upfront, they are now loaded in batches as the user scrolls.
+ The previous implementation inferred users and groups displayed by the `EntityOwnerPicker` component based on the entities available in the `EntityListContext`. The updated version no longer relies on the `EntityListContext` for inference, allowing for better decoupling and improved performance.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/version-bridge@1.0.4
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-permission-common@0.7.5
+
+## 1.6.0
+
+### Minor Changes
+
+- 2258dcae970: Added an entity namespace filter and column on the default catalog page.
+
+ If you have a custom version of the catalog page, you can add this filter in your CatalogPage code:
+
+ ```ts
+
+
+
+
+
+ /* if you want namespace picker */
+
+
+
+
+
+
+ ```
+
+ The namespace column can be added using `createNamespaceColumn();`. This is only needed if you customized the columns for CatalogTable.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/integration@1.4.5
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/version-bridge@1.0.4
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-permission-common@0.7.5
+ - @backstage/plugin-permission-react@0.4.12
+
+## 1.6.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+
## 1.6.0-next.1
### Patch Changes
diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md
index f9725e41e8..90bddecd71 100644
--- a/plugins/catalog-react/api-report.md
+++ b/plugins/catalog-react/api-report.md
@@ -195,7 +195,7 @@ export class EntityLifecycleFilter implements EntityFilter {
// @public (undocumented)
export const EntityLifecyclePicker: (props: {
initialFilter?: string[];
-}) => JSX.Element | null;
+}) => JSX.Element;
// @public
export const EntityListContext: React_2.Context<
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json
index 37190aa4c8..7d925d825b 100644
--- a/plugins/catalog-react/package.json
+++ b/plugins/catalog-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-react",
"description": "A frontend library that helps other Backstage plugins interact with the catalog",
- "version": "1.6.0-next.1",
+ "version": "1.7.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -60,6 +60,7 @@
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
+ "@react-hookz/web": "^23.0.0",
"@types/react": "^16.13.1 || ^17.0.0",
"classnames": "^2.2.6",
"jwt-decode": "^3.1.0",
diff --git a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx
index 3a9a100357..b6de85937d 100644
--- a/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx
+++ b/plugins/catalog-react/src/components/EntityAutocompletePicker/EntityAutocompletePicker.tsx
@@ -54,6 +54,7 @@ export type EntityAutocompletePickerProps<
showCounts?: boolean;
Filter: ConstructableFilter>;
InputProps?: TextFieldProps;
+ initialSelectedOptions?: string[];
};
/** @public */
@@ -61,7 +62,15 @@ export function EntityAutocompletePicker<
T extends DefaultEntityFilters = DefaultEntityFilters,
Name extends AllowedEntityFilters = AllowedEntityFilters,
>(props: EntityAutocompletePickerProps) {
- const { label, name, path, showCounts, Filter, InputProps } = props;
+ const {
+ label,
+ name,
+ path,
+ showCounts,
+ Filter,
+ InputProps,
+ initialSelectedOptions = [],
+ } = props;
const {
updateFilters,
@@ -90,7 +99,8 @@ export function EntityAutocompletePicker<
const [selectedOptions, setSelectedOptions] = useState(
queryParameters.length
? queryParameters
- : (filters[name] as unknown as { values: string[] })?.values ?? [],
+ : (filters[name] as unknown as { values: string[] })?.values ??
+ initialSelectedOptions,
);
// Set selected options on query parameter updates; this happens at initial page load and from
diff --git a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
index 04d8df3d16..2513f8dca9 100644
--- a/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityKindPicker/EntityKindPicker.test.tsx
@@ -19,7 +19,7 @@ import { Entity } from '@backstage/catalog-model';
import { ApiProvider } from '@backstage/core-app-api';
import { alertApiRef } from '@backstage/core-plugin-api';
import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils';
-import { fireEvent, waitFor, screen } from '@testing-library/react';
+import { fireEvent, waitFor, screen, within } from '@testing-library/react';
import { capitalize } from 'lodash';
import { default as React } from 'react';
import { catalogApiRef } from '../../api';
@@ -87,7 +87,7 @@ describe('', () => {
expect(screen.getByText('Kind')).toBeInTheDocument();
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
await waitFor(() => screen.getByText('Domain'));
@@ -115,7 +115,7 @@ describe('', () => {
,
);
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
await waitFor(() => screen.getByText('Domain'));
fireEvent.click(screen.getByText('Domain'));
@@ -171,7 +171,7 @@ describe('', () => {
);
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
expect(
screen.getByRole('option', { name: 'Component' }),
@@ -196,7 +196,7 @@ describe('', () => {
expect(screen.getByText('Frob')).toBeInTheDocument();
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
expect(screen.getByRole('option', { name: 'Domain' })).toBeInTheDocument();
});
});
diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx
index 3ee610ddaf..19554f18b7 100644
--- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.test.tsx
@@ -14,146 +14,116 @@
* limitations under the License.
*/
-import { Entity } from '@backstage/catalog-model';
-import { fireEvent, render, screen } from '@testing-library/react';
+import { fireEvent, render, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityLifecycleFilter } from '../../filters';
import { EntityLifecyclePicker } from './EntityLifecyclePicker';
-
-const sampleEntities: Entity[] = [
- {
- apiVersion: '1',
- kind: 'Component',
- metadata: {
- name: 'component-1',
- },
- spec: {
- lifecycle: 'production',
- },
- },
- {
- apiVersion: '1',
- kind: 'Component',
- metadata: {
- name: 'component-2',
- },
- spec: {
- lifecycle: 'experimental',
- },
- },
- {
- apiVersion: '1',
- kind: 'Component',
- metadata: {
- name: 'component-3',
- },
- spec: {
- lifecycle: 'experimental',
- },
- },
-];
+import { TestApiProvider } from '@backstage/test-utils';
+import { catalogApiRef } from '../../api';
+import { CatalogApi } from '@backstage/catalog-client';
describe('', () => {
- it('renders all lifecycles', () => {
- render(
-
-
- ,
- );
- expect(screen.getByText('Lifecycle')).toBeInTheDocument();
+ const catalogApi = {
+ getEntityFacets: jest.fn(),
+ } as unknown as jest.Mocked;
- fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
- sampleEntities
- .map(e => e.spec?.lifecycle!)
- .forEach(lifecycle => {
- expect(screen.getByText(lifecycle as string)).toBeInTheDocument();
- });
+ beforeEach(() => {
+ catalogApi.getEntityFacets.mockResolvedValue({
+ facets: {
+ 'spec.lifecycle': [
+ { count: 1, value: 'experimental' },
+ { count: 1, value: 'production' },
+ ],
+ },
+ });
});
- it('renders unique lifecycles in alphabetical order', () => {
- render(
-
-
- ,
- );
- expect(screen.getByText('Lifecycle')).toBeInTheDocument();
-
- fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
-
- expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([
- 'experimental',
- 'production',
- ]);
+ afterEach(() => {
+ jest.resetAllMocks();
});
- it('respects the query parameter filter value', () => {
+ it('renders all lifecycles', async () => {
+ render(
+
+
+
+
+ ,
+ );
+ expect(await screen.findByText('Lifecycle')).toBeInTheDocument();
+
+ fireEvent.click(await screen.findByTestId('lifecycles-picker-expand'));
+ expect(screen.getByText('experimental')).toBeInTheDocument();
+ expect(screen.getByText('production')).toBeInTheDocument();
+ });
+
+ it('respects the query parameter filter value', async () => {
const updateFilters = jest.fn();
const queryParameters = { lifecycles: ['experimental'] };
render(
-
-
- ,
+
+
+
+
+ ,
);
+ await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: new EntityLifecycleFilter(['experimental']),
});
});
- it('adds lifecycles to filters', () => {
+ it('adds lifecycles to filters', async () => {
const updateFilters = jest.fn();
render(
-
-
- ,
+
+
+
+
+ ,
);
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: undefined,
});
- fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
+ fireEvent.click(await screen.findByTestId('lifecycles-picker-expand'));
fireEvent.click(screen.getByText('production'));
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: new EntityLifecycleFilter(['production']),
});
});
- it('removes lifecycles from filters', () => {
+ it('removes lifecycles from filters', async () => {
const updateFilters = jest.fn();
render(
-
-
- ,
+
+
+
+
+ ,
);
+
+ await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: new EntityLifecycleFilter(['production']),
});
- fireEvent.click(screen.getByTestId('lifecycle-picker-expand'));
+ fireEvent.click(screen.getByTestId('lifecycles-picker-expand'));
expect(screen.getByLabelText('production')).toBeChecked();
fireEvent.click(screen.getByLabelText('production'));
@@ -162,67 +132,85 @@ describe('', () => {
});
});
- it('responds to external queryParameters changes', () => {
+ it('responds to external queryParameters changes', async () => {
const updateFilters = jest.fn();
const rendered = render(
-
-
- ,
+
+
+
+
+ ,
);
+
+ await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: new EntityLifecycleFilter(['experimental']),
});
+
rendered.rerender(
-
-
- ,
+
+
+
+
+ ,
);
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: new EntityLifecycleFilter(['production']),
});
});
- it('removes lifecycles from filters if there are no available lifecycles', () => {
+
+ it('removes lifecycles from filters if there are no available lifecycles', async () => {
+ catalogApi.getEntityFacets.mockResolvedValue({
+ facets: {
+ 'spec.lifecycle': [],
+ },
+ });
+
const updateFilters = jest.fn();
render(
-
-
- ,
+
+
+
+
+ ,
);
+
+ await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: undefined,
});
});
- it('responds to initialFilter prop', () => {
+
+ it('responds to initialFilter prop', async () => {
const updateFilters = jest.fn();
render(
-
-
- ,
+
+
+
+
+ ,
);
+
+ await waitFor(() => expect(catalogApi.getEntityFacets).toHaveBeenCalled());
expect(updateFilters).toHaveBeenLastCalledWith({
lifecycles: new EntityLifecycleFilter(['production']),
});
diff --git a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx
index dd107b2673..a1bd6ef86b 100644
--- a/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx
+++ b/plugins/catalog-react/src/components/EntityLifecyclePicker/EntityLifecyclePicker.tsx
@@ -14,22 +14,10 @@
* limitations under the License.
*/
-import { Entity } from '@backstage/catalog-model';
-import {
- Box,
- Checkbox,
- FormControlLabel,
- makeStyles,
- TextField,
- Typography,
-} from '@material-ui/core';
-import CheckBoxIcon from '@material-ui/icons/CheckBox';
-import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
-import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
-import { Autocomplete } from '@material-ui/lab';
-import React, { useEffect, useMemo, useState } from 'react';
-import { useEntityList } from '../../hooks/useEntityListProvider';
+import { makeStyles } from '@material-ui/core';
+import React from 'react';
import { EntityLifecycleFilter } from '../../filters';
+import { EntityAutocompletePicker } from '../EntityAutocompletePicker';
/** @public */
export type CatalogReactEntityLifecyclePickerClassKey = 'input';
@@ -43,98 +31,19 @@ const useStyles = makeStyles(
},
);
-const icon = ;
-const checkedIcon = ;
-
/** @public */
export const EntityLifecyclePicker = (props: { initialFilter?: string[] }) => {
const { initialFilter = [] } = props;
const classes = useStyles();
- const {
- updateFilters,
- backendEntities,
- filters,
- queryParameters: { lifecycles: lifecyclesParameter },
- } = useEntityList();
-
- const queryParamLifecycles = useMemo(
- () => [lifecyclesParameter].flat().filter(Boolean) as string[],
- [lifecyclesParameter],
- );
-
- const [selectedLifecycles, setSelectedLifecycles] = useState(
- queryParamLifecycles.length
- ? queryParamLifecycles
- : filters.lifecycles?.values ?? initialFilter,
- );
-
- // Set selected lifecycles on query parameter updates; this happens at initial page load and from
- // external updates to the page location.
- useEffect(() => {
- if (queryParamLifecycles.length) {
- setSelectedLifecycles(queryParamLifecycles);
- }
- }, [queryParamLifecycles]);
-
- const availableLifecycles = useMemo(
- () =>
- [
- ...new Set(
- backendEntities
- .map((e: Entity) => e.spec?.lifecycle)
- .filter(Boolean) as string[],
- ),
- ].sort(),
- [backendEntities],
- );
-
- useEffect(() => {
- updateFilters({
- lifecycles:
- selectedLifecycles.length && availableLifecycles.length
- ? new EntityLifecycleFilter(selectedLifecycles)
- : undefined,
- });
- }, [selectedLifecycles, updateFilters, availableLifecycles]);
-
- if (!availableLifecycles.length) return null;
return (
-
-
- Lifecycle
-
- setSelectedLifecycles(value)
- }
- renderOption={(option, { selected }) => (
-
- }
- onClick={event => event.preventDefault()}
- label={option}
- />
- )}
- size="small"
- popupIcon={}
- renderInput={params => (
-
- )}
- />
-
-
+
);
};
diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx
index e4dc322c7c..e5497d022a 100644
--- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.test.tsx
@@ -14,8 +14,8 @@
* limitations under the License.
*/
-import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
-import { fireEvent, screen } from '@testing-library/react';
+import { Entity } from '@backstage/catalog-model';
+import { fireEvent, screen, waitFor } from '@testing-library/react';
import React from 'react';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityOwnerFilter } from '../../filters';
@@ -28,8 +28,9 @@ import {
} from '@backstage/test-utils';
import { catalogApiRef, CatalogApi } from '../..';
import { errorApiRef } from '@backstage/core-plugin-api';
+import { QueryEntitiesCursorRequest } from '@backstage/catalog-client';
-const ownerEntities: Entity[] = [
+const ownerEntitiesBatch1: Entity[] = [
{
apiVersion: '1',
kind: 'Group',
@@ -68,64 +69,56 @@ const ownerEntities: Entity[] = [
},
];
-const sampleEntities: Entity[] = [
+const ownerEntitiesBatch2: Entity[] = [
{
apiVersion: '1',
- kind: 'Component',
+ kind: 'Group',
metadata: {
- name: 'component-1',
+ name: 'some-owner-batch-2',
},
- relations: [
- {
- type: 'ownedBy',
- targetRef: 'group:default/some-owner',
- },
- {
- type: 'ownedBy',
- targetRef: 'group:default/some-owner-2',
- },
- ],
},
{
apiVersion: '1',
- kind: 'Component',
+ kind: 'Group',
metadata: {
- name: 'component-2',
+ name: 'some-owner-2-batch-2',
},
- relations: [
- {
- type: 'ownedBy',
- targetRef: 'group:default/another-owner',
+ spec: {
+ profile: {
+ displayName: 'Some Owner Batch 2',
},
- {
- type: 'ownedBy',
- targetRef: 'group:test-namespace/another-owner-2',
- },
- ],
+ },
},
{
apiVersion: '1',
- kind: 'Component',
+ kind: 'Group',
metadata: {
- name: 'component-3',
+ name: 'another-owner-batch-2',
+ title: 'Another Owner Batch 2',
+ },
+ },
+ {
+ apiVersion: '1',
+ kind: 'Group',
+ metadata: {
+ namespace: 'test-namespace',
+ name: 'another-owner-2-batch-2',
+ title: 'Another Owner in Another Namespace Batch 2',
},
- relations: [
- {
- type: 'ownedBy',
- targetRef: 'group:default/some-owner',
- },
- ],
},
];
-const getEntitiesByRefs = jest.fn(async ({ entityRefs }) => ({
- items: entityRefs.map((e: string) =>
- ownerEntities.find(f => stringifyEntityRef(f) === e),
- ),
-}));
+const mockedQueryEntities: jest.MockedFn =
+ jest.fn();
+
+const mockedGetEntitiesByRef: jest.MockedFn =
+ jest.fn();
+
const mockCatalogApi: Partial = {
- getEntitiesByRefs,
+ queryEntities: mockedQueryEntities,
+ getEntitiesByRefs: mockedGetEntitiesByRef,
};
+
const mockErrorApi = new MockErrorApi();
describe('', () => {
@@ -134,12 +127,33 @@ describe('', () => {
[errorApiRef, mockErrorApi],
);
- it('renders all owners', async () => {
+ beforeEach(() => {
+ jest.resetAllMocks();
+
+ mockedQueryEntities.mockImplementation(async request => {
+ const totalItems =
+ ownerEntitiesBatch1.length + ownerEntitiesBatch2.length;
+ if ((request as QueryEntitiesCursorRequest).cursor) {
+ return {
+ items: ownerEntitiesBatch2,
+ pageInfo: {},
+ totalItems,
+ };
+ }
+ return {
+ items: ownerEntitiesBatch1,
+ pageInfo: {
+ nextCursor: 'nextCursor',
+ },
+ totalItems,
+ };
+ });
+ });
+
+ it('renders all users and groups', async () => {
await renderWithEffects(
-
+ ,
@@ -147,36 +161,37 @@ describe('', () => {
expect(screen.getByText('Owner')).toBeInTheDocument();
fireEvent.click(screen.getByTestId('owner-picker-expand'));
+
+ await waitFor(() =>
+ expect(screen.getByText('Another Owner')).toBeInTheDocument(),
+ );
+
[
- 'Another Owner',
'some-owner',
'Some Owner 2',
'Another Owner in Another Namespace',
].forEach(owner => {
expect(screen.getByText(owner)).toBeInTheDocument();
});
- });
- it('renders unique owners in alphabetical order', async () => {
- await renderWithEffects(
-
-
-
-
- ,
+ expect(mockedQueryEntities).toHaveBeenCalledTimes(1);
+ expect(mockedGetEntitiesByRef).not.toHaveBeenCalled();
+
+ fireEvent.scroll(screen.getByTestId('owner-picker-listbox'));
+
+ await waitFor(() =>
+ expect(screen.getByText('some-owner-batch-2')).toBeInTheDocument(),
);
- expect(screen.getByText('Owner')).toBeInTheDocument();
- fireEvent.click(screen.getByTestId('owner-picker-expand'));
+ [
+ 'some-owner-batch-2',
+ 'Some Owner Batch 2',
+ 'Another Owner in Another Namespace Batch 2',
+ ].forEach(owner => {
+ expect(screen.getByText(owner)).toBeInTheDocument();
+ });
- expect(screen.getAllByRole('option').map(o => o.textContent)).toEqual([
- 'Another Owner',
- 'Another Owner in Another Namespace',
- 'some-owner',
- 'Some Owner 2',
- ]);
+ expect(mockedQueryEntities).toHaveBeenCalledTimes(2);
});
it('respects the query parameter filter value', async () => {
@@ -186,8 +201,6 @@ describe('', () => {
', () => {
,
);
+ expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({
+ entityRefs: ['another-owner'],
+ });
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['group:default/another-owner']),
});
});
+ it('should display the selected owners as humanized entities', async () => {
+ const updateFilters = jest.fn();
+ const queryParameters = { owners: ['another-owner'] };
+
+ mockedGetEntitiesByRef.mockResolvedValue({
+ items: [
+ {
+ metadata: {
+ name: 'another-owner',
+ title: 'Beautiful display name',
+ namespace: 'default',
+ },
+ apiVersion: '1',
+ kind: 'group',
+ },
+ ],
+ });
+ await renderWithEffects(
+
+
+
+
+ ,
+ );
+
+ await waitFor(() =>
+ expect(
+ screen.getByRole('button', {
+ name: 'Beautiful display name',
+ }),
+ ).toBeInTheDocument(),
+ );
+
+ expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({
+ entityRefs: ['another-owner'],
+ });
+
+ fireEvent.click(screen.getByTestId('owner-picker-expand'));
+ await waitFor(() => screen.getByText('Some Owner 2'));
+ fireEvent.click(screen.getByText('Some Owner 2'));
+
+ expect(mockedGetEntitiesByRef).toHaveBeenCalledTimes(1);
+
+ await waitFor(() =>
+ expect(
+ screen.getByRole('button', {
+ name: 'Some Owner 2',
+ }),
+ ).toBeInTheDocument(),
+ );
+ });
+
it('adds owners to filters', async () => {
const updateFilters = jest.fn();
await renderWithEffects(
@@ -217,11 +288,14 @@ describe('', () => {
,
);
+ expect(mockedGetEntitiesByRef).not.toHaveBeenCalled();
expect(updateFilters).toHaveBeenLastCalledWith({
owners: undefined,
});
fireEvent.click(screen.getByTestId('owner-picker-expand'));
+ await waitFor(() => screen.getByText('some-owner'));
+
fireEvent.click(screen.getByText('some-owner'));
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['group:default/some-owner']),
@@ -234,8 +308,6 @@ describe('', () => {
', () => {
,
);
+ expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({
+ entityRefs: ['group:default/some-owner'],
+ });
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['group:default/some-owner']),
});
fireEvent.click(screen.getByTestId('owner-picker-expand'));
- expect(screen.getByLabelText('some-owner')).toBeChecked();
+ await waitFor(() =>
+ expect(screen.getByLabelText('some-owner')).toBeChecked(),
+ );
fireEvent.click(screen.getByLabelText('some-owner'));
expect(updateFilters).toHaveBeenLastCalledWith({
@@ -265,13 +342,15 @@ describe('', () => {
value={{
updateFilters,
queryParameters: { owners: ['team-a'] },
- backendEntities: sampleEntities,
}}
>
,
);
+ expect(mockedGetEntitiesByRef).toHaveBeenCalledWith({
+ entityRefs: ['team-a'],
+ });
expect(updateFilters).toHaveBeenLastCalledWith({
owners: new EntityOwnerFilter(['group:default/team-a']),
});
@@ -281,7 +360,6 @@ describe('', () => {
value={{
updateFilters,
queryParameters: { owners: ['team-b'] },
- backendEntities: sampleEntities,
}}
>
@@ -292,23 +370,4 @@ describe('', () => {
owners: new EntityOwnerFilter(['group:default/team-b']),
});
});
- it('removes owners from filters if there are none available', async () => {
- const updateFilters = jest.fn();
- await renderWithEffects(
-
-
-
-
- ,
- );
- expect(updateFilters).toHaveBeenLastCalledWith({
- owners: undefined,
- });
- });
});
diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
index 8c4f2bfea0..8b223c50b1 100644
--- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
+++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
@@ -14,32 +14,30 @@
* limitations under the License.
*/
-import {
- Entity,
- parseEntityRef,
- RELATION_OWNED_BY,
- stringifyEntityRef,
-} from '@backstage/catalog-model';
+import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import {
Box,
Checkbox,
FormControlLabel,
- makeStyles,
TextField,
Typography,
+ makeStyles,
} from '@material-ui/core';
import CheckBoxIcon from '@material-ui/icons/CheckBox';
import CheckBoxOutlineBlankIcon from '@material-ui/icons/CheckBoxOutlineBlank';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import { Autocomplete } from '@material-ui/lab';
-import React, { useEffect, useMemo, useState } from 'react';
+import React, { useEffect, useMemo, useRef, useState } from 'react';
import { useEntityList } from '../../hooks/useEntityListProvider';
import { EntityOwnerFilter } from '../../filters';
-import { getEntityRelations } from '../../utils';
-import useAsync from 'react-use/lib/useAsync';
-import { errorApiRef, useApi } from '@backstage/core-plugin-api';
+import { useApi } from '@backstage/core-plugin-api';
import { catalogApiRef } from '../../api';
-import { humanizeEntity, humanizeEntityRef } from '../EntityRefLink/humanize';
+import useAsync from 'react-use/lib/useAsync';
+import useAsyncFn from 'react-use/lib/useAsyncFn';
+import { useDebouncedEffect } from '@react-hookz/web';
+import PersonIcon from '@material-ui/icons/Person';
+import GroupIcon from '@material-ui/icons/Group';
+import { humanizeEntity } from '../EntityRefLink/humanize';
/** @public */
export type CatalogReactEntityOwnerPickerClassKey = 'input';
@@ -61,12 +59,51 @@ export const EntityOwnerPicker = () => {
const classes = useStyles();
const {
updateFilters,
- backendEntities,
filters,
queryParameters: { owners: ownersParameter },
} = useEntityList();
+
const catalogApi = useApi(catalogApiRef);
- const errorApi = useApi(errorApiRef);
+ const [text, setText] = useState('');
+
+ const [{ value, loading }, handleFetch] = useAsyncFn(
+ async (request: { text: string } | { cursor: string; prev: Entity[] }) => {
+ const initialRequest = request as { text: string };
+ const cursorRequest = request as { cursor: string; prev: Entity[] };
+ const limit = 20;
+
+ if (cursorRequest.cursor) {
+ const response = await catalogApi.queryEntities({
+ cursor: cursorRequest.cursor,
+ limit,
+ });
+ return {
+ ...response,
+ items: [...cursorRequest.prev, ...response.items],
+ };
+ }
+
+ return catalogApi.queryEntities({
+ fullTextFilter: {
+ term: initialRequest.text || '',
+ fields: [
+ 'metadata.name',
+ 'kind',
+ 'spec.profile.displayname',
+ 'metadata.title',
+ ],
+ },
+ filter: { kind: ['User', 'Group'] },
+ orderFields: [{ field: 'metadata.name', order: 'asc' }],
+ limit,
+ });
+ },
+ [text],
+ );
+
+ useDebouncedEffect(() => handleFetch({ text }), [text], 250);
+
+ const availableOwners = value?.items || [];
const queryParamOwners = useMemo(
() => [ownersParameter].flat().filter(Boolean) as string[],
@@ -74,71 +111,10 @@ export const EntityOwnerPicker = () => {
);
const [selectedOwners, setSelectedOwners] = useState(
- queryParamOwners.length
- ? new EntityOwnerFilter(queryParamOwners).values
- : filters.owners?.values ?? [],
+ queryParamOwners.length ? queryParamOwners : filters.owners?.values ?? [],
);
- const {
- loading,
- error,
- value: ownerEntities,
- } = useAsync(async () => {
- const ownerEntityRefs = [
- ...new Set(
- backendEntities
- .flatMap((e: Entity) =>
- getEntityRelations(e, RELATION_OWNED_BY).map(o =>
- stringifyEntityRef(o),
- ),
- )
- .filter(Boolean) as string[],
- ),
- ];
- const { items: ownerEntitiesOrNull } = await catalogApi.getEntitiesByRefs({
- entityRefs: ownerEntityRefs,
- fields: [
- 'kind',
- 'metadata.name',
- 'metadata.title',
- 'metadata.namespace',
- 'spec.profile.displayName',
- ],
- });
- const owners = ownerEntitiesOrNull.map((entity, index) => {
- if (entity) {
- return {
- label: humanizeEntity(entity, { defaultKind: 'Group' }),
- entityRef: stringifyEntityRef(entity),
- };
- }
- return {
- label: humanizeEntityRef(parseEntityRef(ownerEntityRefs[index]), {
- defaultKind: 'group',
- }),
- entityRef: ownerEntityRefs[index],
- };
- });
-
- return owners.sort((a, b) =>
- a.label.localeCompare(b.label, 'en-US', {
- ignorePunctuation: true,
- caseFirst: 'upper',
- }),
- );
- }, [backendEntities]);
-
- useEffect(() => {
- if (error) {
- errorApi.post(
- {
- ...error,
- message: `EntityOwnerPicker failed to initialize: ${error.message}`,
- },
- {},
- );
- }
- }, [error, errorApi]);
+ const { getEntity, setEntity } = useSelectedOwners(selectedOwners);
// Set selected owners on query parameter updates; this happens at initial page load and from
// external updates to the page location.
@@ -150,17 +126,20 @@ export const EntityOwnerPicker = () => {
}, [queryParamOwners]);
useEffect(() => {
- if (!loading && ownerEntities) {
- updateFilters({
- owners:
- selectedOwners.length && ownerEntities.length
- ? new EntityOwnerFilter(selectedOwners)
- : undefined,
- });
- }
- }, [selectedOwners, updateFilters, ownerEntities, loading]);
+ updateFilters({
+ owners: selectedOwners.length
+ ? new EntityOwnerFilter(selectedOwners)
+ : undefined,
+ });
+ }, [selectedOwners, updateFilters]);
- if (!loading && !ownerEntities?.length) return null;
+ if (
+ ['user', 'group'].includes(
+ filters.kind?.value.toLocaleLowerCase('en-US') || '',
+ )
+ ) {
+ return null;
+ }
return (
@@ -170,40 +149,130 @@ export const EntityOwnerPicker = () => {
multiple
disableCloseOnSelect
loading={loading}
- options={ownerEntities || []}
- value={
- ownerEntities?.filter(e =>
- selectedOwners.some((f: string) => f === e.entityRef),
- ) ?? []
- }
- onChange={(_: object, value: { entityRef: string }[]) =>
- setSelectedOwners(value.map(e => e.entityRef))
- }
- getOptionLabel={option => option.label}
- renderOption={(option, { selected }) => (
-
- }
- onClick={event => event.preventDefault()}
- label={option.label}
- />
- )}
+ options={availableOwners}
+ value={selectedOwners as unknown as Entity[]}
+ getOptionSelected={(o, v) => {
+ if (typeof v === 'string') {
+ return stringifyEntityRef(o) === v;
+ }
+ return o === v;
+ }}
+ getOptionLabel={o => {
+ const entity = typeof o === 'string' ? getEntity(o) || o : o;
+
+ return typeof entity === 'string'
+ ? entity
+ : humanizeEntity(entity, entity.metadata.name);
+ }}
+ onChange={(_: object, owners) => {
+ setText('');
+ setSelectedOwners(
+ owners.map(e => {
+ const entityRef =
+ typeof e === 'string' ? e : stringifyEntityRef(e);
+
+ if (typeof e !== 'string') {
+ setEntity(e);
+ }
+
+ return entityRef;
+ }),
+ );
+ }}
+ filterOptions={x => x}
+ renderOption={(entity, { selected }) => {
+ const isGroup = entity.kind === 'Group';
+
+ return (
+
+ }
+ onClick={event => event.preventDefault()}
+ label={
+
+ {isGroup ? (
+
+ ) : (
+
+ )}
+
+ {humanizeEntity(entity, entity.metadata.name)}
+
+ }
+ />
+ );
+ }}
size="small"
popupIcon={}
renderInput={params => (
{
+ setText(e.currentTarget.value);
+ }}
variant="outlined"
/>
)}
+ ListboxProps={{
+ onScroll: (e: React.MouseEvent) => {
+ const element = e.currentTarget;
+ const hasReachedEnd =
+ Math.abs(
+ element.scrollHeight -
+ element.clientHeight -
+ element.scrollTop,
+ ) < 1;
+
+ if (hasReachedEnd && value?.pageInfo.nextCursor) {
+ handleFetch({
+ cursor: value.pageInfo.nextCursor,
+ prev: value.items,
+ });
+ }
+ },
+ 'data-testid': 'owner-picker-listbox',
+ }}
/>
);
};
+
+/**
+ * Hook used for storing the full entity of the specified owners
+ * in order to display users and group using the information contained on each entity.
+ * When a component is rendered for the first time, it loads the content of the entities
+ * specified by `initialSelectedOwnersRefs` and export the `getEntity` and `setEntity`
+ * utilities, used to retrieve and modify the owners.
+ */
+function useSelectedOwners(initialSelectedOwnersRefs: string[]) {
+ const allEntities = useRef>({});
+ const catalogApi = useApi(catalogApiRef);
+
+ useAsync(async () => {
+ if (initialSelectedOwnersRefs.length === 0) {
+ return;
+ }
+ const initialSelectedEntities = await catalogApi.getEntitiesByRefs({
+ entityRefs: initialSelectedOwnersRefs,
+ });
+ initialSelectedEntities.items.forEach(e => {
+ if (e) {
+ allEntities.current[stringifyEntityRef(e)] = e;
+ }
+ });
+ }, []);
+
+ return {
+ getEntity: (entityRef: string) => allEntities.current[entityRef],
+ setEntity: (entity: Entity) => {
+ allEntities.current[stringifyEntityRef(entity)] = entity;
+ },
+ };
+}
diff --git a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx
index 00d36e5e13..3321db6d96 100644
--- a/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityProcessingStatusPicker/EntityProcessingStatusPicker.test.tsx
@@ -14,48 +14,16 @@
* limitations under the License.
*/
-import { Entity } from '@backstage/catalog-model';
import { fireEvent, render, screen } from '@testing-library/react';
import React from 'react';
import { EntityErrorFilter, EntityOrphanFilter } from '../../filters';
import { MockEntityListContextProvider } from '../../testUtils/providers';
import { EntityProcessingStatusPicker } from './EntityProcessingStatusPicker';
-const orphanAnnotation: Record = {};
-orphanAnnotation['backstage.io/orphan'] = 'true';
-
-const sampleEntities: Entity[] = [
- {
- apiVersion: '1',
- kind: 'Component',
- metadata: {
- name: 'valid-component',
- },
- },
- {
- apiVersion: '1',
- kind: 'Component',
- metadata: {
- name: 'orphan-component',
- annotations: orphanAnnotation,
- },
- },
- {
- apiVersion: '1',
- kind: 'Component',
- metadata: {
- name: 'error-component',
- tags: ['Invalid Tag'],
- },
- },
-];
-
describe('', () => {
it('renders all processing status options', () => {
render(
-
+ ,
);
@@ -71,8 +39,6 @@ describe('', () => {
render(
@@ -92,8 +58,6 @@ describe('', () => {
render(
@@ -113,8 +77,6 @@ describe('', () => {
render(
@@ -134,8 +96,6 @@ describe('', () => {
render(
diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts
index e7d1649a29..1182a7778d 100644
--- a/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts
+++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.test.ts
@@ -215,9 +215,12 @@ describe('humanizeEntityRef', () => {
describe('humanizeEntity', () => {
it('gives a readable name when one is provided at metadata.title', () => {
expect(
- humanizeEntity({
- metadata: { name: 'my-entity', title: 'My Title' },
- } as Entity),
+ humanizeEntity(
+ {
+ metadata: { name: 'my-entity', title: 'My Title' },
+ } as Entity,
+ 'default',
+ ),
).toBe('My Title');
});
@@ -257,13 +260,16 @@ describe('humanizeEntity', () => {
])(
'gives a readable name for kind %s when one is provided at spec.profile.displayName',
(_, entity: Entity, expected) => {
- expect(humanizeEntity(entity)).toBe(expected);
+ expect(humanizeEntity(entity, 'default')).toBe(expected);
},
);
it('should pass through to humanizeEntityRef when nothing matches', () => {
expect(
- humanizeEntity({ kind: 'Group', metadata: { name: 'test' } } as Entity),
- ).toBe('group:test');
+ humanizeEntity(
+ { kind: 'Group', metadata: { name: 'test' } } as Entity,
+ 'default',
+ ),
+ ).toBe('default');
});
});
diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts
index 7f1501280a..478f0dce9d 100644
--- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts
+++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts
@@ -25,7 +25,8 @@ import get from 'lodash/get';
* @param defaultNamespace - if set to false then namespace is never omitted,
* if set to string which matches namespace of entity then omitted
*
- * @public */
+ * @public
+ **/
export function humanizeEntityRef(
entityRef: Entity | CompoundEntityRef,
opts?: {
@@ -73,27 +74,19 @@ export function humanizeEntityRef(
* If an entity is either User or Group, this will be its `spec.profile.displayName`.
* Otherwise, this is `metadata.title`.
*
- * If neither of those are found or populated, fallback to `humanizeEntityRef`.
+ * If neither of those are found or populated, fallback to `defaultName`.
*
* @param entity - Entity to convert.
- * @param opts - If entity readable name is not available, opts will be used to specify humanizeEntityRef options.
- * @returns Readable name, defaults to unique identifier.
+ * @param defaultName - If entity readable name is not available, `defaultName` will be returned.
+ * @returns Readable name, defaults to `defaultName`.
*
- * @public
*/
-export function humanizeEntity(
- entity: Entity,
- opts?: {
- defaultKind?: string;
- defaultNamespace?: string | false;
- },
-) {
+export function humanizeEntity(entity: Entity, defaultName: string) {
for (const path of ['spec.profile.displayName', 'metadata.title']) {
const value = get(entity, path);
if (value && typeof value === 'string') {
return value;
}
}
-
- return humanizeEntityRef(entity, opts);
+ return defaultName;
}
diff --git a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx
index d61a6c480f..3e69c35180 100644
--- a/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx
+++ b/plugins/catalog-react/src/components/EntityTypePicker/EntityTypePicker.test.tsx
@@ -15,7 +15,7 @@
*/
import React from 'react';
-import { fireEvent, waitFor, screen } from '@testing-library/react';
+import { fireEvent, waitFor, screen, within } from '@testing-library/react';
import { Entity } from '@backstage/catalog-model';
import { EntityTypePicker } from './EntityTypePicker';
import { MockEntityListContextProvider } from '../../testUtils/providers';
@@ -95,7 +95,7 @@ describe('', () => {
expect(screen.getByText('Type')).toBeInTheDocument();
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
await waitFor(() => screen.getByText('service'));
@@ -119,7 +119,7 @@ describe('', () => {
,
);
const input = screen.getByTestId('select');
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
await waitFor(() => screen.getByText('service'));
fireEvent.click(screen.getByText('service'));
@@ -128,7 +128,7 @@ describe('', () => {
type: new EntityTypeFilter(['service']),
});
- fireEvent.click(input);
+ fireEvent.mouseDown(within(input).getByRole('button'));
fireEvent.click(screen.getByText('all'));
expect(updateFilters).toHaveBeenLastCalledWith({ type: undefined });
diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md
index 1082396da1..07603b5a5b 100644
--- a/plugins/catalog/CHANGELOG.md
+++ b/plugins/catalog/CHANGELOG.md
@@ -1,5 +1,76 @@
# @backstage/plugin-catalog
+## 1.11.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/integration-react@1.1.14-next.0
+ - @backstage/plugin-search-react@1.6.1-next.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-search-common@1.2.3
+
+## 1.11.0
+
+### Minor Changes
+
+- 2258dcae970: Added an entity namespace filter and column on the default catalog page.
+
+ If you have a custom version of the catalog page, you can add this filter in your CatalogPage code:
+
+ ```ts
+
+
+
+
+
+ /* if you want namespace picker */
+
+
+
+
+
+
+ ```
+
+ The namespace column can be added using `createNamespaceColumn();`. This is only needed if you customized the columns for CatalogTable.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/plugin-search-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/integration-react@1.1.13
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.13
+ - @backstage/plugin-search-common@1.2.3
+
+## 1.11.0-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-search-react@1.6.0-next.2
+ - @backstage/integration-react@1.1.13-next.2
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 1.11.0-next.1
### Patch Changes
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index 0d5a9badcd..e76be49515 100644
--- a/plugins/catalog/package.json
+++ b/plugins/catalog/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog",
"description": "The Backstage plugin for browsing the Backstage catalog",
- "version": "1.11.0-next.1",
+ "version": "1.11.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
index d895633ad4..3466ea9586 100644
--- a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
+++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
@@ -1,5 +1,31 @@
# @backstage/plugin-cicd-statistics-module-gitlab
+## 0.1.15-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-cicd-statistics@0.1.21-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.1.14
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-cicd-statistics@0.1.20
+
+## 0.1.14-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-cicd-statistics@0.1.20-next.2
+
## 0.1.14-next.1
### Patch Changes
diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json
index 20d82e485f..4c33240415 100644
--- a/plugins/cicd-statistics-module-gitlab/package.json
+++ b/plugins/cicd-statistics-module-gitlab/package.json
@@ -1,14 +1,13 @@
{
"name": "@backstage/plugin-cicd-statistics-module-gitlab",
"description": "CI/CD Statistics plugin module; Gitlab CICD",
- "version": "0.1.14-next.1",
+ "version": "0.1.15-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
"publishConfig": {
"access": "public",
- "main": "dist/index.cjs.js",
- "module": "dist/index.esm.js",
+ "main": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
"backstage": {
diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md
index 3a802df672..cd6ea6dba8 100644
--- a/plugins/cicd-statistics/CHANGELOG.md
+++ b/plugins/cicd-statistics/CHANGELOG.md
@@ -1,5 +1,31 @@
# @backstage/plugin-cicd-statistics
+## 0.1.21-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.1.20
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.20-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.20-next.1
### Patch Changes
diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json
index a6a01ae05c..31027c84e7 100644
--- a/plugins/cicd-statistics/package.json
+++ b/plugins/cicd-statistics/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-cicd-statistics",
"description": "A frontend plugin visualizing CI/CD pipeline statistics (build time)",
- "version": "0.1.20-next.1",
+ "version": "0.1.21-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md
index 8afa59660d..f1ede4c911 100644
--- a/plugins/circleci/CHANGELOG.md
+++ b/plugins/circleci/CHANGELOG.md
@@ -1,5 +1,39 @@
# @backstage/plugin-circleci
+## 0.3.19-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.3.18
+
+### Patch Changes
+
+- 451b3cadb3d: Fixes row display for in progress jobs to not display trailing "took"
+- 1c4958d905f: Hide empty time field data for queued builds which haven't started yet
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.18-next.1
### Patch Changes
diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json
index 683b43795c..dda06965c1 100644
--- a/plugins/circleci/package.json
+++ b/plugins/circleci/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-circleci",
"description": "A Backstage plugin that integrates towards Circle CI",
- "version": "0.3.18-next.1",
+ "version": "0.3.19-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md
index ba462e356c..9ce48f5525 100644
--- a/plugins/cloudbuild/CHANGELOG.md
+++ b/plugins/cloudbuild/CHANGELOG.md
@@ -1,5 +1,37 @@
# @backstage/plugin-cloudbuild
+## 0.3.19-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.3.18
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.3.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.3.18-next.1
### Patch Changes
diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json
index 26a72a6c84..5a2877a9a5 100644
--- a/plugins/cloudbuild/package.json
+++ b/plugins/cloudbuild/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-cloudbuild",
"description": "A Backstage plugin that integrates towards Google Cloud Build",
- "version": "0.3.18-next.1",
+ "version": "0.3.19-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md
index 405e65e814..2f4b706935 100644
--- a/plugins/code-climate/CHANGELOG.md
+++ b/plugins/code-climate/CHANGELOG.md
@@ -1,5 +1,37 @@
# @backstage/plugin-code-climate
+## 0.1.19-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+
+## 0.1.18
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/core-plugin-api@1.5.1
+
+## 0.1.18-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.18-next.1
### Patch Changes
diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json
index 2edda59078..c531a6bff4 100644
--- a/plugins/code-climate/package.json
+++ b/plugins/code-climate/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-code-climate",
- "version": "0.1.18-next.1",
+ "version": "0.1.19-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md
index a63405bd75..d9dd489217 100644
--- a/plugins/code-coverage-backend/CHANGELOG.md
+++ b/plugins/code-coverage-backend/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/plugin-code-coverage-backend
+## 0.2.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/catalog-client@1.4.2-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/integration@1.4.5
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+
+## 0.2.11
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.5
+ - @backstage/integration@1.4.5
+ - @backstage/catalog-client@1.4.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 0.2.11-next.1
### Patch Changes
diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json
index 549e0a1b2c..24cdbc2f60 100644
--- a/plugins/code-coverage-backend/package.json
+++ b/plugins/code-coverage-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-code-coverage-backend",
"description": "A Backstage backend plugin that helps you keep track of your code coverage",
- "version": "0.2.11-next.1",
+ "version": "0.2.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/code-coverage/CHANGELOG.md b/plugins/code-coverage/CHANGELOG.md
index 7f5789d3a5..064f1bc791 100644
--- a/plugins/code-coverage/CHANGELOG.md
+++ b/plugins/code-coverage/CHANGELOG.md
@@ -1,5 +1,42 @@
# @backstage/plugin-code-coverage
+## 0.2.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/errors@1.1.5
+
+## 0.2.11
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+
+## 0.2.11-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.2.11-next.1
### Patch Changes
diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json
index 412c7d3281..9f4b310d04 100644
--- a/plugins/code-coverage/package.json
+++ b/plugins/code-coverage/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-code-coverage",
"description": "A Backstage plugin that helps you keep track of your code coverage",
- "version": "0.2.11-next.1",
+ "version": "0.2.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md
index 3c6338dfb0..0dc5111af3 100644
--- a/plugins/codescene/CHANGELOG.md
+++ b/plugins/codescene/CHANGELOG.md
@@ -1,5 +1,37 @@
# @backstage/plugin-codescene
+## 0.1.14-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+
+## 0.1.13
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/core-components@0.13.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+
+## 0.1.13-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.13-next.0
### Patch Changes
diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json
index 744634130c..9cd8baa955 100644
--- a/plugins/codescene/package.json
+++ b/plugins/codescene/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-codescene",
- "version": "0.1.13-next.0",
+ "version": "0.1.14-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md
index e744ff73b1..4e1ed10101 100644
--- a/plugins/config-schema/CHANGELOG.md
+++ b/plugins/config-schema/CHANGELOG.md
@@ -1,5 +1,39 @@
# @backstage/plugin-config-schema
+## 0.1.42-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 0.1.41
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/core-components@0.13.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
+## 0.1.41-next.1
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.1.41-next.0
### Patch Changes
diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json
index 893048274e..4e1349974f 100644
--- a/plugins/config-schema/package.json
+++ b/plugins/config-schema/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-config-schema",
"description": "A Backstage plugin that lets you browse the configuration schema of your app",
- "version": "0.1.41-next.0",
+ "version": "0.1.42-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md
index 89ba2ebaa0..59670358ad 100644
--- a/plugins/cost-insights/CHANGELOG.md
+++ b/plugins/cost-insights/CHANGELOG.md
@@ -1,5 +1,42 @@
# @backstage/plugin-cost-insights
+## 0.12.8-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.7.0-next.0
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/config@1.0.7
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/plugin-cost-insights-common@0.1.1
+
+## 0.12.7
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0
+ - @backstage/plugin-catalog-react@1.6.0
+ - @backstage/core-components@0.13.1
+ - @backstage/catalog-model@1.3.0
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-cost-insights-common@0.1.1
+
+## 0.12.7-next.2
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/plugin-catalog-react@1.6.0-next.2
+ - @backstage/config@1.0.7
+ - @backstage/core-plugin-api@1.5.1
+
## 0.12.7-next.1
### Patch Changes
diff --git a/plugins/cost-insights/contrib/aws-cost-explorer-api.md b/plugins/cost-insights/contrib/aws-cost-explorer-api.md
index 1e3f4e9de6..4fbf8a0cbe 100644
--- a/plugins/cost-insights/contrib/aws-cost-explorer-api.md
+++ b/plugins/cost-insights/contrib/aws-cost-explorer-api.md
@@ -75,7 +75,7 @@ try {
We can use the data provided by the Cost Explorer API to implement CostInsightsApi methods such as `getGroupDailyCost`, `getProjectDailyCost`, and `getProductInsights`. You'll still need to provide external data for methods such as `getUserGroups` and `getAlerts`.
-### 1. [getGroupDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L93)
+### 1. [getGroupDailyCost](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L119)
The `getGroupDailyCost` method is expected to return daily cost aggregations for a given group and interval time frame as a [`Cost`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Cost.ts).
@@ -206,13 +206,13 @@ Sample `groupedCosts` based on the response:
}
```
-### 2. [getProjectDailyCost](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L111)
+### 2. [getProjectDailyCost](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L137)
The `getProjectDailyCost` method is expected to return daily cost aggregations for a given billing entity and interval time frame as a [`Cost`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Cost.ts).
This should be similar to the `getGroupDailyCost` method implementation, but with an updated `LINKED_ACCOUNT` filter to get narrower cost data for a lower-level linked account.
-### 3. [getProductInsights](https://github.com/backstage/backstage/blob/master/plugins/cost-insights/src/api/CostInsightsApi.ts#L111)
+### 3. [getProductInsights](https://github.com/backstage/backstage/blob/e0506af8fc54074a160fb91c83d6cae8172d3bb3/plugins/cost-insights/src/api/CostInsightsApi.ts#L165)
The `getProductInsights` method is expected to return cost aggregations for a particular cloud product and interval time frame as an [`Entity`](https://github.com/backstage/backstage/blob/master/plugins/cost-insights-common/src/types/Entity.ts).
diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json
index 4855aae01a..b1ab7f444c 100644
--- a/plugins/cost-insights/package.json
+++ b/plugins/cost-insights/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-cost-insights",
"description": "A Backstage plugin that helps you keep track of your cloud spend",
- "version": "0.12.7-next.1",
+ "version": "0.12.8-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/devtools-backend/.eslintrc.js b/plugins/devtools-backend/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/devtools-backend/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/devtools-backend/CHANGELOG.md b/plugins/devtools-backend/CHANGELOG.md
new file mode 100644
index 0000000000..d4b5c74d9c
--- /dev/null
+++ b/plugins/devtools-backend/CHANGELOG.md
@@ -0,0 +1,50 @@
+# @backstage/plugin-devtools-backend
+
+## 0.1.1-next.0
+
+### Patch Changes
+
+- 3d11596a72b5: Update plugin installation docs to be more consistent across documentations
+- Updated dependencies
+ - @backstage/config-loader@1.3.1-next.0
+ - @backstage/backend-common@0.18.6-next.0
+ - @backstage/config@1.0.7
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.15-next.0
+ - @backstage/plugin-devtools-common@0.1.0
+ - @backstage/plugin-permission-common@0.7.5
+ - @backstage/plugin-permission-node@0.7.9-next.0
+
+## 0.1.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-common@0.1.0
+ - @backstage/backend-common@0.18.5
+ - @backstage/config-loader@1.3.0
+ - @backstage/plugin-permission-node@0.7.8
+ - @backstage/plugin-auth-node@0.2.14
+ - @backstage/cli-common@0.1.12
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-permission-common@0.7.5
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-common@0.1.0-next.0
+ - @backstage/config@1.0.7
diff --git a/plugins/devtools-backend/README.md b/plugins/devtools-backend/README.md
new file mode 100644
index 0000000000..7304ce33d7
--- /dev/null
+++ b/plugins/devtools-backend/README.md
@@ -0,0 +1,55 @@
+# DevTools Backend
+
+Welcome to the DevTools backend plugin! This plugin provides data for the [DevTools frontend](../devtools/) features.
+
+## Setup
+
+Here's how to get the DevTools Backend up and running:
+
+1. First we need to add the `@backstage/plugin-devtools-backend` package to your backend:
+
+ ```sh
+ # From the Backstage root directory
+ yarn add --cwd packages/backend @backstage/plugin-devtools-backend
+ ```
+
+2. Then we will create a new file named `packages/backend/src/plugins/devtools.ts`, and add the
+ following to it:
+
+ ```ts
+ import { createRouter } from '@backstage/plugin-devtools-backend';
+ import { Router } from 'express';
+ import type { PluginEnvironment } from '../types';
+
+ export default function createPlugin(
+ env: PluginEnvironment,
+ ): Promise {
+ return createRouter({
+ logger: env.logger,
+ config: env.config,
+ permissions: env.permissions,
+ });
+ }
+ ```
+
+3. Next we wire this into the overall backend router, edit `packages/backend/src/index.ts`:
+
+ ```ts
+ import devTools from './plugins/devtools';
+ // ...
+ async function main() {
+ // ...
+ // Add this line under the other lines that follow the useHotMemoize pattern
+ const devToolsEnv = useHotMemoize(module, () => createEnv('devtools'));
+ // ...
+ // Insert this line under the other lines that add their routers to apiRouter in the same way
+ apiRouter.use('/devtools', await devTools(devToolsEnv));
+ ```
+
+4. Now run `yarn start-backend` from the repo root
+5. Finally open `http://localhost:7007/api/devtools/health` in a browser and it should return `{"status":"ok"}`
+
+## Links
+
+- [Frontend part of the plugin](../devtools/README.md)
+- [The Backstage homepage](https://backstage.io)
diff --git a/plugins/devtools-backend/api-report.md b/plugins/devtools-backend/api-report.md
new file mode 100644
index 0000000000..e28f0697ab
--- /dev/null
+++ b/plugins/devtools-backend/api-report.md
@@ -0,0 +1,39 @@
+## API Report File for "@backstage/plugin-devtools-backend"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { Config } from '@backstage/config';
+import { ConfigInfo } from '@backstage/plugin-devtools-common';
+import { DevToolsInfo } from '@backstage/plugin-devtools-common';
+import express from 'express';
+import { ExternalDependency } from '@backstage/plugin-devtools-common';
+import { Logger } from 'winston';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
+
+// @public (undocumented)
+export function createRouter(options: RouterOptions): Promise;
+
+// @public (undocumented)
+export class DevToolsBackendApi {
+ constructor(logger: Logger, config: Config);
+ // (undocumented)
+ listConfig(): Promise;
+ // (undocumented)
+ listExternalDependencyDetails(): Promise;
+ // (undocumented)
+ listInfo(): Promise;
+}
+
+// @public (undocumented)
+export interface RouterOptions {
+ // (undocumented)
+ config: Config;
+ // (undocumented)
+ devToolsBackendApi?: DevToolsBackendApi;
+ // (undocumented)
+ logger: Logger;
+ // (undocumented)
+ permissions: PermissionEvaluator;
+}
+```
diff --git a/plugins/devtools-backend/config.d.ts b/plugins/devtools-backend/config.d.ts
new file mode 100644
index 0000000000..15e70afcf8
--- /dev/null
+++ b/plugins/devtools-backend/config.d.ts
@@ -0,0 +1,44 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export interface Config {
+ /**
+ * DevTools configuration.
+ */
+ devTools?: {
+ /**
+ * External dependency configuration.
+ */
+ externalDependencies?: {
+ /**
+ * The list of endpoints to check.
+ */
+ endpoints?: Array<{
+ /**
+ * The name of the endpoint.
+ */
+ name: string;
+ /**
+ * Type of check to perform; currently fetch or ping
+ */
+ type: string;
+ /**
+ * The target of the endpoint; currently either a URL for fetch or server name for ping.
+ */
+ target: string;
+ }>;
+ };
+ };
+}
diff --git a/plugins/devtools-backend/package.json b/plugins/devtools-backend/package.json
new file mode 100644
index 0000000000..d59cf963db
--- /dev/null
+++ b/plugins/devtools-backend/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@backstage/plugin-devtools-backend",
+ "version": "0.1.1-next.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/cli-common": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "@backstage/config-loader": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-auth-node": "workspace:^",
+ "@backstage/plugin-devtools-common": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
+ "@backstage/plugin-permission-node": "workspace:^",
+ "@backstage/types": "workspace:^",
+ "@manypkg/get-packages": "^1.1.3",
+ "@types/express": "*",
+ "@yarnpkg/lockfile": "^1.1.0",
+ "@yarnpkg/parsers": "^3.0.0-rc.4",
+ "express": "^4.18.1",
+ "express-promise-router": "^4.1.0",
+ "fs-extra": "^10.0.0",
+ "lodash": "^4.17.21",
+ "node-fetch": "^2.6.7",
+ "ping": "^0.4.1",
+ "semver": "^7.3.2",
+ "winston": "^3.2.1",
+ "yn": "^4.0.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@types/minimist": "^1.2.0",
+ "@types/ping": "^0.4.1",
+ "@types/supertest": "^2.0.8",
+ "@types/yarnpkg__lockfile": "^1.1.4",
+ "msw": "^1.0.0",
+ "supertest": "^6.2.4"
+ },
+ "files": [
+ "dist",
+ "config.d.ts"
+ ],
+ "configSchema": "config.d.ts"
+}
diff --git a/plugins/devtools-backend/src/api/DevToolsBackendApi.ts b/plugins/devtools-backend/src/api/DevToolsBackendApi.ts
new file mode 100644
index 0000000000..efd0407b31
--- /dev/null
+++ b/plugins/devtools-backend/src/api/DevToolsBackendApi.ts
@@ -0,0 +1,254 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Config, ConfigReader } from '@backstage/config';
+import { loadConfigSchema } from '@backstage/config-loader';
+import {
+ PackageDependency,
+ DevToolsInfo,
+ ExternalDependency,
+ Endpoint,
+ ExternalDependencyStatus,
+ ConfigInfo,
+} from '@backstage/plugin-devtools-common';
+
+import { JsonObject } from '@backstage/types';
+import { Logger } from 'winston';
+import fetch from 'node-fetch';
+import { findPaths } from '@backstage/cli-common';
+import { getPackages } from '@manypkg/get-packages';
+import ping from 'ping';
+import os from 'os';
+import fs from 'fs-extra';
+import { Lockfile } from '../util/Lockfile';
+import { memoize } from 'lodash';
+import { assertError } from '@backstage/errors';
+
+/** @public */
+export class DevToolsBackendApi {
+ public constructor(
+ private readonly logger: Logger,
+ private readonly config: Config,
+ ) {}
+
+ public async listExternalDependencyDetails(): Promise {
+ const result: ExternalDependency[] = [];
+
+ const endpoints = this.config.getOptional(
+ 'devTools.externalDependencies.endpoints',
+ );
+ if (!endpoints) {
+ // No external dependency endpoints configured
+ return result;
+ }
+ for (const endpoint of endpoints) {
+ this.logger?.info(
+ `Checking external dependency "${endpoint.name}" at "${endpoint.target}"`,
+ );
+
+ switch (endpoint.type) {
+ case 'ping': {
+ const pingResult = await this.pingExternalDependency(endpoint);
+ result.push(pingResult);
+ break;
+ }
+ case 'fetch': {
+ const fetchResult = await this.fetchExternalDependency(endpoint);
+ result.push(fetchResult);
+ break;
+ }
+ default:
+ return result;
+ }
+ }
+
+ return result;
+ }
+
+ private async fetchExternalDependency(
+ endpoint: Endpoint,
+ ): Promise {
+ let status;
+ let error;
+
+ await fetch(endpoint.target)
+ .then(res => {
+ status =
+ res.status === 200
+ ? ExternalDependencyStatus.healthy
+ : ExternalDependencyStatus.unhealthy;
+ this.logger.debug(
+ `Fetch for ${endpoint.name} resulted in status code "${res.status}"`,
+ );
+ })
+ .catch((err: Error) => {
+ this.logger.error(`Fetch failed for ${endpoint.name} - ${err.message}`);
+ error = err.message;
+ });
+
+ const result: ExternalDependency = {
+ name: endpoint.name,
+ type: endpoint.type,
+ target: endpoint.target,
+ status: status ?? ExternalDependencyStatus.unhealthy,
+ error: error ?? undefined,
+ };
+
+ return result;
+ }
+
+ private async pingExternalDependency(
+ endpoint: Endpoint,
+ ): Promise {
+ const pingResult = await ping.promise.probe(endpoint.target);
+
+ let error;
+ if (
+ pingResult.packetLoss === '100.000' ||
+ pingResult.packetLoss === 'unknown'
+ ) {
+ this.logger.error(
+ `Ping failed for ${endpoint.name} - ${pingResult.output}`,
+ );
+ error =
+ pingResult.output === ''
+ ? `${endpoint.target} - Unknown`
+ : pingResult.output;
+ }
+
+ this.logger.debug(
+ `Ping results for ${endpoint.name}: ${pingResult.output}`,
+ );
+
+ const result: ExternalDependency = {
+ name: endpoint.name,
+ type: endpoint.type,
+ target: endpoint.target,
+ status: pingResult.alive
+ ? ExternalDependencyStatus.healthy
+ : ExternalDependencyStatus.unhealthy,
+ error: error ?? undefined,
+ };
+
+ return result;
+ }
+
+ public async listConfig(): Promise {
+ /* eslint-disable-next-line no-restricted-syntax */
+ const paths = findPaths(__dirname);
+
+ const { packages } = await getPackages(paths.targetDir);
+ const schemaFunc = async () => {
+ return await loadConfigSchema({
+ dependencies: packages.map(p => p.packageJson.name),
+ });
+ };
+
+ const schemaMemo = memoize(schemaFunc);
+ const schema = await schemaMemo();
+
+ const configInfo: ConfigInfo = {
+ config: undefined,
+ error: undefined,
+ };
+ try {
+ const config = {
+ data: this.config.get() as JsonObject,
+ context: 'inline',
+ };
+ const sanitizedConfigs = schema.process([config], {
+ ignoreSchemaErrors: false,
+ valueTransform: (value, context) =>
+ context.visibility === 'secret' ? '' : value,
+ });
+
+ const data = ConfigReader.fromConfigs(sanitizedConfigs).get();
+ configInfo.config = data;
+ } catch (error) {
+ assertError(error);
+ // The config is not valid for some reason but we want to be able to see it still
+ const config = {
+ data: this.config.get() as JsonObject,
+ context: 'inline',
+ };
+ const sanitizedConfigs = schema.process([config], {
+ ignoreSchemaErrors: true,
+ valueTransform: (value, context) =>
+ context.visibility === 'secret' ? '' : value,
+ });
+
+ const data = ConfigReader.fromConfigs(sanitizedConfigs).get();
+ configInfo.config = data;
+ configInfo.error = {
+ name: error.name,
+ message: error.message,
+ messages: error.messages as string[] | undefined,
+ stack: error.stack,
+ };
+ }
+
+ return configInfo;
+ }
+
+ public async listInfo(): Promise {
+ const operatingSystem = `${os.type} ${os.release} - ${os.platform}/${os.arch}`;
+ const nodeJsVersion = process.version;
+
+ /* eslint-disable-next-line no-restricted-syntax */
+ const paths = findPaths(__dirname);
+ const backstageFile = paths.resolveTargetRoot('backstage.json');
+ let backstageJson = undefined;
+ if (fs.existsSync(backstageFile)) {
+ const buffer = await fs.readFile(backstageFile);
+ backstageJson = JSON.parse(buffer.toString());
+ }
+
+ const lockfilePath = paths.resolveTargetRoot('yarn.lock');
+ const lockfile = await Lockfile.load(lockfilePath);
+
+ const deps = [...lockfile.keys()].filter(n => n.startsWith('@backstage/'));
+
+ const infoDependencies: PackageDependency[] = [];
+ for (const dep of deps) {
+ const versions = new Set(lockfile.get(dep)!.map(i => i.version));
+ const infoDependency: PackageDependency = {
+ name: dep,
+ versions: [...versions].join(', '),
+ };
+ infoDependencies.push(infoDependency);
+ }
+
+ const info: DevToolsInfo = {
+ operatingSystem: operatingSystem ?? 'N/A',
+ nodeJsVersion: nodeJsVersion ?? 'N/A',
+ backstageVersion:
+ backstageJson && backstageJson.version ? backstageJson.version : 'N/A',
+ dependencies: infoDependencies,
+ };
+
+ return info;
+ }
+}
+
+export function isValidUrl(url: string): boolean {
+ try {
+ // eslint-disable-next-line no-new
+ new URL(url);
+ return true;
+ } catch {
+ return false;
+ }
+}
diff --git a/plugins/devtools-backend/src/api/index.ts b/plugins/devtools-backend/src/api/index.ts
new file mode 100644
index 0000000000..ed3390b113
--- /dev/null
+++ b/plugins/devtools-backend/src/api/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { DevToolsBackendApi } from './DevToolsBackendApi';
diff --git a/plugins/devtools-backend/src/index.ts b/plugins/devtools-backend/src/index.ts
new file mode 100644
index 0000000000..e1545e0022
--- /dev/null
+++ b/plugins/devtools-backend/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Backstage DevTools backend plugin that contains the API for retrieving package dependencies and current configuration which is used by the Backstage DevTools frontend plugin.
+ *
+ * @packageDocumentation
+ */
+
+export { DevToolsBackendApi } from './api';
+export * from './service/router';
diff --git a/plugins/devtools-backend/src/run.ts b/plugins/devtools-backend/src/run.ts
new file mode 100644
index 0000000000..d945aa13f0
--- /dev/null
+++ b/plugins/devtools-backend/src/run.ts
@@ -0,0 +1,33 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getRootLogger } from '@backstage/backend-common';
+import yn from 'yn';
+import { startStandaloneServer } from './service/standaloneServer';
+
+const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 7007;
+const enableCors = yn(process.env.PLUGIN_CORS, { default: false });
+const logger = getRootLogger();
+
+startStandaloneServer({ port, enableCors, logger }).catch(err => {
+ logger.error(err);
+ process.exit(1);
+});
+
+process.on('SIGINT', () => {
+ logger.info('CTRL+C pressed; exiting.');
+ process.exit(0);
+});
diff --git a/plugins/devtools-backend/src/service/router.test.ts b/plugins/devtools-backend/src/service/router.test.ts
new file mode 100644
index 0000000000..f3c78242cf
--- /dev/null
+++ b/plugins/devtools-backend/src/service/router.test.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { getVoidLogger } from '@backstage/backend-common';
+import { ConfigReader } from '@backstage/config';
+import express from 'express';
+import request from 'supertest';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
+import { createRouter } from './router';
+
+const mockedAuthorize: jest.MockedFunction =
+ jest.fn();
+const mockedPermissionQuery: jest.MockedFunction<
+ PermissionEvaluator['authorizeConditional']
+> = jest.fn();
+
+const permissionEvaluator: PermissionEvaluator = {
+ authorize: mockedAuthorize,
+ authorizeConditional: mockedPermissionQuery,
+};
+
+describe('createRouter', () => {
+ let app: express.Express;
+
+ beforeAll(async () => {
+ const router = await createRouter({
+ logger: getVoidLogger(),
+ config: new ConfigReader({
+ healthCheck: {
+ endpoint: [
+ {
+ name: '',
+ type: '',
+ target: '',
+ },
+ ],
+ },
+ }),
+ permissions: permissionEvaluator,
+ });
+ app = express().use(router);
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ describe('GET /health', () => {
+ it('returns ok', async () => {
+ const response = await request(app).get('/health');
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({ status: 'ok' });
+ });
+ });
+});
diff --git a/plugins/devtools-backend/src/service/router.ts b/plugins/devtools-backend/src/service/router.ts
new file mode 100644
index 0000000000..fbeeb5af84
--- /dev/null
+++ b/plugins/devtools-backend/src/service/router.ts
@@ -0,0 +1,137 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ AuthorizeResult,
+ PermissionEvaluator,
+} from '@backstage/plugin-permission-common';
+import {
+ devToolsConfigReadPermission,
+ devToolsExternalDependenciesReadPermission,
+ devToolsInfoReadPermission,
+ devToolsPermissions,
+} from '@backstage/plugin-devtools-common';
+
+import { Config } from '@backstage/config';
+import { DevToolsBackendApi } from '../api';
+import { Logger } from 'winston';
+import { NotAllowedError } from '@backstage/errors';
+import Router from 'express-promise-router';
+import { errorHandler } from '@backstage/backend-common';
+import express from 'express';
+import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
+import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
+
+/** @public */
+export interface RouterOptions {
+ devToolsBackendApi?: DevToolsBackendApi;
+ logger: Logger;
+ config: Config;
+ permissions: PermissionEvaluator;
+}
+
+/** @public */
+export async function createRouter(
+ options: RouterOptions,
+): Promise {
+ const { logger, config, permissions } = options;
+
+ const devToolsBackendApi =
+ options.devToolsBackendApi || new DevToolsBackendApi(logger, config);
+
+ const router = Router();
+ router.use(express.json());
+ router.use(
+ createPermissionIntegrationRouter({
+ permissions: devToolsPermissions,
+ }),
+ );
+
+ router.get('/health', (_req, res) => {
+ res.status(200).json({ status: 'ok' });
+ });
+
+ router.get('/info', async (req, response) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const decision = (
+ await permissions.authorize(
+ [{ permission: devToolsInfoReadPermission }],
+ {
+ token,
+ },
+ )
+ )[0];
+
+ if (decision.result === AuthorizeResult.DENY) {
+ throw new NotAllowedError('Unauthorized');
+ }
+
+ const info = await devToolsBackendApi.listInfo();
+
+ response.status(200).json(info);
+ });
+
+ router.get('/config', async (req, response) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const decision = (
+ await permissions.authorize(
+ [{ permission: devToolsConfigReadPermission }],
+ {
+ token,
+ },
+ )
+ )[0];
+
+ if (decision.result === AuthorizeResult.DENY) {
+ throw new NotAllowedError('Unauthorized');
+ }
+
+ const configList = await devToolsBackendApi.listConfig();
+
+ response.status(200).json(configList);
+ });
+
+ router.get('/external-dependencies', async (req, response) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const decision = (
+ await permissions.authorize(
+ [{ permission: devToolsExternalDependenciesReadPermission }],
+ {
+ token,
+ },
+ )
+ )[0];
+
+ if (decision.result === AuthorizeResult.DENY) {
+ throw new NotAllowedError('Unauthorized');
+ }
+
+ const health = await devToolsBackendApi.listExternalDependencyDetails();
+
+ response.status(200).json(health);
+ });
+
+ router.use(errorHandler());
+ return router;
+}
diff --git a/plugins/devtools-backend/src/service/standaloneServer.ts b/plugins/devtools-backend/src/service/standaloneServer.ts
new file mode 100644
index 0000000000..d2fdce169d
--- /dev/null
+++ b/plugins/devtools-backend/src/service/standaloneServer.ts
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ ServerTokenManager,
+ SingleHostDiscovery,
+ createServiceBuilder,
+ loadBackendConfig,
+} from '@backstage/backend-common';
+
+import { Logger } from 'winston';
+import { Server } from 'http';
+import { ServerPermissionClient } from '@backstage/plugin-permission-node';
+import { createRouter } from './router';
+
+export interface ServerOptions {
+ port: number;
+ enableCors: boolean;
+ logger: Logger;
+}
+
+export async function startStandaloneServer(
+ options: ServerOptions,
+): Promise {
+ const logger = options.logger.child({ service: 'devtools-backend-backend' });
+ const config = await loadBackendConfig({ logger, argv: process.argv });
+ const discovery = SingleHostDiscovery.fromConfig(config);
+ const tokenManager = ServerTokenManager.fromConfig(config, {
+ logger,
+ });
+ const permissions = ServerPermissionClient.fromConfig(config, {
+ discovery,
+ tokenManager,
+ });
+ logger.debug('Starting application server...');
+ const router = await createRouter({
+ logger,
+ config,
+ permissions,
+ });
+
+ let service = createServiceBuilder(module)
+ .setPort(options.port)
+ .addRouter('/devtools-backend', router);
+ if (options.enableCors) {
+ service = service.enableCors({ origin: 'http://localhost:3000' });
+ }
+
+ return await service.start().catch(err => {
+ logger.error(err);
+ process.exit(1);
+ });
+}
+
+module.hot?.accept();
diff --git a/plugins/devtools-backend/src/setupTests.ts b/plugins/devtools-backend/src/setupTests.ts
new file mode 100644
index 0000000000..813cdeaae3
--- /dev/null
+++ b/plugins/devtools-backend/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {};
diff --git a/plugins/devtools-backend/src/util/Lockfile.ts b/plugins/devtools-backend/src/util/Lockfile.ts
new file mode 100644
index 0000000000..52dbb847bc
--- /dev/null
+++ b/plugins/devtools-backend/src/util/Lockfile.ts
@@ -0,0 +1,317 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import fs from 'fs-extra';
+import semver from 'semver';
+import { parseSyml, stringifySyml } from '@yarnpkg/parsers';
+import { stringify as legacyStringifyLockfile } from '@yarnpkg/lockfile';
+
+const ENTRY_PATTERN = /^((?:@[^/]+\/)?[^@/]+)@(.+)$/;
+
+type LockfileData = {
+ [entry: string]: {
+ version: string;
+ resolved?: string;
+ integrity?: string;
+ dependencies?: { [name: string]: string };
+ };
+};
+
+type LockfileQueryEntry = {
+ range: string;
+ version: string;
+};
+
+/** Entries that have an invalid version range, for example an npm tag */
+type AnalyzeResultInvalidRange = {
+ name: string;
+ range: string;
+};
+
+/** Entries that can be deduplicated by bumping to an existing higher version */
+type AnalyzeResultNewVersion = {
+ name: string;
+ range: string;
+ oldVersion: string;
+ newVersion: string;
+};
+
+/** Entries that would need a dependency update in package.json to be deduplicated */
+type AnalyzeResultNewRange = {
+ name: string;
+ oldRange: string;
+ newRange: string;
+ oldVersion: string;
+ newVersion: string;
+};
+
+type AnalyzeResult = {
+ invalidRanges: AnalyzeResultInvalidRange[];
+ newVersions: AnalyzeResultNewVersion[];
+ newRanges: AnalyzeResultNewRange[];
+};
+
+function parseLockfile(lockfileContents: string) {
+ try {
+ return {
+ object: parseSyml(lockfileContents),
+ type: 'success',
+ };
+ } catch (err) {
+ return {
+ object: null,
+ type: err,
+ };
+ }
+}
+
+// the new yarn header is handled out of band of the parsing
+// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-core/sources/Project.ts#L1741-L1746
+const NEW_HEADER = `${[
+ `# This file is generated by running "yarn install" inside your project.\n`,
+ `# Manual changes might be lost - proceed with caution!\n`,
+].join(``)}\n`;
+
+function stringifyLockfile(data: LockfileData, legacy: boolean) {
+ return legacy
+ ? legacyStringifyLockfile(data)
+ : NEW_HEADER + stringifySyml(data);
+}
+// taken from yarn parser package
+// https://github.com/yarnpkg/berry/blob/0c5974f193a9397630e9aee2b3876cca62611149/packages/yarnpkg-parsers/sources/syml.ts#L136
+const LEGACY_REGEX = /^(#.*(\r?\n))*?#\s+yarn\s+lockfile\s+v1\r?\n/i;
+
+// these are special top level yarn keys.
+// https://github.com/yarnpkg/berry/blob/9bd61fbffb83d0b8166a9cc26bec3a58743aa453/packages/yarnpkg-parsers/sources/syml.ts#L9
+const SPECIAL_OBJECT_KEYS = [
+ `__metadata`,
+ `version`,
+ `resolution`,
+ `dependencies`,
+ `peerDependencies`,
+ `dependenciesMeta`,
+ `peerDependenciesMeta`,
+ `binaries`,
+];
+
+export class Lockfile {
+ static async load(path: string) {
+ const lockfileContents = await fs.readFile(path, 'utf8');
+ const legacy = LEGACY_REGEX.test(lockfileContents);
+ const lockfile = parseLockfile(lockfileContents);
+ if (lockfile.type !== 'success') {
+ throw new Error(`Failed yarn.lock parse with ${lockfile.type}`);
+ }
+
+ const data = lockfile.object as LockfileData;
+ const packages = new Map();
+
+ for (const [key, value] of Object.entries(data)) {
+ if (SPECIAL_OBJECT_KEYS.includes(key)) continue;
+
+ const [, name, range] = ENTRY_PATTERN.exec(key) ?? [];
+ if (!name) {
+ throw new Error(`Failed to parse yarn.lock entry '${key}'`);
+ }
+
+ let queries = packages.get(name);
+ if (!queries) {
+ queries = [];
+ packages.set(name, queries);
+ }
+ queries.push({ range, version: value.version });
+ }
+
+ return new Lockfile(path, packages, data, legacy);
+ }
+
+ private constructor(
+ private readonly path: string,
+ private readonly packages: Map,
+ private readonly data: LockfileData,
+ private readonly legacy: boolean = false,
+ ) {}
+
+ /** Get the entries for a single package in the lockfile */
+ get(name: string): LockfileQueryEntry[] | undefined {
+ return this.packages.get(name);
+ }
+
+ /** Returns the name of all packages available in the lockfile */
+ keys(): IterableIterator {
+ return this.packages.keys();
+ }
+
+ /** Analyzes the lockfile to identify possible actions and warnings for the entries */
+ analyze(options?: { filter?: (name: string) => boolean }): AnalyzeResult {
+ const { filter } = options ?? {};
+ const result: AnalyzeResult = {
+ invalidRanges: [],
+ newVersions: [],
+ newRanges: [],
+ };
+
+ for (const [name, allEntries] of this.packages) {
+ if (filter && !filter(name)) {
+ continue;
+ }
+
+ // Get rid of and signal any invalid ranges upfront
+ const invalid = allEntries.filter(e => !semver.validRange(e.range));
+ result.invalidRanges.push(
+ ...invalid.map(({ range }) => ({ name, range })),
+ );
+
+ // Grab all valid entries, if there aren't at least 2 different valid ones we're done
+ const entries = allEntries.filter(e => semver.validRange(e.range));
+ if (entries.length < 2) {
+ continue;
+ }
+
+ // Find all versions currently in use
+ const versions = Array.from(new Set(entries.map(e => e.version))).sort(
+ (v1, v2) => semver.rcompare(v1, v2),
+ );
+
+ // If we're not using at least 2 different versions we're done
+ if (versions.length < 2) {
+ continue;
+ }
+
+ const acceptedVersions = new Set();
+ for (const { version, range } of entries) {
+ // Finds the highest matching version from the the known versions
+ // TODO(Rugvip): We may want to select the version that satisfies the most ranges rather than the highest one
+ const acceptedVersion = versions.find(v => semver.satisfies(v, range));
+ if (!acceptedVersion) {
+ throw new Error(
+ `No existing version was accepted for range ${range}, searching through ${versions}, for package ${name}`,
+ );
+ }
+
+ if (acceptedVersion !== version) {
+ result.newVersions.push({
+ name,
+ range,
+ newVersion: acceptedVersion,
+ oldVersion: version,
+ });
+ }
+
+ acceptedVersions.add(acceptedVersion);
+ }
+
+ // If all ranges were able to accept the same version, we're done
+ if (acceptedVersions.size === 1) {
+ continue;
+ }
+
+ // Find the max version that we may want bump older packages to
+ const maxVersion = Array.from(acceptedVersions).sort(semver.rcompare)[0];
+ // Find all existing ranges that satisfy the new max version, and pick the one that
+ // results in the highest minimum allowed version, usually being the more specific one
+ const maxEntry = entries
+ .filter(e => semver.satisfies(maxVersion, e.range))
+ .map(e => ({ e, min: semver.minVersion(e.range) }))
+ .filter(p => p.min)
+ .sort((a, b) => semver.rcompare(a.min!, b.min!))[0]?.e;
+ if (!maxEntry) {
+ throw new Error(
+ `No entry found that satisfies max version '${maxVersion}'`,
+ );
+ }
+
+ // Find all entries that don't satisfy the max version
+ for (const { version, range } of entries) {
+ if (semver.satisfies(maxVersion, range)) {
+ continue;
+ }
+
+ result.newRanges.push({
+ name,
+ oldRange: range,
+ newRange: maxEntry.range,
+ oldVersion: version,
+ newVersion: maxVersion,
+ });
+ }
+ }
+
+ return result;
+ }
+
+ remove(name: string, range: string): boolean {
+ const query = `${name}@${range}`;
+ const existed = Boolean(this.data[query]);
+ delete this.data[query];
+
+ const newEntries = this.packages.get(name)?.filter(e => e.range !== range);
+ if (newEntries) {
+ this.packages.set(name, newEntries);
+ }
+
+ return existed;
+ }
+
+ /** Modifies the lockfile by bumping packages to the suggested versions */
+ replaceVersions(results: AnalyzeResultNewVersion[]) {
+ for (const { name, range, oldVersion, newVersion } of results) {
+ const query = `${name}@${range}`;
+
+ // Update the backing data
+ const entryData = this.data[query];
+ if (!entryData) {
+ throw new Error(`No entry data for ${query}`);
+ }
+ if (entryData.version !== oldVersion) {
+ throw new Error(
+ `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`,
+ );
+ }
+
+ // Modifying the data in the entry is not enough, we need to reference an existing version object
+ const matchingEntry = Object.entries(this.data).find(
+ ([q, e]) => q.startsWith(`${name}@`) && e.version === newVersion,
+ );
+ if (!matchingEntry) {
+ throw new Error(
+ `No matching entry found for ${name} at version ${newVersion}`,
+ );
+ }
+ this.data[query] = matchingEntry[1];
+
+ // Update our internal data structure
+ const entry = this.packages.get(name)?.find(e => e.range === range);
+ if (!entry) {
+ throw new Error(`No entry data for ${query}`);
+ }
+ if (entry.version !== oldVersion) {
+ throw new Error(
+ `Expected existing version data for ${query} to be ${oldVersion}, was ${entryData.version}`,
+ );
+ }
+ entry.version = newVersion;
+ }
+ }
+
+ async save() {
+ await fs.writeFile(this.path, this.toString(), 'utf8');
+ }
+
+ toString() {
+ return stringifyLockfile(this.data, this.legacy);
+ }
+}
diff --git a/plugins/devtools-common/.eslintrc.js b/plugins/devtools-common/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/devtools-common/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/devtools-common/CHANGELOG.md b/plugins/devtools-common/CHANGELOG.md
new file mode 100644
index 0000000000..6a566d0a67
--- /dev/null
+++ b/plugins/devtools-common/CHANGELOG.md
@@ -0,0 +1,19 @@
+# @backstage/plugin-devtools-common
+
+## 0.1.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/types@1.0.2
+ - @backstage/plugin-permission-common@0.7.5
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
diff --git a/plugins/devtools-common/README.md b/plugins/devtools-common/README.md
new file mode 100644
index 0000000000..5be5e58773
--- /dev/null
+++ b/plugins/devtools-common/README.md
@@ -0,0 +1,3 @@
+# DevTools Common
+
+Common types and permissions for the DevTools plugin.
diff --git a/plugins/devtools-common/api-report.md b/plugins/devtools-common/api-report.md
new file mode 100644
index 0000000000..6594b0d8a4
--- /dev/null
+++ b/plugins/devtools-common/api-report.md
@@ -0,0 +1,75 @@
+## API Report File for "@backstage/plugin-devtools-common"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BasicPermission } from '@backstage/plugin-permission-common';
+import { JsonValue } from '@backstage/types';
+
+// @public (undocumented)
+export type ConfigError = {
+ name: string;
+ message: string;
+ messages?: string[];
+ stack?: string;
+};
+
+// @public (undocumented)
+export type ConfigInfo = {
+ config?: JsonValue;
+ error?: ConfigError;
+};
+
+// @public (undocumented)
+export const devToolsAdministerPermission: BasicPermission;
+
+// @public (undocumented)
+export const devToolsConfigReadPermission: BasicPermission;
+
+// @public (undocumented)
+export const devToolsExternalDependenciesReadPermission: BasicPermission;
+
+// @public (undocumented)
+export type DevToolsInfo = {
+ operatingSystem: string;
+ nodeJsVersion: string;
+ backstageVersion: string;
+ dependencies: PackageDependency[];
+};
+
+// @public (undocumented)
+export const devToolsInfoReadPermission: BasicPermission;
+
+// @public
+export const devToolsPermissions: BasicPermission[];
+
+// @public (undocumented)
+export type Endpoint = {
+ name: string;
+ type: string;
+ target: string;
+};
+
+// @public (undocumented)
+export type ExternalDependency = {
+ name: string;
+ type: string;
+ target: string;
+ status: string;
+ error?: string;
+};
+
+// @public (undocumented)
+export enum ExternalDependencyStatus {
+ // (undocumented)
+ healthy = 'Healthy',
+ // (undocumented)
+ unhealthy = 'Unhealthy',
+}
+
+// @public (undocumented)
+export type PackageDependency = {
+ name: string;
+ versions: string;
+};
+```
diff --git a/plugins/devtools-common/package.json b/plugins/devtools-common/package.json
new file mode 100644
index 0000000000..0e114e39b9
--- /dev/null
+++ b/plugins/devtools-common/package.json
@@ -0,0 +1,35 @@
+{
+ "name": "@backstage/plugin-devtools-common",
+ "description": "Common functionalities for the devtools plugin",
+ "version": "0.1.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.cjs.js",
+ "module": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "common-library"
+ },
+ "scripts": {
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/plugin-permission-common": "workspace:^",
+ "@backstage/types": "workspace:^"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/devtools-common/src/index.ts b/plugins/devtools-common/src/index.ts
new file mode 100644
index 0000000000..be7426394b
--- /dev/null
+++ b/plugins/devtools-common/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * Common types and permissions for the DevTools frontend and backend plugins.
+ *
+ * @packageDocumentation
+ */
+
+export * from './types';
+export * from './permissions';
diff --git a/plugins/devtools-common/src/permissions.ts b/plugins/devtools-common/src/permissions.ts
new file mode 100644
index 0000000000..ba464f7d80
--- /dev/null
+++ b/plugins/devtools-common/src/permissions.ts
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createPermission } from '@backstage/plugin-permission-common';
+
+/**
+ * @public
+ */
+export const devToolsAdministerPermission = createPermission({
+ name: 'devtools.administer',
+ attributes: { action: 'read' },
+});
+
+/**
+ * @public
+ */
+export const devToolsInfoReadPermission = createPermission({
+ name: 'devtools.info',
+ attributes: { action: 'read' },
+});
+
+/**
+ * @public
+ */
+export const devToolsConfigReadPermission = createPermission({
+ name: 'devtools.config',
+ attributes: { action: 'read' },
+});
+
+/**
+ * @public
+ */
+export const devToolsExternalDependenciesReadPermission = createPermission({
+ name: 'devtools.external-dependencies',
+ attributes: { action: 'read' },
+});
+
+/**
+ * List of all Devtools permissions
+ *
+ * @public
+ */
+export const devToolsPermissions = [
+ devToolsAdministerPermission,
+ devToolsInfoReadPermission,
+ devToolsConfigReadPermission,
+ devToolsExternalDependenciesReadPermission,
+];
diff --git a/plugins/devtools-common/src/setupTests.ts b/plugins/devtools-common/src/setupTests.ts
new file mode 100644
index 0000000000..813cdeaae3
--- /dev/null
+++ b/plugins/devtools-common/src/setupTests.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export {};
diff --git a/plugins/devtools-common/src/types.ts b/plugins/devtools-common/src/types.ts
new file mode 100644
index 0000000000..d08232b155
--- /dev/null
+++ b/plugins/devtools-common/src/types.ts
@@ -0,0 +1,67 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { JsonValue } from '@backstage/types';
+
+/** @public */
+export type Endpoint = {
+ name: string;
+ type: string;
+ target: string;
+};
+
+/** @public */
+export type ExternalDependency = {
+ name: string;
+ type: string;
+ target: string;
+ status: string;
+ error?: string;
+};
+
+/** @public */
+export type DevToolsInfo = {
+ operatingSystem: string;
+ nodeJsVersion: string;
+ backstageVersion: string;
+ dependencies: PackageDependency[];
+};
+
+/** @public */
+export type PackageDependency = {
+ name: string;
+ versions: string;
+};
+
+/** @public */
+export enum ExternalDependencyStatus {
+ healthy = 'Healthy',
+ unhealthy = 'Unhealthy',
+}
+
+/** @public */
+export type ConfigInfo = {
+ config?: JsonValue;
+ error?: ConfigError;
+};
+
+/** @public */
+export type ConfigError = {
+ name: string;
+ message: string;
+ messages?: string[];
+ stack?: string;
+};
diff --git a/plugins/devtools/.eslintrc.js b/plugins/devtools/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/devtools/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/devtools/CHANGELOG.md b/plugins/devtools/CHANGELOG.md
new file mode 100644
index 0000000000..c9db73ff44
--- /dev/null
+++ b/plugins/devtools/CHANGELOG.md
@@ -0,0 +1,46 @@
+# @backstage/plugin-devtools
+
+## 0.1.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/theme@0.4.0-next.0
+ - @backstage/core-components@0.13.2-next.0
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-devtools-common@0.1.0
+
+## 0.1.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-common@0.1.0
+ - @backstage/theme@0.3.0
+ - @backstage/core-components@0.13.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-permission-react@0.4.12
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 347aeca204c: Introduced the DevTools plugin, checkout the plugin's [`README.md`](https://github.com/backstage/backstage/tree/master/plugins/devtools) for more details!
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-devtools-common@0.1.0-next.0
+ - @backstage/theme@0.3.0-next.0
+ - @backstage/core-components@0.13.1-next.1
+ - @backstage/core-plugin-api@1.5.1
+ - @backstage/plugin-permission-react@0.4.12
diff --git a/plugins/devtools/README.md b/plugins/devtools/README.md
new file mode 100644
index 0000000000..26226d2976
--- /dev/null
+++ b/plugins/devtools/README.md
@@ -0,0 +1,375 @@
+# DevTools
+
+Welcome to the DevTools plugin! This plugin helps elevate useful information about a Backstage instance through the frontend, which can be helpful for an integrator for troubleshooting, reviewing, and understanding their installation.
+
+## Features
+
+The DevTools plugin comes with two tabs out of the box.
+
+### Info
+
+Lists helpful information about your current running Backstage instance such as: OS, NodeJS version, Backstage version, and package versions.
+
+
+
+### Config
+
+Lists the configuration being used by your current running Backstage instance.
+
+
+
+## Optional Features
+
+The DevTools plugin can be setup with other tabs with additional helpful features.
+
+### External Dependencies
+
+Lists the status of configured External Dependencies based on your current running Backstage instance's ability to reach them.
+
+
+
+## Setup
+
+The following sections will help you get the DevTools plugin setup and running.
+
+### Backend
+
+You need to setup the [DevTools backend plugin](../devtools-backend/README.md) before you move forward with any of the following steps if you haven't already.
+
+### Frontend
+
+To setup the DevTools frontend you'll need to do the following steps:
+
+1. First we need to add the `@backstage/plugin-devtools` package to your frontend app:
+
+ ```sh
+ # From your Backstage root directory
+ yarn add --cwd packages/app @backstage/plugin-devtools
+ ```
+
+2. Now open the `packages/app/src/App.tsx` file
+3. Then after all the import statements add the following line:
+
+ ```ts
+ import { DevToolsPage } from '@backstage/plugin-devtools';
+ ```
+
+4. In this same file just before the closing ` FlatRoutes>`, this will be near the bottom of the file, add this line:
+
+ ```ts
+ } />
+ ```
+
+5. Next open the `packages/app/src/components/Root/Root.tsx` file
+6. We want to add this icon import after all the existing import statements:
+
+ ```ts
+ import BuildIcon from '@material-ui/icons/Build';
+ ```
+
+7. Then add this line just after the `` line:
+
+ ```ts
+
+ ```
+
+8. Now run `yarn dev` from the root of your project and you should see the DevTools option show up just below Settings in your sidebar and clicking on it will get you to the [Info tab](#info)
+
+## Customizing
+
+The DevTools plugin has been designed so that you can customize the tabs to suite your needs. You may only want some or none of the out of the box tabs or you may want to add your own. The following sections explains how to do that (assuming you've already done the [setup steps](#setup)). As part of this example we'll also be showing how you can add the optional [External Dependencies](#external-dependencies) tab.
+
+1. In the `packages/app/src/components` folder create a new sub-folder called `devtools`
+2. Then in this new `devtools` folder add a file called `CustomDevToolsPage.tsx`
+3. In the `CustomDevToolsPage.tsx` file add the following content:
+
+ ```tsx
+ import {
+ ConfigContent,
+ ExternalDependenciesContent,
+ InfoContent,
+ } from '@backstage/plugin-devtools';
+ import { DevToolsLayout } from '@backstage/plugin-devtools';
+ import React from 'react';
+
+ export const DevToolsPage = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ );
+ };
+
+ export const customDevToolsPage = ;
+ ```
+
+4. Now open the `packages/app/src/App.tsx` file and add the following import after all the existing import statements:
+
+ ```ts
+ import { customDevToolsPage } from './components/devtools/CustomDevToolsPage';
+ ```
+
+5. Then we need to adjust our route as follows
+
+ ```diff
+ - } />
+ + } >
+ + {customDevToolsPage}
+ +
+ ```
+
+6. Now run `yarn dev` from the root of your project. When you go to the DevTools you'll now see you have a third tab for [External Dependencies](#external-dependencies)
+
+With this setup you can add or remove the tabs as you'd like or add your own simply by editing your `CustomDevToolsPage.tsx` file
+
+## Permissions
+
+The DevTools plugin supports the [permissions framework](https://backstage.io/docs/permissions/overview), the following sections outline how you can use them with the assumption that you have the permissions framework setup and working.
+
+**Note:** These sections are intended as guidance and are completely optional. The DevTools plugin will work with the permission framework off or on without any specific policy setup.
+
+### Secure Sidebar Option
+
+To use the permission framework to secure the DevTools sidebar option you'll want to do the following:
+
+1. First we need to add the `@backstage/plugin-devtools-common` package to your frontend app:
+
+ ```sh
+ # From your Backstage root directory
+ yarn add --cwd packages/app @backstage/plugin-devtools-common
+ ```
+
+2. Then open the `packages/app/src/components/Root/Root.tsx` file
+3. The add these imports after all the existing import statements:
+
+ ```ts
+ import { devToolsAdministerPermission } from '@backstage/plugin-devtools-common';
+ import { RequirePermission } from '@backstage/plugin-permission-react';
+ ```
+
+4. Then make the following change:
+
+ ```diff
+ -
+ + >}>
+ +
+ +
+ ```
+
+### Secure the DevTools Route
+
+To use the permission framework to secure the DevTools route you'll want to do the following:
+
+1. First we need to add the `@backstage/plugin-devtools-common` package to your frontend app (skip this step if you've already done this):
+
+ ```sh
+ # From your Backstage root directory
+ yarn add --cwd packages/app @backstage/plugin-devtools-common
+ ```
+
+2. Then open the `packages/app/src/App.tsx` file
+3. The add this import after all the existing import statements:
+
+ ```ts
+ import { devToolsAdministerPermission } from '@backstage/plugin-devtools-common';
+ ```
+
+4. Then make the following change:
+
+ ```diff
+ - } />
+ +
+ +
+ +
+ + }
+ + />
+ ```
+
+Note: if you are using a `customDevToolsPage` as per the [Customizing](#customizing) documentation the changes for Step 4 will be:
+
+```diff
+- } />
++
++
++
++ }
++ >
++ {customDevToolsPage}
++
+```
+
+### Permission Policy
+
+Here is an example permission policy that you might use to secure the DevTools plugin:
+
+```ts
+// packages/backend/src/plugins/permission.ts
+
+class TestPermissionPolicy implements PermissionPolicy {
+ async handle(request: PolicyQuery): Promise {
+ if (isPermission(request.permission, devToolsAdministerPermission)) {
+ if (
+ user?.identity.ownershipEntityRefs.includes(
+ 'group:default/backstage-admins',
+ )
+ ) {
+ return { result: AuthorizeResult.ALLOW };
+ }
+ return { result: AuthorizeResult.DENY };
+ }
+
+ if (isPermission(request.permission, devToolsInfoReadPermission)) {
+ if (
+ user?.identity.ownershipEntityRefs.includes(
+ 'group:default/backstage-admins',
+ )
+ ) {
+ return { result: AuthorizeResult.ALLOW };
+ }
+ return { result: AuthorizeResult.DENY };
+ }
+
+ if (isPermission(request.permission, devToolsConfigReadPermission)) {
+ if (
+ user?.identity.ownershipEntityRefs.includes(
+ 'group:default/backstage-admins',
+ )
+ ) {
+ return { result: AuthorizeResult.ALLOW };
+ }
+ return { result: AuthorizeResult.DENY };
+ }
+
+ if (
+ isPermission(
+ request.permission,
+ devToolsExternalDependenciesReadPermission,
+ )
+ ) {
+ if (
+ user?.identity.ownershipEntityRefs.includes(
+ 'group:default/backstage-admins',
+ )
+ ) {
+ return { result: AuthorizeResult.ALLOW };
+ }
+ return { result: AuthorizeResult.DENY };
+ }
+
+ return { result: AuthorizeResult.ALLOW };
+ }
+}
+```
+
+To use this policy you'll need to make sure to add the `@backstage/plugin-devtools-common` package to your backend you can do that by running this command:
+
+```sh
+# From your Backstage root directory
+yarn add --cwd packages/backend @backstage/plugin-devtools-common
+```
+
+You'll also need to add these imports:
+
+```ts
+import {
+ devToolsAdministerPermission,
+ devToolsConfigReadPermission,
+ devToolsExternalDependenciesReadPermission,
+ devToolsInfoReadPermission,
+} from '@backstage/plugin-devtools-common';
+```
+
+**Note:** The group "group:default/backstage-admins" is simply an example and does not exist. You can point this to any group you have in your catalog instead.
+
+### Customizing with Permissions
+
+If you followed the [Customizing](#customizing) documentation and want to use permission there this is what your `CustomDevToolsPage.tsx` would look like:
+
+```tsx
+import {
+ ConfigContent,
+ ExternalDependenciesContent,
+ InfoContent,
+} from '@backstage/plugin-devtools';
+import { DevToolsLayout } from '@backstage/plugin-devtools';
+import {
+ devToolsConfigReadPermission,
+ devToolsExternalDependenciesReadPermission,
+ devToolsInfoReadPermission,
+} from '@backstage/plugin-devtools-common';
+import { RequirePermission } from '@backstage/plugin-permission-react';
+import React from 'react';
+
+const DevToolsPage = () => {
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
+
+export const customDevToolsPage = ;
+```
+
+## Configuration
+
+The following sections outline the configuration for the DevTools plugin
+
+### External Dependencies Configuration
+
+If you decide to use the External Dependencies tab then you'll need to setup the configuration for it in your `app-config.yaml`, if there is no config setup then the tab will be empty. Here's an example:
+
+```yaml
+devTools:
+ externalDependencies:
+ endpoints:
+ - name: 'Google'
+ type: 'fetch'
+ target: 'https://google.ca'
+ - name: 'Google Public DNS'
+ type: 'ping'
+ target: '8.8.8.8'
+```
+
+Configuration details:
+
+- `endpoints` is an array
+- `name` is the friendly name for your endpoint
+- `type` can be either `ping` or `fetch` and will perform the respective action on the `target`
+- `target` is either a URL or server that you want to trigger a `type` action on
diff --git a/plugins/devtools/api-report.md b/plugins/devtools/api-report.md
new file mode 100644
index 0000000000..c9cfbf77e5
--- /dev/null
+++ b/plugins/devtools/api-report.md
@@ -0,0 +1,57 @@
+## API Report File for "@backstage/plugin-devtools"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { BackstagePlugin } from '@backstage/core-plugin-api';
+import { default as default_2 } from 'react';
+import { RouteRef } from '@backstage/core-plugin-api';
+import { TabProps } from '@material-ui/core';
+
+// @public (undocumented)
+export const ConfigContent: () => JSX.Element;
+
+// @public
+export const DevToolsLayout: {
+ ({ children }: DevToolsLayoutProps): JSX.Element;
+ Route: (props: SubRoute) => null;
+};
+
+// @public (undocumented)
+export type DevToolsLayoutProps = {
+ children?: default_2.ReactNode;
+};
+
+// @public (undocumented)
+export const DevToolsPage: () => JSX.Element;
+
+// @public (undocumented)
+export const devToolsPlugin: BackstagePlugin<
+ {
+ root: RouteRef;
+ },
+ {},
+ {}
+>;
+
+// @public (undocumented)
+export const ExternalDependenciesContent: () => JSX.Element;
+
+// @public (undocumented)
+export const InfoContent: () => JSX.Element;
+
+// @public (undocumented)
+export type SubRoute = {
+ path: string;
+ title: string;
+ children: JSX.Element;
+ tabProps?: TabProps<
+ default_2.ElementType,
+ {
+ component?: default_2.ElementType;
+ }
+ >;
+};
+```
diff --git a/plugins/devtools/dev/index.tsx b/plugins/devtools/dev/index.tsx
new file mode 100644
index 0000000000..00c966a801
--- /dev/null
+++ b/plugins/devtools/dev/index.tsx
@@ -0,0 +1,27 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import React from 'react';
+import { createDevApp } from '@backstage/dev-utils';
+import { devToolsPlugin, DevToolsPage } from '../src/plugin';
+
+createDevApp()
+ .registerPlugin(devToolsPlugin)
+ .addPage({
+ element: ,
+ title: 'Root Page',
+ path: '/devtools',
+ })
+ .render();
diff --git a/plugins/devtools/docs/devtools-config-tab.png b/plugins/devtools/docs/devtools-config-tab.png
new file mode 100644
index 0000000000..b5c22f691b
Binary files /dev/null and b/plugins/devtools/docs/devtools-config-tab.png differ
diff --git a/plugins/devtools/docs/devtools-external-dependencies.png b/plugins/devtools/docs/devtools-external-dependencies.png
new file mode 100644
index 0000000000..9356d83ef5
Binary files /dev/null and b/plugins/devtools/docs/devtools-external-dependencies.png differ
diff --git a/plugins/devtools/docs/devtools-info-tab.png b/plugins/devtools/docs/devtools-info-tab.png
new file mode 100644
index 0000000000..739045a77f
Binary files /dev/null and b/plugins/devtools/docs/devtools-info-tab.png differ
diff --git a/plugins/devtools/package.json b/plugins/devtools/package.json
new file mode 100644
index 0000000000..3023c83d71
--- /dev/null
+++ b/plugins/devtools/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "@backstage/plugin-devtools",
+ "version": "0.1.1-next.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "frontend-plugin"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/devtools"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "clean": "backstage-cli package clean",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack"
+ },
+ "dependencies": {
+ "@backstage/core-components": "workspace:^",
+ "@backstage/core-plugin-api": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/plugin-devtools-common": "workspace:^",
+ "@backstage/plugin-permission-react": "workspace:^",
+ "@backstage/theme": "workspace:^",
+ "@backstage/types": "workspace:^",
+ "@material-ui/core": "^4.9.13",
+ "@material-ui/icons": "^4.9.1",
+ "@material-ui/lab": "^4.0.0-alpha.57",
+ "react-json-view": "^1.21.3",
+ "react-use": "^17.2.4"
+ },
+ "peerDependencies": {
+ "@types/react": "^16.13.1 || ^17.0.0",
+ "react": "^16.13.1 || ^17.0.0",
+ "react-router-dom": "6.0.0-beta.0 || ^6.3.0"
+ },
+ "devDependencies": {
+ "@backstage/cli": "workspace:^",
+ "@backstage/core-app-api": "workspace:^",
+ "@backstage/dev-utils": "workspace:^",
+ "@backstage/test-utils": "workspace:^",
+ "@testing-library/jest-dom": "^5.10.1",
+ "@testing-library/react": "^12.1.3",
+ "@testing-library/user-event": "^14.0.0",
+ "@types/node": "*",
+ "cross-fetch": "^3.1.5",
+ "msw": "^1.0.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/devtools/src/api/DevToolsApi.ts b/plugins/devtools/src/api/DevToolsApi.ts
new file mode 100644
index 0000000000..ff58a4ea16
--- /dev/null
+++ b/plugins/devtools/src/api/DevToolsApi.ts
@@ -0,0 +1,32 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createApiRef } from '@backstage/core-plugin-api';
+import {
+ ConfigInfo,
+ DevToolsInfo,
+ ExternalDependency,
+} from '@backstage/plugin-devtools-common';
+
+export const devToolsApiRef = createApiRef({
+ id: 'plugin.devtools.service',
+});
+
+export interface DevToolsApi {
+ getConfig(): Promise;
+ getExternalDependencies(): Promise;
+ getInfo(): Promise;
+}
diff --git a/plugins/devtools/src/api/DevToolsClient.ts b/plugins/devtools/src/api/DevToolsClient.ts
new file mode 100644
index 0000000000..0be8294b3f
--- /dev/null
+++ b/plugins/devtools/src/api/DevToolsClient.ts
@@ -0,0 +1,78 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { DiscoveryApi, IdentityApi } from '@backstage/core-plugin-api';
+import {
+ ConfigInfo,
+ DevToolsInfo,
+ ExternalDependency,
+} from '@backstage/plugin-devtools-common';
+import { ResponseError } from '@backstage/errors';
+import { DevToolsApi } from './DevToolsApi';
+
+export class DevToolsClient implements DevToolsApi {
+ private readonly discoveryApi: DiscoveryApi;
+ private readonly identityApi: IdentityApi;
+
+ public constructor(options: {
+ discoveryApi: DiscoveryApi;
+ identityApi: IdentityApi;
+ }) {
+ this.discoveryApi = options.discoveryApi;
+ this.identityApi = options.identityApi;
+ }
+
+ public async getConfig(): Promise {
+ const urlSegment = 'config';
+
+ const configInfo = await this.get(urlSegment);
+ return configInfo;
+ }
+
+ public async getExternalDependencies(): Promise<
+ ExternalDependency[] | undefined
+ > {
+ const urlSegment = 'external-dependencies';
+
+ const externalDependencies = await this.get<
+ ExternalDependency[] | undefined
+ >(urlSegment);
+ return externalDependencies;
+ }
+
+ public async getInfo(): Promise {
+ const urlSegment = 'info';
+
+ const info = await this.get(urlSegment);
+ return info;
+ }
+
+ private async get(path: string): Promise {
+ const baseUrl = `${await this.discoveryApi.getBaseUrl('devtools')}/`;
+ const url = new URL(path, baseUrl);
+
+ const { token } = await this.identityApi.getCredentials();
+ const response = await fetch(url.toString(), {
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
+ });
+
+ if (!response.ok) {
+ throw await ResponseError.fromResponse(response);
+ }
+
+ return response.json() as Promise;
+ }
+}
diff --git a/plugins/devtools/src/api/index.ts b/plugins/devtools/src/api/index.ts
new file mode 100644
index 0000000000..6bea0a07aa
--- /dev/null
+++ b/plugins/devtools/src/api/index.ts
@@ -0,0 +1,18 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './DevToolsApi';
+export * from './DevToolsClient';
diff --git a/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx b/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx
new file mode 100644
index 0000000000..66129b5dae
--- /dev/null
+++ b/plugins/devtools/src/components/Content/ConfigContent/ConfigContent.tsx
@@ -0,0 +1,95 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Progress, WarningPanel } from '@backstage/core-components';
+import {
+ Box,
+ createStyles,
+ makeStyles,
+ Paper,
+ Theme,
+ Typography,
+ useTheme,
+} from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import React from 'react';
+import ReactJson from 'react-json-view';
+import { useConfig } from '../../../hooks';
+import { ConfigError } from '@backstage/plugin-devtools-common';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ warningStyle: {
+ paddingBottom: theme.spacing(2),
+ },
+ paperStyle: {
+ padding: theme.spacing(2),
+ },
+ }),
+);
+
+export const WarningContent = ({ error }: { error: ConfigError }) => {
+ if (!error.messages) {
+ return {error.message};
+ }
+
+ const messages = error.messages as string[];
+
+ return (
+
+ {messages.map(message => (
+ {message}
+ ))}
+
+ );
+};
+
+/** @public */
+export const ConfigContent = () => {
+ const classes = useStyles();
+ const theme = useTheme();
+ const { configInfo, loading, error } = useConfig();
+
+ if (loading) {
+ return ;
+ } else if (error) {
+ return {error.message};
+ }
+
+ if (!configInfo) {
+ return Unable to load config data;
+ }
+
+ return (
+
+ {configInfo && configInfo.error && (
+
+
+
+
+
+ )}
+
+
+
+
+ );
+};
diff --git a/plugins/devtools/src/components/Content/ConfigContent/index.ts b/plugins/devtools/src/components/Content/ConfigContent/index.ts
new file mode 100644
index 0000000000..b1cc671de8
--- /dev/null
+++ b/plugins/devtools/src/components/Content/ConfigContent/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { ConfigContent } from './ConfigContent';
diff --git a/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx b/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx
new file mode 100644
index 0000000000..a5dd825fe5
--- /dev/null
+++ b/plugins/devtools/src/components/Content/ExternalDependenciesContent/ExternalDependenciesContent.tsx
@@ -0,0 +1,140 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ Progress,
+ StatusError,
+ StatusOK,
+ StatusWarning,
+ Table,
+ TableColumn,
+} from '@backstage/core-components';
+import { ExternalDependency } from '@backstage/plugin-devtools-common';
+import {
+ Box,
+ createStyles,
+ Grid,
+ makeStyles,
+ Paper,
+ Theme,
+ Typography,
+} from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import React from 'react';
+import { useExternalDependencies } from '../../../hooks';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ paperStyle: {
+ padding: theme.spacing(2),
+ },
+ }),
+);
+
+export const getExternalDependencyStatus = (
+ result: Partial | undefined,
+) => {
+ switch (result?.status) {
+ case 'Healthy':
+ return (
+
+ {result.status}
+
+ );
+ case 'Unhealthy':
+ return (
+
+ {`${result.status}`}
+
+ );
+ case undefined:
+ default:
+ return (
+
+ Unknown
+
+ );
+ }
+};
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ width: 'auto',
+ field: 'name',
+ },
+ {
+ title: 'Target',
+ width: 'auto',
+ field: 'target',
+ },
+ {
+ title: 'Type',
+ width: 'auto',
+ field: 'type',
+ },
+ {
+ title: 'Status',
+ width: 'auto',
+ render: (row: Partial) => (
+
+
+
+ {getExternalDependencyStatus(row)}
+
+
+ {row.error && {row.error}}
+
+ ),
+ },
+];
+
+/** @public */
+export const ExternalDependenciesContent = () => {
+ const classes = useStyles();
+ const { externalDependencies, loading, error } = useExternalDependencies();
+
+ if (loading) {
+ return ;
+ } else if (error) {
+ return {error.message};
+ }
+
+ if (!externalDependencies || externalDependencies.length === 0) {
+ return (
+
+
+ No external dependencies found
+
+
+ );
+ }
+
+ return (
+
+ );
+};
diff --git a/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts b/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts
new file mode 100644
index 0000000000..bf747f3065
--- /dev/null
+++ b/plugins/devtools/src/components/Content/ExternalDependenciesContent/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { ExternalDependenciesContent } from './ExternalDependenciesContent';
diff --git a/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx b/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx
new file mode 100644
index 0000000000..32f35600f2
--- /dev/null
+++ b/plugins/devtools/src/components/Content/InfoContent/BackstageLogoIcon.tsx
@@ -0,0 +1,25 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { SvgIcon, SvgIconProps } from '@material-ui/core';
+
+import React from 'react';
+
+export const BackstageLogoIcon = (props: SvgIconProps) => (
+
+
+
+);
diff --git a/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx b/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx
new file mode 100644
index 0000000000..7a0ebb3e06
--- /dev/null
+++ b/plugins/devtools/src/components/Content/InfoContent/InfoContent.tsx
@@ -0,0 +1,138 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Progress } from '@backstage/core-components';
+import {
+ Avatar,
+ Box,
+ createStyles,
+ Divider,
+ List,
+ ListItem,
+ ListItemAvatar,
+ ListItemText,
+ makeStyles,
+ Paper,
+ Theme,
+} from '@material-ui/core';
+import { Alert } from '@material-ui/lab';
+import React from 'react';
+import { useInfo } from '../../../hooks';
+import { InfoDependenciesTable } from './InfoDependenciesTable';
+import DescriptionIcon from '@material-ui/icons/Description';
+import DeveloperBoardIcon from '@material-ui/icons/DeveloperBoard';
+import { BackstageLogoIcon } from './BackstageLogoIcon';
+import FileCopyIcon from '@material-ui/icons/FileCopy';
+import { DevToolsInfo } from '@backstage/plugin-devtools-common';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ paperStyle: {
+ marginBottom: theme.spacing(2),
+ },
+ flexContainer: {
+ display: 'flex',
+ flexDirection: 'row',
+ padding: 0,
+ },
+ copyButton: {
+ float: 'left',
+ margin: theme.spacing(2),
+ },
+ }),
+);
+
+const copyToClipboard = ({ about }: { about: DevToolsInfo | undefined }) => {
+ if (about) {
+ let formatted = `OS: ${about.operatingSystem}\nnode: ${about.nodeJsVersion}\nbackstage: ${about.backstageVersion}\nDependencies:\n`;
+ const deps = about.dependencies;
+ for (const key in deps) {
+ if (Object.prototype.hasOwnProperty.call(deps, key)) {
+ formatted = `${formatted} ${deps[key].name}: ${deps[key].versions}\n`;
+ }
+ }
+ window.navigator.clipboard.writeText(formatted);
+ }
+};
+
+/** @public */
+export const InfoContent = () => {
+ const classes = useStyles();
+ const { about, loading, error } = useInfo();
+
+ if (loading) {
+ return ;
+ } else if (error) {
+ return {error.message};
+ }
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {
+ copyToClipboard({ about });
+ }}
+ className={classes.copyButton}
+ >
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx b/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx
new file mode 100644
index 0000000000..e1ab59f279
--- /dev/null
+++ b/plugins/devtools/src/components/Content/InfoContent/InfoDependenciesTable.tsx
@@ -0,0 +1,55 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Table, TableColumn } from '@backstage/core-components';
+import { PackageDependency } from '@backstage/plugin-devtools-common';
+
+import React from 'react';
+
+const columns: TableColumn[] = [
+ {
+ title: 'Name',
+ width: 'auto',
+ field: 'name',
+ defaultSort: 'asc',
+ },
+ {
+ title: 'Versions',
+ width: 'auto',
+ field: 'versions',
+ },
+];
+
+export const InfoDependenciesTable = ({
+ infoDependencies,
+}: {
+ infoDependencies: PackageDependency[] | undefined;
+}) => {
+ return (
+
+ );
+};
diff --git a/plugins/devtools/src/components/Content/InfoContent/index.ts b/plugins/devtools/src/components/Content/InfoContent/index.ts
new file mode 100644
index 0000000000..6d5bad53f8
--- /dev/null
+++ b/plugins/devtools/src/components/Content/InfoContent/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { InfoContent } from './InfoContent';
diff --git a/plugins/devtools/src/components/Content/index.ts b/plugins/devtools/src/components/Content/index.ts
new file mode 100644
index 0000000000..f5bd64f7c0
--- /dev/null
+++ b/plugins/devtools/src/components/Content/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './ConfigContent';
+export * from './InfoContent';
+export * from './ExternalDependenciesContent';
diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx
new file mode 100644
index 0000000000..a5e4f2439b
--- /dev/null
+++ b/plugins/devtools/src/components/DefaultDevToolsPage/DefaultDevToolsPage.tsx
@@ -0,0 +1,42 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import {
+ devToolsConfigReadPermission,
+ devToolsInfoReadPermission,
+} from '@backstage/plugin-devtools-common';
+
+import { ConfigContent } from '../Content/ConfigContent';
+import { DevToolsLayout } from '../DevToolsLayout';
+import { InfoContent } from '../Content/InfoContent';
+import React from 'react';
+import { RequirePermission } from '@backstage/plugin-permission-react';
+
+/** @public */
+export const DefaultDevToolsPage = () => (
+
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/plugins/devtools/src/components/DefaultDevToolsPage/index.ts b/plugins/devtools/src/components/DefaultDevToolsPage/index.ts
new file mode 100644
index 0000000000..133dcf6e3a
--- /dev/null
+++ b/plugins/devtools/src/components/DefaultDevToolsPage/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { DefaultDevToolsPage } from './DefaultDevToolsPage';
diff --git a/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx
new file mode 100644
index 0000000000..12cd3f7666
--- /dev/null
+++ b/plugins/devtools/src/components/DevToolsLayout/DevToolsLayout.tsx
@@ -0,0 +1,79 @@
+/*
+ * Copyright 2022 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Header, Page, RoutedTabs } from '@backstage/core-components';
+import {
+ attachComponentData,
+ useElementFilter,
+} from '@backstage/core-plugin-api';
+import { TabProps } from '@material-ui/core';
+import { default as React } from 'react';
+
+/** @public */
+export type SubRoute = {
+ path: string;
+ title: string;
+ children: JSX.Element;
+ tabProps?: TabProps;
+};
+
+const dataKey = 'plugin.devtools.devtoolsLayoutRoute';
+
+const Route: (props: SubRoute) => null = () => null;
+attachComponentData(Route, dataKey, true);
+
+// This causes all mount points that are discovered within this route to use the path of the route itself
+attachComponentData(Route, 'core.gatherMountPoints', true);
+
+/** @public */
+export type DevToolsLayoutProps = {
+ children?: React.ReactNode;
+};
+
+/**
+ * DevTools is a compound component, which allows you to define a custom layout
+ *
+ * @example
+ * ```jsx
+ *
+ *
+ *
This is rendered under /example/anything-here route