Build a plugin
diff --git a/microsite/src/pages/home/home.module.scss b/microsite/src/pages/home/home.module.scss
index 57f2b9acde..5877f7b5f5 100644
--- a/microsite/src/pages/home/home.module.scss
+++ b/microsite/src/pages/home/home.module.scss
@@ -1,4 +1,4 @@
-$desktopUpBreakpoint: 997px;
+@use '../../theme/variables.scss';
.homePage {
font-size: 1.25rem;
@@ -70,7 +70,7 @@ $desktopUpBreakpoint: 997px;
}
.catalogContainer {
- @media (min-width: $desktopUpBreakpoint) {
+ @media (min-width: variables.$desktopUpBreakpoint) {
grid-template-columns: repeat(4, 1fr);
picture {
@@ -85,7 +85,7 @@ $desktopUpBreakpoint: 997px;
}
.softwareTemplatesContainer {
- @media (min-width: $desktopUpBreakpoint) {
+ @media (min-width: variables.$desktopUpBreakpoint) {
grid: repeat(2, auto) / repeat(4, 1fr);
picture {
diff --git a/microsite/src/pages/plugins/_pluginCard.tsx b/microsite/src/pages/plugins/_pluginCard.tsx
index 99f27b865d..d6b4017db5 100644
--- a/microsite/src/pages/plugins/_pluginCard.tsx
+++ b/microsite/src/pages/plugins/_pluginCard.tsx
@@ -35,9 +35,7 @@ export const PluginCard = ({
by
{author}
-
- {category}
-
+
{category}
>
}
body={
{description}
}
diff --git a/microsite/src/pages/plugins/plugins.module.scss b/microsite/src/pages/plugins/plugins.module.scss
index d21444583a..bb08c2fd33 100644
--- a/microsite/src/pages/plugins/plugins.module.scss
+++ b/microsite/src/pages/plugins/plugins.module.scss
@@ -30,4 +30,8 @@
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
}
+
+ :global(.fit-content) {
+ width: fit-content;
+ }
}
diff --git a/microsite/src/css/customTheme.css b/microsite/src/theme/customTheme.scss
similarity index 86%
rename from microsite/src/css/customTheme.css
rename to microsite/src/theme/customTheme.scss
index 4ba7f1df86..f216762418 100644
--- a/microsite/src/css/customTheme.css
+++ b/microsite/src/theme/customTheme.scss
@@ -75,4 +75,12 @@
border-left: 3px solid rgba(255, 0, 0, 0.5);
}
+.code-block-highlight-line {
+ background-color: rgba(255, 255, 255, 0.08);
+ display: block;
+ margin: 0 calc(-1 * var(--ifm-pre-padding));
+ padding: 0 var(--ifm-pre-padding);
+ border-left: 3px solid rgb(255, 255, 255, 0.5);
+}
+
/* #endregion */
diff --git a/microsite/src/theme/variables.scss b/microsite/src/theme/variables.scss
new file mode 100644
index 0000000000..3d74e59c9c
--- /dev/null
+++ b/microsite/src/theme/variables.scss
@@ -0,0 +1,2 @@
+$xlUpBreakpoint: 1441px;
+$desktopUpBreakpoint: 997px;
diff --git a/microsite/static/img/github-statusneo.png b/microsite/static/img/github-statusneo.png
new file mode 100644
index 0000000000..927ace351a
Binary files /dev/null and b/microsite/static/img/github-statusneo.png differ
diff --git a/microsite/static/img/outlook-statusneo.png b/microsite/static/img/outlook-statusneo.png
new file mode 100644
index 0000000000..99e3b6a0bc
Binary files /dev/null and b/microsite/static/img/outlook-statusneo.png differ
diff --git a/microsite/yarn.lock b/microsite/yarn.lock
index 81b78f043b..38067f5214 100644
--- a/microsite/yarn.lock
+++ b/microsite/yarn.lock
@@ -2623,90 +2623,90 @@ __metadata:
languageName: node
linkType: hard
-"@swc/core-darwin-arm64@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-darwin-arm64@npm:1.3.38"
+"@swc/core-darwin-arm64@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-darwin-arm64@npm:1.3.42"
conditions: os=darwin & cpu=arm64
languageName: node
linkType: hard
-"@swc/core-darwin-x64@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-darwin-x64@npm:1.3.38"
+"@swc/core-darwin-x64@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-darwin-x64@npm:1.3.42"
conditions: os=darwin & cpu=x64
languageName: node
linkType: hard
-"@swc/core-linux-arm-gnueabihf@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.38"
+"@swc/core-linux-arm-gnueabihf@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-linux-arm-gnueabihf@npm:1.3.42"
conditions: os=linux & cpu=arm
languageName: node
linkType: hard
-"@swc/core-linux-arm64-gnu@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-linux-arm64-gnu@npm:1.3.38"
+"@swc/core-linux-arm64-gnu@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-linux-arm64-gnu@npm:1.3.42"
conditions: os=linux & cpu=arm64 & libc=glibc
languageName: node
linkType: hard
-"@swc/core-linux-arm64-musl@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-linux-arm64-musl@npm:1.3.38"
+"@swc/core-linux-arm64-musl@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-linux-arm64-musl@npm:1.3.42"
conditions: os=linux & cpu=arm64 & libc=musl
languageName: node
linkType: hard
-"@swc/core-linux-x64-gnu@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-linux-x64-gnu@npm:1.3.38"
+"@swc/core-linux-x64-gnu@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-linux-x64-gnu@npm:1.3.42"
conditions: os=linux & cpu=x64 & libc=glibc
languageName: node
linkType: hard
-"@swc/core-linux-x64-musl@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-linux-x64-musl@npm:1.3.38"
+"@swc/core-linux-x64-musl@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-linux-x64-musl@npm:1.3.42"
conditions: os=linux & cpu=x64 & libc=musl
languageName: node
linkType: hard
-"@swc/core-win32-arm64-msvc@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-win32-arm64-msvc@npm:1.3.38"
+"@swc/core-win32-arm64-msvc@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-win32-arm64-msvc@npm:1.3.42"
conditions: os=win32 & cpu=arm64
languageName: node
linkType: hard
-"@swc/core-win32-ia32-msvc@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-win32-ia32-msvc@npm:1.3.38"
+"@swc/core-win32-ia32-msvc@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-win32-ia32-msvc@npm:1.3.42"
conditions: os=win32 & cpu=ia32
languageName: node
linkType: hard
-"@swc/core-win32-x64-msvc@npm:1.3.38":
- version: 1.3.38
- resolution: "@swc/core-win32-x64-msvc@npm:1.3.38"
+"@swc/core-win32-x64-msvc@npm:1.3.42":
+ version: 1.3.42
+ resolution: "@swc/core-win32-x64-msvc@npm:1.3.42"
conditions: os=win32 & cpu=x64
languageName: node
linkType: hard
"@swc/core@npm:^1.3.36":
- version: 1.3.38
- resolution: "@swc/core@npm:1.3.38"
+ version: 1.3.42
+ resolution: "@swc/core@npm:1.3.42"
dependencies:
- "@swc/core-darwin-arm64": 1.3.38
- "@swc/core-darwin-x64": 1.3.38
- "@swc/core-linux-arm-gnueabihf": 1.3.38
- "@swc/core-linux-arm64-gnu": 1.3.38
- "@swc/core-linux-arm64-musl": 1.3.38
- "@swc/core-linux-x64-gnu": 1.3.38
- "@swc/core-linux-x64-musl": 1.3.38
- "@swc/core-win32-arm64-msvc": 1.3.38
- "@swc/core-win32-ia32-msvc": 1.3.38
- "@swc/core-win32-x64-msvc": 1.3.38
+ "@swc/core-darwin-arm64": 1.3.42
+ "@swc/core-darwin-x64": 1.3.42
+ "@swc/core-linux-arm-gnueabihf": 1.3.42
+ "@swc/core-linux-arm64-gnu": 1.3.42
+ "@swc/core-linux-arm64-musl": 1.3.42
+ "@swc/core-linux-x64-gnu": 1.3.42
+ "@swc/core-linux-x64-musl": 1.3.42
+ "@swc/core-win32-arm64-msvc": 1.3.42
+ "@swc/core-win32-ia32-msvc": 1.3.42
+ "@swc/core-win32-x64-msvc": 1.3.42
dependenciesMeta:
"@swc/core-darwin-arm64":
optional: true
@@ -2728,7 +2728,7 @@ __metadata:
optional: true
"@swc/core-win32-x64-msvc":
optional: true
- checksum: c55d30e57638bcd21f788add8490c3f3e71bfe027aa5a8b153e1b1b9686ecddd6deeaaa6a6b17717c7eab4c1e2a232b465b6755b6c891506fc0d03139badfbf7
+ checksum: 5c28c03ee6a4e1f630c4a519b56d7e23a0102f70fad977a3ab144a3da96231cf4849e473cdc9ee7be6b9266ba0838d0bc0b6b3575a75540f302befe96cc9606e
languageName: node
linkType: hard
@@ -3738,6 +3738,7 @@ __metadata:
docusaurus-plugin-sass: ^0.2.3
js-yaml: ^4.1.0
prettier: ^2.6.2
+ prism-react-renderer: ^1.3.5
react: ^17.0.2
react-dom: ^17.0.2
replace: ^1.2.2
@@ -8558,11 +8559,11 @@ __metadata:
linkType: hard
"prettier@npm:^2.6.2":
- version: 2.8.4
- resolution: "prettier@npm:2.8.4"
+ version: 2.8.6
+ resolution: "prettier@npm:2.8.6"
bin:
prettier: bin-prettier.js
- checksum: c173064bf3df57b6d93d19aa98753b9b9dd7657212e33b41ada8e2e9f9884066bb9ca0b4005b89b3ab137efffdf8fbe0b462785aba20364798ff4303aadda57e
+ checksum: 8ac94fa67aec0e65743ea15ebf954ef2f1e52638abd129dc04e8b49e8bb3224c0233c98df6b5c98efd31bd2a43866590486559438ee4ead09dc81be389068572
languageName: node
linkType: hard
@@ -9437,15 +9438,15 @@ __metadata:
linkType: hard
"sass@npm:^1.57.1":
- version: 1.58.3
- resolution: "sass@npm:1.58.3"
+ version: 1.59.3
+ resolution: "sass@npm:1.59.3"
dependencies:
chokidar: ">=3.0.0 <4.0.0"
immutable: ^4.0.0
source-map-js: ">=0.6.2 <2.0.0"
bin:
sass: sass.js
- checksum: 35a2b98c037ef80fdc93c9b0be846e6ccc7d75596351a37ee79c397e66666d0a754c52c4696e746c0aff32327471e185343ca349e998a58340411adc9d0489a5
+ checksum: 839b5282cdf7d0ba3fdbfb605277dd584a8c40fa3e3e58ad905d64cd812acfb82ff0a4072d4981673db884ee61505472ff07c5c5a8a497f16ba013b183ba6473
languageName: node
linkType: hard
@@ -10889,8 +10890,8 @@ __metadata:
linkType: hard
"webpack@npm:^5.73.0":
- version: 5.75.0
- resolution: "webpack@npm:5.75.0"
+ version: 5.76.2
+ resolution: "webpack@npm:5.76.2"
dependencies:
"@types/eslint-scope": ^3.7.3
"@types/estree": ^0.0.51
@@ -10921,7 +10922,7 @@ __metadata:
optional: true
bin:
webpack: bin/webpack.js
- checksum: 2bcc5f3c195f375944e8af2f00bf2feea39cb9fda5f763b0d1b00077f1c51783db25c94d3fae96a07dead9fa085e6ae7474417e5ab31719c9776ea5969ceb83a
+ checksum: 86db98299a175c371031449c26077e87b33acd8f45de7f7945ed4b9b37c8ca11bc5169af9c44743efccd4d55e08042a3aa3a3bc42aff831309a0821ffbcd395e
languageName: node
linkType: hard
diff --git a/package.json b/package.json
index e4cb3ef1d3..8550a72a97 100644
--- a/package.json
+++ b/package.json
@@ -47,7 +47,7 @@
"@types/react": "^17",
"@types/react-dom": "^17"
},
- "version": "1.12.0",
+ "version": "1.13.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 e388b3e4ad..b2d85cea1a 100644
--- a/packages/app-defaults/CHANGELOG.md
+++ b/packages/app-defaults/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/app-defaults
+## 1.3.0-next.0
+
+### Minor Changes
+
+- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-app-api@1.7.0-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/plugin-permission-react@0.4.11
+ - @backstage/theme@0.2.18
+
## 1.2.1
### Patch Changes
diff --git a/packages/app-defaults/package.json b/packages/app-defaults/package.json
index 31fb64a079..d4540bfd1d 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.2.1",
+ "version": "1.3.0-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/app-defaults/src/defaults/apis.ts b/packages/app-defaults/src/defaults/apis.ts
index d3ebddfb84..3a552c25c7 100644
--- a/packages/app-defaults/src/defaults/apis.ts
+++ b/packages/app-defaults/src/defaults/apis.ts
@@ -130,6 +130,7 @@ export const apis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GoogleAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -144,6 +145,7 @@ export const apis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
MicrosoftAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -158,6 +160,7 @@ export const apis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GithubAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
@@ -173,6 +176,7 @@ export const apis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OktaAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -187,6 +191,7 @@ export const apis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GitlabAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -201,6 +206,7 @@ export const apis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OneLoginAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -215,6 +221,7 @@ export const apis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
BitbucketAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
defaultScopes: ['team'],
@@ -226,9 +233,11 @@ export const apis = [
deps: {
discoveryApi: discoveryApiRef,
oauthRequestApi: oauthRequestApiRef,
+ configApi: configApiRef,
},
- factory: ({ discoveryApi, oauthRequestApi }) =>
+ factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
BitbucketServerAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
defaultScopes: ['REPO_READ'],
@@ -243,6 +252,7 @@ export const apis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
return AtlassianAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
diff --git a/packages/app/CHANGELOG.md b/packages/app/CHANGELOG.md
index 4b24b827b4..7c0a558a86 100644
--- a/packages/app/CHANGELOG.md
+++ b/packages/app/CHANGELOG.md
@@ -1,5 +1,74 @@
# example-app
+## 0.2.82-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-api-docs@0.9.2-next.0
+ - @backstage/plugin-scaffolder@1.13.0-next.0
+ - @backstage/plugin-scaffolder-react@1.3.0-next.0
+ - @backstage/plugin-entity-feedback@0.2.0-next.0
+ - @backstage/plugin-tech-insights@0.3.9-next.0
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/app-defaults@1.3.0-next.0
+ - @backstage/core-app-api@1.7.0-next.0
+ - @backstage/plugin-search-react@1.5.2-next.0
+ - @backstage/plugin-techdocs@1.6.1-next.0
+ - @backstage/cli@0.22.6-next.0
+ - @backstage/plugin-techdocs-react@1.1.5-next.0
+ - @backstage/plugin-techdocs-module-addons-contrib@1.0.12-next.0
+ - @backstage/plugin-newrelic-dashboard@0.2.10-next.0
+ - @backstage/plugin-apache-airflow@0.2.10-next.0
+ - @backstage/plugin-catalog-import@0.9.7-next.0
+ - @backstage/plugin-github-actions@0.5.17-next.0
+ - @backstage/plugin-catalog-graph@0.2.29-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/plugin-code-coverage@0.2.10-next.0
+ - @backstage/plugin-cost-insights@0.12.6-next.0
+ - @backstage/plugin-user-settings@0.7.2-next.0
+ - @backstage/plugin-azure-devops@0.2.8-next.0
+ - @backstage/plugin-gcp-projects@0.3.36-next.0
+ - @backstage/plugin-azure-sites@0.1.6-next.0
+ - @backstage/plugin-cloudbuild@0.3.17-next.0
+ - @backstage/plugin-kubernetes@0.7.10-next.0
+ - @backstage/plugin-lighthouse@0.4.2-next.0
+ - @backstage/plugin-tech-radar@0.6.3-next.0
+ - @backstage/plugin-dynatrace@3.0.1-next.0
+ - @backstage/plugin-gcalendar@0.3.13-next.0
+ - @backstage/plugin-pagerduty@0.5.10-next.0
+ - @backstage/plugin-shortcuts@0.3.9-next.0
+ - @backstage/plugin-airbrake@0.3.17-next.0
+ - @backstage/plugin-circleci@0.3.17-next.0
+ - @backstage/plugin-graphiql@0.2.49-next.0
+ - @backstage/plugin-newrelic@0.3.35-next.0
+ - @backstage/plugin-explore@0.4.2-next.0
+ - @backstage/plugin-jenkins@0.7.16-next.0
+ - @backstage/plugin-rollbar@0.4.17-next.0
+ - @backstage/plugin-badges@0.2.41-next.0
+ - @backstage/plugin-search@1.1.2-next.0
+ - @backstage/plugin-sentry@0.5.2-next.0
+ - @backstage/plugin-kafka@0.3.17-next.0
+ - @backstage/plugin-gocd@0.1.23-next.0
+ - @backstage/plugin-home@0.4.33-next.0
+ - @backstage/plugin-todo@0.2.19-next.0
+ - @backstage/plugin-org@0.6.7-next.0
+ - @internal/plugin-catalog-customized@0.0.9-next.0
+ - @backstage/integration-react@1.1.12-next.0
+ - @backstage/plugin-linguist@0.1.2-next.0
+ - @backstage/plugin-microsoft-calendar@0.1.2-next.0
+ - @backstage/plugin-playlist@0.1.8-next.0
+ - @backstage/plugin-stack-overflow@0.1.13-next.0
+ - @backstage/plugin-stackstorm@0.1.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-permission-react@0.4.11
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-linguist-common@0.1.0
+ - @backstage/plugin-search-common@1.2.2
+
## 0.2.81
### Patch Changes
diff --git a/packages/app/package.json b/packages/app/package.json
index a5a420c621..5902184f89 100644
--- a/packages/app/package.json
+++ b/packages/app/package.json
@@ -1,6 +1,6 @@
{
"name": "example-app",
- "version": "0.2.81",
+ "version": "0.2.82-next.0",
"private": true,
"backstage": {
"role": "frontend"
@@ -72,7 +72,7 @@
"@internal/plugin-catalog-customized": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@octokit/rest": "^19.0.3",
"@oriflame/backstage-plugin-score-card": "^0.6.0",
"@roadiehq/backstage-plugin-buildkite": "^2.0.8",
diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index 4dacbbc007..1ee6978a87 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -27,7 +27,7 @@ import {
RELATION_PROVIDES_API,
} from '@backstage/catalog-model';
import { createApp } from '@backstage/app-defaults';
-import { AppRouter, FlatRoutes } from '@backstage/core-app-api';
+import { AppRouter, FeatureFlagged, FlatRoutes } from '@backstage/core-app-api';
import {
AlertDisplay,
OAuthRequestDialog,
@@ -117,10 +117,13 @@ const app = createApp({
// Custom icon example
alert: AlarmIcon,
},
- // Example of application level feature flag
- // featureFlags: [
- // { name: 'tech-radar', description: 'Enables the tech radar plugin' },
- // ],
+ featureFlags: [
+ {
+ name: 'scaffolder-next-preview',
+ description: 'Preview the new Scaffolder Next',
+ pluginId: '',
+ },
+ ],
components: {
SignInPage: props => {
return (
@@ -208,49 +211,53 @@ const routes = (
-
- entity?.metadata?.tags?.includes('recommended') ?? false,
- },
- ]}
- />
- }
- >
-
-
-
-
-
-
-
-
- entity?.metadata?.tags?.includes('recommended') ?? false,
- },
- ]}
- />
- }
- >
-
-
-
-
-
-
-
+
+
+ entity?.metadata?.tags?.includes('recommended') ?? false,
+ },
+ ]}
+ />
+ }
+ >
+
+
+
+
+
+
+
+
+
+
+ entity?.metadata?.tags?.includes('recommended') ?? false,
+ },
+ ]}
+ />
+ }
+ >
+
+
+
+
+
+
+
+
} />
) => (
-
+
diff --git a/packages/backend-app-api/CHANGELOG.md b/packages/backend-app-api/CHANGELOG.md
index 1e587b94a3..ed9deb5037 100644
--- a/packages/backend-app-api/CHANGELOG.md
+++ b/packages/backend-app-api/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/backend-app-api
+## 0.4.2-next.0
+
+### Patch Changes
+
+- 8cce2205a39: Register unhandled rejection and uncaught exception handlers to avoid backend crashes.
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/config-loader@1.1.9
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-permission-node@0.7.7-next.0
+
## 0.4.1
### Patch Changes
diff --git a/packages/backend-app-api/package.json b/packages/backend-app-api/package.json
index 8ed273b451..dda56af96c 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.1",
+ "version": "0.4.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts
index 2a8fa1e720..b2c62c2122 100644
--- a/packages/backend-app-api/src/wiring/BackendInitializer.ts
+++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts
@@ -165,6 +165,25 @@ export class BackendInitializer {
);
await registerInit.init.func(deps);
}
+
+ // Once the backend is started, any uncaught errors or unhandled rejections are caught
+ // and logged, in order to avoid crashing the entire backend on local failures.
+ if (process.env.NODE_ENV !== 'test') {
+ const rootLogger = await this.#serviceHolder.get(
+ coreServices.rootLogger,
+ 'root',
+ );
+ process.on('unhandledRejection', (reason: Error) => {
+ rootLogger
+ ?.child({ type: 'unhandledRejection' })
+ ?.error('Unhandled rejection', reason);
+ });
+ process.on('uncaughtException', error => {
+ rootLogger
+ ?.child({ type: 'uncaughtException' })
+ ?.error('Uncaught exception', error);
+ });
+ }
}
#resolveInitOrder(registerInits: Array) {
diff --git a/packages/backend-common/CHANGELOG.md b/packages/backend-common/CHANGELOG.md
index 8d73268bc1..3ec4fe5837 100644
--- a/packages/backend-common/CHANGELOG.md
+++ b/packages/backend-common/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/backend-common
+## 0.18.4-next.0
+
+### Patch Changes
+
+- 420164593cf: Improve GitlabUrlReader to only load requested sub-path
+- Updated dependencies
+ - @backstage/backend-app-api@0.4.2-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/integration-aws-node@0.1.2
+ - @backstage/backend-dev-utils@0.1.1
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/config-loader@1.1.9
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.18.3
### Patch Changes
diff --git a/packages/backend-common/package.json b/packages/backend-common/package.json
index 5d54e5ae12..3f96d5f4ce 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.3",
+ "version": "0.18.4-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
index d926b2be9b..5af4faf7e5 100644
--- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts
+++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts
@@ -556,6 +556,10 @@ describe('GitlabUrlReader', () => {
path.resolve(__dirname, '__fixtures__/gitlab-archive.tar.gz'),
);
+ const archiveSubPathBuffer = fs.readFileSync(
+ path.resolve(__dirname, '__fixtures__/gitlab-subpath-archive.tar.gz'),
+ );
+
const projectGitlabApiResponse = {
id: 11111111,
default_branch: 'main',
@@ -566,21 +570,34 @@ describe('GitlabUrlReader', () => {
id: 'sha123abc',
},
];
+ const commitsOfSubPathGitlabApiResponse = [
+ {
+ id: 'sha456abc',
+ },
+ ];
beforeEach(() => {
worker.use(
rest.get(
'https://gitlab.com/api/v4/projects/backstage%2Fmock/repository/archive',
- (_, res, ctx) =>
- res(
+ (req, res, ctx) => {
+ const filepath = req.url.searchParams.get('path');
+ let filename = 'mock-main-sha123abc.zip';
+ let body = archiveBuffer;
+ if (filepath === 'docs') {
+ filename = 'gitlab-subpath-archive.tar.gz';
+ body = archiveSubPathBuffer;
+ }
+ return res(
ctx.status(200),
ctx.set('Content-Type', 'application/zip'),
ctx.set(
'content-disposition',
- 'attachment; filename="mock-main-sha123abc.zip"',
+ `attachment; filename="${filename}"`,
),
- ctx.body(archiveBuffer),
- ),
+ ctx.body(body),
+ );
+ },
),
rest.get(
'https://gitlab.com/api/v4/projects/backstage%2Fmock',
@@ -596,6 +613,14 @@ describe('GitlabUrlReader', () => {
(req, res, ctx) => {
const refName = req.url.searchParams.get('ref_name');
if (refName === 'main') {
+ const filepath = req.url.searchParams.get('path');
+ if (filepath === 'docs') {
+ return res(
+ ctx.status(200),
+ ctx.set('Content-Type', 'application/json'),
+ ctx.json(commitsOfSubPathGitlabApiResponse),
+ );
+ }
return res(
ctx.status(200),
ctx.set('Content-Type', 'application/json'),
@@ -622,6 +647,21 @@ describe('GitlabUrlReader', () => {
);
});
+ it('load only relevant path', async () => {
+ const result = await gitlabProcessor.search(
+ 'https://gitlab.com/backstage/mock/tree/main/docs/**/index.*',
+ );
+
+ expect(result.etag).toBe('sha456abc');
+ expect(result.files.length).toBe(1);
+ expect(result.files[0].url).toBe(
+ 'https://gitlab.com/backstage/mock/tree/main/docs/index.md',
+ );
+ await expect(result.files[0].content()).resolves.toEqual(
+ Buffer.from('# Test Subpath\n'),
+ );
+ });
+
it('throws NotModifiedError when same etag', async () => {
await expect(
gitlabProcessor.search(
diff --git a/packages/backend-common/src/reading/GitlabUrlReader.ts b/packages/backend-common/src/reading/GitlabUrlReader.ts
index df8441c1f7..ab59c19b16 100644
--- a/packages/backend-common/src/reading/GitlabUrlReader.ts
+++ b/packages/backend-common/src/reading/GitlabUrlReader.ts
@@ -26,7 +26,6 @@ import parseGitUrl from 'git-url-parse';
import { Minimatch } from 'minimatch';
import { Readable } from 'stream';
import { NotFoundError, NotModifiedError } from '@backstage/errors';
-import { stripFirstDirectoryFromPath } from './tree/util';
import {
ReadTreeResponseFactory,
ReaderFactory,
@@ -163,7 +162,7 @@ export class GitlabUrlReader implements UrlReader {
// ref is an empty string if no branch is set in provided url to readTree.
const branch = ref || projectGitlabResponseJson.default_branch;
- // Fetch the latest commit that modifies the the filepath in the provided or default branch
+ // Fetch the latest commit that modifies the filepath in the provided or default branch
// to compare against the provided sha.
const commitsReqParams = new URLSearchParams();
commitsReqParams.set('ref_name', branch);
@@ -200,11 +199,16 @@ export class GitlabUrlReader implements UrlReader {
throw new NotModifiedError();
}
+ const archiveReqParams = new URLSearchParams();
+ archiveReqParams.set('sha', branch);
+ if (!!filepath) {
+ archiveReqParams.set('path', filepath);
+ }
// https://docs.gitlab.com/ee/api/repositories.html#get-file-archive
const archiveGitLabResponse = await fetch(
`${this.integration.config.apiBaseUrl}/projects/${encodeURIComponent(
repoFullName,
- )}/repository/archive?sha=${branch}`,
+ )}/repository/archive?${archiveReqParams.toString()}`,
{
...getGitLabRequestOptions(this.integration.config),
// TODO(freben): The signal cast is there because pre-3.x versions of
@@ -234,31 +238,54 @@ export class GitlabUrlReader implements UrlReader {
async search(url: string, options?: SearchOptions): Promise {
const { filepath } = parseGitUrl(url);
+ const staticPart = this.getStaticPart(filepath);
const matcher = new Minimatch(filepath);
-
- // TODO(freben): For now, read the entire repo and filter through that. In
- // a future improvement, we could be smart and try to deduce that non-glob
- // prefixes (like for filepaths such as some-prefix/**/a.yaml) can be used
- // to get just that part of the repo.
- const treeUrl = trimEnd(url.replace(filepath, ''), '/');
-
+ const treeUrl = trimEnd(url.replace(filepath, staticPart), `/`);
+ const pathPrefix = staticPart ? `${staticPart}/` : '';
const tree = await this.readTree(treeUrl, {
etag: options?.etag,
signal: options?.signal,
- filter: path => matcher.match(stripFirstDirectoryFromPath(path)),
+ filter: path => matcher.match(`${pathPrefix}${path}`),
});
- const files = await tree.files();
+ const files = await tree.files();
return {
etag: tree.etag,
files: files.map(file => ({
- url: this.integration.resolveUrl({ url: `/${file.path}`, base: url }),
+ url: this.integration.resolveUrl({
+ url: `/${pathPrefix}${file.path}`,
+ base: url,
+ }),
content: file.content,
lastModifiedAt: file.lastModifiedAt,
})),
};
}
+ /**
+ * This function splits the input globPattern string into segments using the path separator /. It then iterates over
+ * the segments from the end of the array towards the beginning, checking if the concatenated string up to that
+ * segment matches the original globPattern using the minimatch function. If a match is found, it continues iterating.
+ * If no match is found, it returns the concatenated string up to the current segment, which is the static part of the
+ * glob pattern.
+ *
+ * E.g. `catalog/foo/*.yaml` will return `catalog/foo`.
+ *
+ * @param globPattern the glob pattern
+ * @private
+ */
+ private getStaticPart(globPattern: string) {
+ const segments = globPattern.split('/');
+ let i = segments.length;
+ while (
+ i > 0 &&
+ new Minimatch(segments.slice(0, i).join('/')).match(globPattern)
+ ) {
+ i--;
+ }
+ return segments.slice(0, i).join('/');
+ }
+
toString() {
const { host, token } = this.integration.config;
return `gitlab{host=${host},authed=${Boolean(token)}}`;
diff --git a/packages/backend-common/src/reading/__fixtures__/gitlab-subpath-archive.tar.gz b/packages/backend-common/src/reading/__fixtures__/gitlab-subpath-archive.tar.gz
new file mode 100644
index 0000000000..c41714b452
Binary files /dev/null and b/packages/backend-common/src/reading/__fixtures__/gitlab-subpath-archive.tar.gz differ
diff --git a/packages/backend-defaults/CHANGELOG.md b/packages/backend-defaults/CHANGELOG.md
index edfc39bd91..01afac257b 100644
--- a/packages/backend-defaults/CHANGELOG.md
+++ b/packages/backend-defaults/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/backend-defaults
+## 0.1.9-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.4.2-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/backend-plugin-api@0.5.1-next.0
+
## 0.1.8
### Patch Changes
diff --git a/packages/backend-defaults/package.json b/packages/backend-defaults/package.json
index 2759c3c089..2f9655384e 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.8",
+ "version": "0.1.9-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 e51c65963d..39af1fa1c3 100644
--- a/packages/backend-next/CHANGELOG.md
+++ b/packages/backend-next/CHANGELOG.md
@@ -1,5 +1,17 @@
# example-backend-next
+## 0.0.10-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.12.1-next.0
+ - @backstage/plugin-catalog-backend@1.8.1-next.0
+ - @backstage/backend-defaults@0.1.9-next.0
+ - @backstage/plugin-app-backend@0.3.44-next.0
+ - @backstage/plugin-techdocs-backend@1.6.1-next.0
+ - @backstage/plugin-todo-backend@0.1.41-next.0
+
## 0.0.9
### Patch Changes
diff --git a/packages/backend-next/package.json b/packages/backend-next/package.json
index 14ec71f352..bed4f10975 100644
--- a/packages/backend-next/package.json
+++ b/packages/backend-next/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend-next",
- "version": "0.0.9",
+ "version": "0.0.10-next.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,6 +29,11 @@
"@backstage/plugin-app-backend": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
"@backstage/plugin-scaffolder-backend": "workspace:^",
+ "@backstage/plugin-search-backend": "workspace:^",
+ "@backstage/plugin-search-backend-module-catalog": "workspace:^",
+ "@backstage/plugin-search-backend-module-explore": "workspace:^",
+ "@backstage/plugin-search-backend-module-techdocs": "workspace:^",
+ "@backstage/plugin-search-backend-node": "workspace:^",
"@backstage/plugin-techdocs-backend": "workspace:^",
"@backstage/plugin-todo-backend": "workspace:^"
},
diff --git a/packages/backend-next/src/index.ts b/packages/backend-next/src/index.ts
index 8f8984b192..e3cbbd53b8 100644
--- a/packages/backend-next/src/index.ts
+++ b/packages/backend-next/src/index.ts
@@ -14,18 +14,35 @@
* limitations under the License.
*/
-import { catalogPlugin } from '@backstage/plugin-catalog-backend/alpha';
-import { catalogModuleTemplateKind } from '@backstage/plugin-scaffolder-backend/alpha';
import { createBackend } from '@backstage/backend-defaults';
import { appPlugin } from '@backstage/plugin-app-backend/alpha';
import { todoPlugin } from '@backstage/plugin-todo-backend';
import { techdocsPlugin } from '@backstage/plugin-techdocs-backend/alpha';
+import { catalogPlugin } from '@backstage/plugin-catalog-backend/alpha';
+import { catalogModuleTemplateKind } from '@backstage/plugin-scaffolder-backend/alpha';
+import { searchPlugin } from '@backstage/plugin-search-backend/alpha';
+import { searchModuleCatalogCollator } from '@backstage/plugin-search-backend-module-catalog/alpha';
+import { searchModuleTechDocsCollator } from '@backstage/plugin-search-backend-module-techdocs/alpha';
+import { searchModuleExploreCollator } from '@backstage/plugin-search-backend-module-explore/alpha';
const backend = createBackend();
+backend.add(appPlugin({ appPackageName: 'example-app' }));
+
+// Todo
+backend.add(todoPlugin());
+
+// Techdocs
+backend.add(techdocsPlugin());
+
+// Catalog
backend.add(catalogPlugin());
backend.add(catalogModuleTemplateKind());
-backend.add(appPlugin({ appPackageName: 'example-app' }));
-backend.add(todoPlugin());
-backend.add(techdocsPlugin());
+
+// Search
+backend.add(searchPlugin());
+backend.add(searchModuleCatalogCollator());
+backend.add(searchModuleTechDocsCollator());
+backend.add(searchModuleExploreCollator());
+
backend.start();
diff --git a/packages/backend-plugin-api/CHANGELOG.md b/packages/backend-plugin-api/CHANGELOG.md
index dd78ffaca1..f3cb679fce 100644
--- a/packages/backend-plugin-api/CHANGELOG.md
+++ b/packages/backend-plugin-api/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/backend-plugin-api
+## 0.5.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-permission-common@0.7.4
+
## 0.5.0
### Minor Changes
diff --git a/packages/backend-plugin-api/package.json b/packages/backend-plugin-api/package.json
index cc155fe546..6566772f2f 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.0",
+ "version": "0.5.1-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 dea40d399b..7d2540d83a 100644
--- a/packages/backend-tasks/CHANGELOG.md
+++ b/packages/backend-tasks/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/backend-tasks
+## 0.5.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.5.0
### Minor Changes
diff --git a/packages/backend-tasks/package.json b/packages/backend-tasks/package.json
index 5042ff735a..43553cd740 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.0",
+ "version": "0.5.1-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 529c92d423..a1f4e76ea2 100644
--- a/packages/backend-test-utils/CHANGELOG.md
+++ b/packages/backend-test-utils/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/backend-test-utils
+## 0.1.36-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-app-api@0.4.2-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.13-next.0
+
## 0.1.35
### Patch Changes
diff --git a/packages/backend-test-utils/package.json b/packages/backend-test-utils/package.json
index 80135ad4a9..05195f98aa 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.35",
+ "version": "0.1.36-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"publishConfig": {
diff --git a/packages/backend/CHANGELOG.md b/packages/backend/CHANGELOG.md
index 7ead0abeb6..fd9247bb44 100644
--- a/packages/backend/CHANGELOG.md
+++ b/packages/backend/CHANGELOG.md
@@ -1,5 +1,56 @@
# example-backend
+## 0.2.82-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.12.1-next.0
+ - @backstage/plugin-kubernetes-backend@0.10.0-next.0
+ - @backstage/plugin-entity-feedback-backend@0.1.2-next.0
+ - @backstage/plugin-auth-backend@0.18.2-next.0
+ - @backstage/plugin-catalog-backend@1.8.1-next.0
+ - @backstage/plugin-graphql-backend@0.1.34-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/plugin-scaffolder-backend-module-rails@0.4.12-next.0
+ - example-app@0.2.82-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/plugin-adr-backend@0.3.2-next.0
+ - @backstage/plugin-app-backend@0.3.44-next.0
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-azure-devops-backend@0.3.23-next.0
+ - @backstage/plugin-azure-sites-backend@0.1.6-next.0
+ - @backstage/plugin-badges-backend@0.1.38-next.0
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+ - @backstage/plugin-code-coverage-backend@0.2.10-next.0
+ - @backstage/plugin-events-backend@0.2.5-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+ - @backstage/plugin-explore-backend@0.0.6-next.0
+ - @backstage/plugin-jenkins-backend@0.1.34-next.0
+ - @backstage/plugin-kafka-backend@0.2.37-next.0
+ - @backstage/plugin-lighthouse-backend@0.1.2-next.0
+ - @backstage/plugin-linguist-backend@0.2.1-next.0
+ - @backstage/plugin-permission-backend@0.5.19-next.0
+ - @backstage/plugin-permission-common@0.7.4
+ - @backstage/plugin-permission-node@0.7.7-next.0
+ - @backstage/plugin-playlist-backend@0.2.7-next.0
+ - @backstage/plugin-proxy-backend@0.2.38-next.0
+ - @backstage/plugin-rollbar-backend@0.1.41-next.0
+ - @backstage/plugin-search-backend@1.2.5-next.0
+ - @backstage/plugin-search-backend-module-elasticsearch@1.1.5-next.0
+ - @backstage/plugin-search-backend-module-pg@0.5.5-next.0
+ - @backstage/plugin-search-backend-node@1.1.5-next.0
+ - @backstage/plugin-search-common@1.2.2
+ - @backstage/plugin-tech-insights-backend@0.5.10-next.0
+ - @backstage/plugin-tech-insights-backend-module-jsonfc@0.1.28-next.0
+ - @backstage/plugin-tech-insights-node@0.4.2-next.0
+ - @backstage/plugin-techdocs-backend@1.6.1-next.0
+ - @backstage/plugin-todo-backend@0.1.41-next.0
+
## 0.2.81
### Patch Changes
diff --git a/packages/backend/package.json b/packages/backend/package.json
index 54dbe640df..22da65099f 100644
--- a/packages/backend/package.json
+++ b/packages/backend/package.json
@@ -1,6 +1,6 @@
{
"name": "example-backend",
- "version": "0.2.81",
+ "version": "0.2.82-next.0",
"main": "dist/index.cjs.js",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/packages/backend/src/plugins/kubernetes.ts b/packages/backend/src/plugins/kubernetes.ts
index 3bc7a5dcbe..3bc6648862 100644
--- a/packages/backend/src/plugins/kubernetes.ts
+++ b/packages/backend/src/plugins/kubernetes.ts
@@ -27,6 +27,7 @@ export default async function createPlugin(
logger: env.logger,
config: env.config,
catalogApi,
+ permissions: env.permissions,
}).build();
return router;
}
diff --git a/packages/backend/src/plugins/scaffolder.ts b/packages/backend/src/plugins/scaffolder.ts
index d079b64c28..813092c9b9 100644
--- a/packages/backend/src/plugins/scaffolder.ts
+++ b/packages/backend/src/plugins/scaffolder.ts
@@ -34,5 +34,6 @@ export default async function createPlugin(
reader: env.reader,
identity: env.identity,
scheduler: env.scheduler,
+ permissionApi: env.permissions,
});
}
diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md
index 6903e61425..e88e554b70 100644
--- a/packages/cli/CHANGELOG.md
+++ b/packages/cli/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/cli
+## 0.22.6-next.0
+
+### Patch Changes
+
+- b9839d7135c: Fixed backend start command on Windows by removing the use of platform dependent path joins.
+- c07c3b7364b: Add `onboard` command. While still in development, this command aims to guide users in setting up their Backstage App.
+- Updated dependencies
+ - @backstage/eslint-plugin@0.1.3-next.0
+ - @backstage/config@1.0.7
+ - @backstage/release-manifests@0.0.9
+ - @backstage/cli-common@0.1.12
+ - @backstage/config-loader@1.1.9
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.22.4
### Patch Changes
diff --git a/packages/cli/asset-types/asset-types.d.ts b/packages/cli/asset-types/asset-types.d.ts
index 9bd158bb8e..a1c053dc54 100644
--- a/packages/cli/asset-types/asset-types.d.ts
+++ b/packages/cli/asset-types/asset-types.d.ts
@@ -40,6 +40,11 @@ declare module '*.jpeg' {
export default src;
}
+declare module '*.md' {
+ const src: string;
+ export default src;
+}
+
declare module '*.png' {
const src: string;
export default src;
diff --git a/packages/cli/package.json b/packages/cli/package.json
index fd7f9a20c1..707a9a1f6e 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.4",
+ "version": "0.22.6-next.0",
"publishConfig": {
"access": "public"
},
@@ -39,6 +39,7 @@
"@backstage/types": "workspace:^",
"@esbuild-kit/cjs-loader": "^2.4.1",
"@manypkg/get-packages": "^1.1.3",
+ "@octokit/oauth-app": "^4.2.0",
"@octokit/request": "^6.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.7",
"@rollup/plugin-commonjs": "^23.0.0",
diff --git a/packages/cli/src/commands/index.ts b/packages/cli/src/commands/index.ts
index ddec5c6a4a..ba11a53cfd 100644
--- a/packages/cli/src/commands/index.ts
+++ b/packages/cli/src/commands/index.ts
@@ -25,6 +25,13 @@ const configOption = [
Array(),
] as const;
+export function registerOnboardCommand(program: Command) {
+ program
+ .command('onboard', { hidden: true })
+ .description('Get help setting up your Backstage App.')
+ .action(lazy(() => import('./onboard').then(m => m.command)));
+}
+
export function registerRepoCommand(program: Command) {
const command = program
.command('repo [command]')
@@ -360,6 +367,7 @@ export function registerCommands(program: Command) {
registerRepoCommand(program);
registerScriptCommand(program);
registerMigrateCommand(program);
+ registerOnboardCommand(program);
program
.command('versions:bump')
diff --git a/packages/cli/src/commands/onboard/README.md b/packages/cli/src/commands/onboard/README.md
new file mode 100644
index 0000000000..0fb4d22666
--- /dev/null
+++ b/packages/cli/src/commands/onboard/README.md
@@ -0,0 +1,22 @@
+# onboard
+
+## Local dev
+
+# Create a new app
+
+```bash
+cd ..
+npx @backstage/create-app
+cd new-backstage-app
+
+# Run the modded cli in the new app
+
+../RELATIVE_PATH_TO_PROJECT/backstage/packages/cli/bin/backstage-cli onboard
+? Do you want to set up Authentication for this project? (Y/n)
+...
+
+# Try the app with the new changes
+
+yarn dev
+
+```
diff --git a/packages/cli/src/commands/onboard/auth/index.ts b/packages/cli/src/commands/onboard/auth/index.ts
new file mode 100644
index 0000000000..0f4537b110
--- /dev/null
+++ b/packages/cli/src/commands/onboard/auth/index.ts
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import inquirer from 'inquirer';
+import { Task } from '../../../lib/tasks';
+import { github, Answers as GitHubAnswers } from './providers/github';
+import { gitlab, Answers as GitLabAnswers } from './providers/gitlab';
+
+export { type GitHubAnswers, type GitLabAnswers };
+
+export async function auth(): Promise<{
+ provider: string;
+ answers: GitHubAnswers | GitLabAnswers;
+}> {
+ const answers = await inquirer.prompt<{
+ provider: string;
+ }>([
+ {
+ type: 'list',
+ name: 'provider',
+ message: 'Please select an authentication provider:',
+ choices: ['GitHub', 'GitLab'],
+ },
+ ]);
+
+ const { provider } = answers;
+
+ let providerAnswers;
+ switch (provider) {
+ case 'GitHub': {
+ providerAnswers = await github();
+ break;
+ }
+ case 'GitLab': {
+ providerAnswers = await gitlab();
+ break;
+ }
+ default:
+ throw new Error(`Provider ${provider} not implemented yet.`);
+ }
+
+ Task.log();
+ Task.log(`Done setting up ${provider} Authentication!`);
+ Task.log();
+
+ return {
+ provider,
+ answers: providerAnswers,
+ };
+}
diff --git a/packages/cli/src/commands/onboard/auth/patch.ts b/packages/cli/src/commands/onboard/auth/patch.ts
new file mode 100644
index 0000000000..d004eeb157
--- /dev/null
+++ b/packages/cli/src/commands/onboard/auth/patch.ts
@@ -0,0 +1,43 @@
+/*
+ * 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 * as fs from 'fs-extra';
+import * as path from 'path';
+import * as differ from 'diff';
+import { PATCH_FOLDER } from '../files';
+import { findPaths } from '@backstage/cli-common';
+
+/* eslint-disable-next-line no-restricted-syntax */
+const { targetRoot } = findPaths(__dirname);
+
+export const patch = async (patchFile: string) => {
+ const patchContent = await fs.readFile(
+ path.join(PATCH_FOLDER, patchFile),
+ 'utf8',
+ );
+ const targetName = patchContent.split('\n')[0].replace('--- a', '');
+ const targetFile = path.join(targetRoot, targetName);
+ const oldContent = await fs.readFile(targetFile, 'utf8');
+ const newContent = differ.applyPatch(oldContent, patchContent);
+ if (!newContent) {
+ throw new Error(
+ `Patch ${patchFile} was not applied correctly.
+ Did you change ${targetName} manually before running this command?`,
+ );
+ }
+
+ return await fs.writeFile(targetFile, newContent, 'utf8');
+};
diff --git a/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch b/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch
new file mode 100644
index 0000000000..db9f5228e8
--- /dev/null
+++ b/packages/cli/src/commands/onboard/auth/patches/01-github.Add-SignInPage.patch
@@ -0,0 +1,25 @@
+--- a/packages/app/src/App.tsx
++++ b/packages/app/src/App.tsx
+@@ -30 +30,5 @@ import { Root } from './components/Root';
+-import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
++import {
++ AlertDisplay,
++ OAuthRequestDialog,
++ SignInPage,
++} from '@backstage/core-components';
+@@ -35,0 +40 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/
++import { githubAuthApiRef } from '@backstage/core-plugin-api';
+@@ -38,0 +44,14 @@ const app = createApp({
++ components: {
++ SignInPage: props => (
++
++ ),
++ },
\ No newline at end of file
diff --git a/packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch b/packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch
new file mode 100644
index 0000000000..da849220b8
--- /dev/null
+++ b/packages/cli/src/commands/onboard/auth/patches/02-gitlab.Add-SignInPage.patch
@@ -0,0 +1,25 @@
+--- a/packages/app/src/App.tsx
++++ b/packages/app/src/App.tsx
+@@ -30 +30,5 @@ import { Root } from './components/Root';
+-import { AlertDisplay, OAuthRequestDialog } from '@backstage/core-components';
++import {
++ AlertDisplay,
++ OAuthRequestDialog,
++ SignInPage,
++} from '@backstage/core-components';
+@@ -35,0 +40 @@ import { catalogEntityCreatePermission } from '@backstage/plugin-catalog-common/
++import { gitlabAuthApiRef } from '@backstage/core-plugin-api';
+@@ -38,0 +44,14 @@ const app = createApp({
++ components: {
++ SignInPage: props => (
++
++ ),
++ },
\ No newline at end of file
diff --git a/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch b/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch
new file mode 100644
index 0000000000..7a69669674
--- /dev/null
+++ b/packages/cli/src/commands/onboard/auth/patches/03-gitlab.Change-signIn-resolver.patch
@@ -0,0 +1,5 @@
+--- a/packages/backend/src/plugins/auth.ts
++++ b/packages/backend/src/plugins/auth.ts
+@@ -38 +38 @@ export default async function createPlugin(
+- github: providers.github.create({
++ gitlab: providers.gitlab.create({
\ No newline at end of file
diff --git a/packages/cli/src/commands/onboard/auth/providers/github.ts b/packages/cli/src/commands/onboard/auth/providers/github.ts
new file mode 100644
index 0000000000..36c6b02038
--- /dev/null
+++ b/packages/cli/src/commands/onboard/auth/providers/github.ts
@@ -0,0 +1,147 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { OAuthApp } from '@octokit/oauth-app';
+import chalk from 'chalk';
+import * as fs from 'fs-extra';
+import inquirer from 'inquirer';
+import { Task } from '../../../../lib/tasks';
+import { updateConfigFile } from '../../config';
+import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files';
+import { patch } from '../patch';
+
+export type Answers = {
+ clientSecret: string;
+ clientId: string;
+ hasEnterprise: boolean;
+ enterpriseInstanceUrl?: string;
+};
+
+const validateCredentials = async (clientId: string, clientSecret: string) => {
+ try {
+ const app = new OAuthApp({
+ clientId,
+ clientSecret,
+ });
+ await app.createToken({
+ code: '%NOT-VALID-CODE%',
+ });
+ } catch (error) {
+ // @octokit/request returns a error.response object when a request is rejected.
+ // We can check it to see what kind of error we received.
+
+ // If error.response is successful we can double-check that the error itself was due to the bad code.
+ // If that's the case then we can assume that the client id and secret exists as we otherwise would
+ // have gotten a 400/404.
+ if (
+ error.response.status !== 200 &&
+ error.response.data.error !== 'bad_verification_code'
+ ) {
+ throw new Error(`Validating GitHub Credentials failed.`);
+ }
+ }
+};
+
+const getConfig = (answers: Answers) => {
+ const { clientId, clientSecret, hasEnterprise, enterpriseInstanceUrl } =
+ answers;
+
+ return {
+ auth: {
+ providers: {
+ github: {
+ development: {
+ clientId,
+ clientSecret,
+ ...(hasEnterprise && {
+ enterpriseInstanceUrl,
+ }),
+ },
+ },
+ },
+ },
+ };
+};
+
+export const github = async (): Promise => {
+ Task.log(`
+ To add GitHub authentication, you must create an OAuth App from the GitHub developer settings: ${chalk.blue(
+ 'https://github.com/settings/developers',
+ )}
+ The Homepage URL should point to Backstage's frontend, while the Authorization callback URL will point to the auth backend.
+
+ Settings for local development:
+ ${chalk.cyan(`
+ Homepage URL: http://localhost:3000
+ Authorization callback URL: http://localhost:7007/api/auth/github/handler/frame`)}
+
+ You can find the full documentation page here: ${chalk.blue(
+ 'https://backstage.io/docs/auth/github/provider',
+ )}
+ `);
+
+ const answers = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'clientId',
+ message: 'What is your Client Id?',
+ validate: (input: string) => (input.length ? true : false),
+ },
+ {
+ type: 'input',
+ name: 'clientSecret',
+ message: 'What is your Client Secret?',
+ validate: (input: string) => (input.length ? true : false),
+ },
+ {
+ type: 'confirm',
+ name: 'hasEnterprise',
+ message: 'Are you using GitHub Enterprise?',
+ },
+ {
+ type: 'input',
+ name: 'enterpriseInstanceUrl',
+ message: 'What is your URL for GitHub Enterprise?',
+ when: ({ hasEnterprise }) => hasEnterprise,
+ validate: (input: string) => Boolean(new URL(input)),
+ },
+ ]);
+
+ const { clientId, clientSecret } = answers;
+ const config = getConfig(answers);
+
+ Task.log('Setting up GitHub Authentication for you...');
+
+ await Task.forItem(
+ 'Validating',
+ 'credentials',
+ async () => await validateCredentials(clientId, clientSecret),
+ );
+ await Task.forItem(
+ 'Updating',
+ APP_CONFIG_FILE,
+ async () => await updateConfigFile(APP_CONFIG_FILE, config),
+ );
+
+ const patches = await fs.readdir(PATCH_FOLDER);
+ for (const patchFile of patches.filter(p => p.includes('github'))) {
+ await Task.forItem('Patching', patchFile, async () => {
+ await patch(patchFile);
+ });
+ }
+
+ return answers;
+};
diff --git a/packages/cli/src/commands/onboard/auth/providers/gitlab.ts b/packages/cli/src/commands/onboard/auth/providers/gitlab.ts
new file mode 100644
index 0000000000..bc5491111c
--- /dev/null
+++ b/packages/cli/src/commands/onboard/auth/providers/gitlab.ts
@@ -0,0 +1,115 @@
+/*
+ * 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 chalk from 'chalk';
+import * as fs from 'fs-extra';
+import inquirer from 'inquirer';
+import { Task } from '../../../../lib/tasks';
+import { updateConfigFile } from '../../config';
+import { APP_CONFIG_FILE, PATCH_FOLDER } from '../../files';
+import { patch } from '../patch';
+
+const getConfig = (answers: Answers) => {
+ const { clientId, clientSecret, hasAudience, audience } = answers;
+
+ return {
+ auth: {
+ providers: {
+ gitlab: {
+ development: {
+ clientId,
+ clientSecret,
+ ...(hasAudience && {
+ audience,
+ }),
+ },
+ },
+ },
+ },
+ };
+};
+
+export type Answers = {
+ clientSecret: string;
+ clientId: string;
+ hasAudience: boolean;
+ audience?: string;
+};
+
+export const gitlab = async (): Promise => {
+ Task.log(`
+ To add GitLab authentication, you must create an Application from the GitLab Settings: ${chalk.blue(
+ 'https://gitlab.com/-/profile/applications',
+ )}
+ The Redirect URI should point to your Backstage backend auth handler.
+
+ Settings for local development:
+ ${chalk.cyan(`
+ Name: Backstage (or your custom app name)
+ Redirect URI: http://localhost:7007/api/auth/gitlab/handler/frame
+ Scopes: read_api and read_user`)}
+
+ You can find the full documentation page here: ${chalk.blue(
+ 'https://backstage.io/docs/auth/gitlab/provider',
+ )}
+ `);
+
+ const answers = await inquirer.prompt([
+ {
+ type: 'input',
+ name: 'clientId',
+ message: 'What is your Application Id?',
+ validate: (input: string) => (input.length ? true : false),
+ },
+ {
+ type: 'input',
+ name: 'clientSecret',
+ message: 'What is your Application Secret?',
+ validate: (input: string) => (input.length ? true : false),
+ },
+ {
+ type: 'confirm',
+ name: 'hasAudience',
+ message: 'Do you have a self-hosted instance of GitLab?',
+ },
+ {
+ type: 'input',
+ name: 'audience',
+ message: 'What is the URL for your GitLab instance?',
+ when: ({ hasAudience }) => hasAudience,
+ validate: (input: string) => Boolean(new URL(input)),
+ },
+ ]);
+
+ const config = getConfig(answers);
+
+ Task.log('Setting up GitLab Authentication for you...');
+
+ await Task.forItem(
+ 'Updating',
+ APP_CONFIG_FILE,
+ async () => await updateConfigFile(APP_CONFIG_FILE, config),
+ );
+
+ const patches = await fs.readdir(PATCH_FOLDER);
+ for (const patchFile of patches.filter(p => p.includes('gitlab'))) {
+ await Task.forItem('Patching', patchFile, async () => {
+ await patch(patchFile);
+ });
+ }
+
+ return answers;
+};
diff --git a/packages/cli/src/commands/onboard/command.ts b/packages/cli/src/commands/onboard/command.ts
new file mode 100644
index 0000000000..5cf25953ab
--- /dev/null
+++ b/packages/cli/src/commands/onboard/command.ts
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import chalk from 'chalk';
+import inquirer from 'inquirer';
+import { Task } from '../../lib/tasks';
+import { auth } from './auth';
+import { integrations } from './integrations';
+
+export async function command(): Promise {
+ const answers = await inquirer.prompt<{
+ shouldSetupAuth: boolean;
+ shouldSetupScaffolder: boolean;
+ }>([
+ {
+ type: 'confirm',
+ name: 'shouldSetupAuth',
+ message: 'Do you want to set up Authentication for this project?',
+ default: true,
+ },
+ {
+ type: 'confirm',
+ name: 'shouldSetupScaffolder',
+ message: 'Do you want to use Software Templates in this project?',
+ default: true,
+ },
+ ]);
+
+ const { shouldSetupAuth, shouldSetupScaffolder } = answers;
+
+ let providerInfo;
+ if (shouldSetupAuth) {
+ providerInfo = await auth();
+ }
+
+ if (shouldSetupScaffolder) {
+ await integrations(providerInfo);
+ }
+
+ if (!shouldSetupAuth && !shouldSetupScaffolder) {
+ Task.log(
+ chalk.yellow(
+ 'If you change your mind, feel free to re-run this command.',
+ ),
+ );
+ return;
+ }
+
+ Task.log();
+ Task.log(
+ `You can now start your app with ${chalk.inverse(
+ chalk.italic('yarn dev'),
+ )}`,
+ );
+ Task.log();
+}
diff --git a/packages/cli/src/commands/onboard/config.ts b/packages/cli/src/commands/onboard/config.ts
new file mode 100644
index 0000000000..cce3119fcc
--- /dev/null
+++ b/packages/cli/src/commands/onboard/config.ts
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import * as fs from 'fs-extra';
+import yaml from 'yaml';
+
+const readYaml = async (file: string) => {
+ return yaml.parse(await fs.readFile(file, 'utf8'));
+};
+
+export const updateConfigFile = async (file: string, config: T) => {
+ const staticContent =
+ '# Backstage override configuration for your local development environment \n';
+
+ const content = fs.existsSync(file)
+ ? yaml.stringify(
+ { ...(await readYaml(file)), ...config },
+ {
+ indent: 2,
+ },
+ )
+ : staticContent.concat(
+ yaml.stringify(
+ { ...config },
+ {
+ indent: 2,
+ },
+ ),
+ );
+
+ return await fs.writeFile(file, content, 'utf8');
+};
diff --git a/packages/cli/src/commands/onboard/files.ts b/packages/cli/src/commands/onboard/files.ts
new file mode 100644
index 0000000000..17f8a0f85e
--- /dev/null
+++ b/packages/cli/src/commands/onboard/files.ts
@@ -0,0 +1,30 @@
+/*
+ * 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 { findPaths } from '@backstage/cli-common';
+import * as path from 'path';
+
+/* eslint-disable-next-line no-restricted-syntax */
+const { targetRoot, ownDir } = findPaths(__dirname);
+export const APP_CONFIG_FILE = path.join(targetRoot, 'app-config.local.yaml');
+export const PATCH_FOLDER = path.join(
+ ownDir,
+ 'src',
+ 'commands',
+ 'onboard',
+ 'auth',
+ 'patches',
+);
diff --git a/packages/cli/src/commands/onboard/index.ts b/packages/cli/src/commands/onboard/index.ts
new file mode 100644
index 0000000000..4c23ea6e48
--- /dev/null
+++ b/packages/cli/src/commands/onboard/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { command } from './command';
diff --git a/packages/cli/src/commands/onboard/integrations/github.ts b/packages/cli/src/commands/onboard/integrations/github.ts
new file mode 100644
index 0000000000..a0538c7ace
--- /dev/null
+++ b/packages/cli/src/commands/onboard/integrations/github.ts
@@ -0,0 +1,136 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import chalk from 'chalk';
+import inquirer from 'inquirer';
+import { Task } from '../../../lib/tasks';
+import { updateConfigFile } from '../config';
+import { APP_CONFIG_FILE } from '../files';
+import { GitHubAnswers } from '../auth';
+
+type Answers = {
+ hasEnterprise: boolean;
+ enterpriseInstanceUrl: string;
+ apiBaseUrl: string;
+};
+
+const getConfig = ({
+ hasEnterprise,
+ apiBaseUrl,
+ host,
+ token,
+}: {
+ hasEnterprise: boolean;
+ apiBaseUrl: string;
+ host: string;
+ token: string;
+}) => ({
+ integrations: {
+ github: [
+ {
+ host,
+ token,
+ ...(hasEnterprise && {
+ apiBaseUrl,
+ }),
+ },
+ ],
+ },
+});
+
+export const github = async (providerAnswers?: GitHubAnswers) => {
+ // TODO(tudi2d): Is GitHub Enterprise a valid setup if there is no Authentication?
+ const answers = await inquirer.prompt([
+ {
+ type: 'confirm',
+ name: 'hasEnterprise',
+ message: 'Are you using GitHub Enterprise?',
+ when: () => typeof providerAnswers === 'undefined',
+ },
+ {
+ type: 'input',
+ name: 'enterpriseInstanceUrl',
+ message: 'What is your URL for GitHub Enterprise?',
+ when: ({ hasEnterprise }) => hasEnterprise,
+ validate: (input: string) => Boolean(new URL(input)),
+ },
+ {
+ type: 'input',
+ name: 'apiBaseUrl',
+ message: 'What is your GitHub Enterprise API path?',
+ default: '/api/v3',
+ when: ({ hasEnterprise }) =>
+ hasEnterprise || providerAnswers?.hasEnterprise,
+ // TODO(tudi2d): Fetch API using OAuth Token if Auth was set up
+ },
+ ]);
+
+ const host = new URL(
+ providerAnswers?.enterpriseInstanceUrl ??
+ answers?.enterpriseInstanceUrl ??
+ 'http://github.com',
+ );
+
+ Task.log(`
+ To create new repositories in GitHub using Software Templates you first need to create a personal access token: ${chalk.blue(
+ `${host.origin}/settings/tokens/new`,
+ )}
+
+ Select the following scopes:
+
+ Reading software components:${chalk.cyan(`
+ - "repo"`)}
+
+ Reading organization data:${chalk.cyan(`
+ - "read:org"
+ - "read:user"
+ - "user:email"`)}
+
+ Publishing software templates:${chalk.cyan(`
+ - "repo"
+ - "workflow" (if templates include GitHub workflows)
+ `)}
+
+ You can find the full documentation page here: ${chalk.blue(
+ 'https://backstage.io/docs/integrations/github/locations',
+ )}
+ `);
+
+ const { token } = await inquirer.prompt<{ token: string }>([
+ {
+ type: 'input',
+ name: 'token',
+ message:
+ 'Please insert your personal access token to setup the GitHub Integration',
+ // TODO(tudi2d): validate
+ },
+ ]);
+
+ const config = getConfig({
+ hasEnterprise: providerAnswers?.hasEnterprise ?? answers.hasEnterprise,
+ apiBaseUrl: host.origin + answers.apiBaseUrl,
+ host: host.hostname,
+ token,
+ });
+
+ Task.log('Setting up Software Templates using GitHub integration for you...');
+
+ await Task.forItem(
+ 'Updating',
+ APP_CONFIG_FILE,
+ async () => await updateConfigFile(APP_CONFIG_FILE, config),
+ );
+};
diff --git a/packages/cli/src/commands/onboard/integrations/index.ts b/packages/cli/src/commands/onboard/integrations/index.ts
new file mode 100644
index 0000000000..5cb2f95b38
--- /dev/null
+++ b/packages/cli/src/commands/onboard/integrations/index.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 inquirer from 'inquirer';
+import { Task } from '../../../lib/tasks';
+import { GitHubAnswers, GitLabAnswers } from '../auth';
+import { github } from './github';
+
+enum Integration {
+ GITHUB = 'GitHub',
+}
+
+const Integrations: Integration[] = [Integration.GITHUB];
+
+export async function integrations(providerInfo?: {
+ provider: string;
+ answers: GitHubAnswers | GitLabAnswers;
+}): Promise {
+ const answers = await inquirer.prompt<{
+ integration?: Integration;
+ shouldUsePreviousProvider: boolean;
+ }>([
+ {
+ type: 'confirm',
+ name: 'shouldUsePreviousProvider',
+ message: `Do you want to keep using ${providerInfo?.provider} as your provider when setting up Software Templates?`,
+ when: () =>
+ providerInfo?.provider &&
+ Object.values(Integrations).includes(
+ providerInfo!.provider as Integration,
+ ),
+ },
+ {
+ // TODO(tudi2d): Let's start with one, but it should be multiple choice in the future
+ type: 'list',
+ name: 'integration',
+ message: 'Please select an integration provider:',
+ choices: Integrations,
+ when: ({ shouldUsePreviousProvider }) => !shouldUsePreviousProvider,
+ },
+ ]);
+
+ if (answers.shouldUsePreviousProvider) {
+ answers.integration = providerInfo!.provider as Integration;
+ }
+
+ switch (answers.integration) {
+ case Integration.GITHUB: {
+ const providerAnswers =
+ providerInfo?.provider === 'GitHub'
+ ? (providerInfo!.answers as GitHubAnswers)
+ : undefined;
+ await github(providerAnswers);
+ break;
+ }
+ default:
+ }
+
+ Task.log();
+ Task.log(`Done setting up ${answers.integration} Integration!`);
+ Task.log();
+}
diff --git a/packages/cli/src/lib/builder/config.ts b/packages/cli/src/lib/builder/config.ts
index ecd755d05b..3e61dd113a 100644
--- a/packages/cli/src/lib/builder/config.ts
+++ b/packages/cli/src/lib/builder/config.ts
@@ -132,6 +132,7 @@ export async function makeRollupConfigs(
/\.woff$/,
/\.woff2$/,
/\.ttf$/,
+ /\.md$/,
],
}),
json(),
diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts
index 01f323a082..6fc7482dce 100644
--- a/packages/cli/src/lib/bundler/config.ts
+++ b/packages/cli/src/lib/bundler/config.ts
@@ -15,7 +15,7 @@
*/
import fs from 'fs-extra';
-import { resolve as resolvePath, join as joinPath } from 'path';
+import { resolve as resolvePath, posix as posixPath } from 'path';
import ForkTsCheckerWebpackPlugin from 'fork-ts-checker-webpack-plugin';
import HtmlWebpackPlugin from 'html-webpack-plugin';
import ModuleScopePlugin from 'react-dev-utils/ModuleScopePlugin';
@@ -236,7 +236,7 @@ export async function createBackendConfig(
const { packages } = await getPackages(cliPaths.targetDir);
const localPackageEntryPoints = packages.flatMap(p => {
const entryPoints = readEntryPoints((p as ExtendedPackage).packageJson);
- return entryPoints.map(e => joinPath(p.packageJson.name, e.mount));
+ return entryPoints.map(e => posixPath.join(p.packageJson.name, e.mount));
});
const moduleDirs = packages.map(p => resolvePath(p.dir, 'node_modules'));
// See frontend config
diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs
index de42f3cdef..10f893d7dc 100644
--- a/packages/cli/templates/default-plugin/package.json.hbs
+++ b/packages/cli/templates/default-plugin/package.json.hbs
@@ -33,7 +33,7 @@
"@backstage/theme": "{{versionQuery '@backstage/theme'}}",
"@material-ui/core": "{{versionQuery '@material-ui/core' '4.12.2'}}",
"@material-ui/icons": "{{versionQuery '@material-ui/icons' '4.9.1'}}",
- "@material-ui/lab": "{{versionQuery '@material-ui/lab' '4.0.0-alpha.57'}}",
+ "@material-ui/lab": "{{versionQuery '@material-ui/lab' '4.0.0-alpha.61'}}",
"react-use": "{{versionQuery 'react-use' '17.2.4'}}"
},
"peerDependencies": {
diff --git a/packages/core-app-api/CHANGELOG.md b/packages/core-app-api/CHANGELOG.md
index 2f54b5d041..af0ca61ef0 100644
--- a/packages/core-app-api/CHANGELOG.md
+++ b/packages/core-app-api/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/core-app-api
+## 1.7.0-next.0
+
+### Minor Changes
+
+- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/types@1.0.2
+ - @backstage/version-bridge@1.0.3
+
## 1.6.0
### Minor Changes
diff --git a/packages/core-app-api/api-report.md b/packages/core-app-api/api-report.md
index 37f7a98de0..d7e4a22873 100644
--- a/packages/core-app-api/api-report.md
+++ b/packages/core-app-api/api-report.md
@@ -25,6 +25,7 @@ import { bitbucketAuthApiRef } from '@backstage/core-plugin-api';
import { bitbucketServerAuthApiRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { Config } from '@backstage/config';
+import { ConfigApi } from '@backstage/core-plugin-api';
import { ConfigReader } from '@backstage/config';
import { DiscoveryApi } from '@backstage/core-plugin-api';
import { ErrorApi } from '@backstage/core-plugin-api';
@@ -264,6 +265,7 @@ export type AuthApiCreateOptions = {
discoveryApi: DiscoveryApi;
environment?: string;
provider?: AuthProviderInfo;
+ configApi?: ConfigApi;
};
// @public
@@ -533,6 +535,7 @@ export class OneLoginAuth {
// @public
export type OneLoginAuthCreateOptions = {
+ configApi?: ConfigApi;
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
diff --git a/packages/core-app-api/config.d.ts b/packages/core-app-api/config.d.ts
index ae876f7fe0..2de1d96114 100644
--- a/packages/core-app-api/config.d.ts
+++ b/packages/core-app-api/config.d.ts
@@ -115,4 +115,11 @@ export interface Config {
*/
environment?: string;
};
+
+ /**
+ $ Enable redirect authentication flow type, instead of a popup for authentication
+ * default value: 'false'
+ * @visibility frontend
+ */
+ enableExperimentalRedirectFlow?: boolean;
}
diff --git a/packages/core-app-api/package.json b/packages/core-app-api/package.json
index c9ae2abc25..a2239301f2 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.6.0",
+ "version": "1.7.0-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
diff --git a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts
index 07ebefca28..3477eec361 100644
--- a/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/atlassian/AtlassianAuth.ts
@@ -32,6 +32,7 @@ const DEFAULT_PROVIDER = {
export default class AtlassianAuth {
static create(options: OAuthApiCreateOptions): typeof atlassianAuthApiRef.T {
const {
+ configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
@@ -39,6 +40,7 @@ export default class AtlassianAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts
index f4bd7cbe85..1335808280 100644
--- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.test.ts
@@ -17,6 +17,7 @@
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import BitbucketAuth from './BitbucketAuth';
+import { MockConfigApi } from '@backstage/test-utils';
const getSession = jest.fn();
@@ -32,6 +33,8 @@ describe('BitbucketAuth', () => {
jest.resetAllMocks();
});
+ const configApi = new MockConfigApi({});
+
it.each([
['team api write_repository', ['team', 'api', 'write_repository']],
['read_repository sudo', ['read_repository', 'sudo']],
@@ -39,6 +42,7 @@ describe('BitbucketAuth', () => {
const gitlabAuth = BitbucketAuth.create({
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
+ configApi: configApi,
});
gitlabAuth.getAccessToken(scope);
diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts
index 7c5d4f3fc0..7a9f1bc094 100644
--- a/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/bitbucket/BitbucketAuth.ts
@@ -47,6 +47,7 @@ const DEFAULT_PROVIDER = {
export default class BitbucketAuth {
static create(options: OAuthApiCreateOptions): typeof bitbucketAuthApiRef.T {
const {
+ configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
@@ -55,6 +56,7 @@ export default class BitbucketAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts
index 32e7755a00..5631a9a886 100644
--- a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.test.ts
@@ -17,6 +17,7 @@
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import BitbucketServerAuth from './BitbucketServerAuth';
+import { MockConfigApi } from '@backstage/test-utils';
const getSession = jest.fn();
@@ -40,7 +41,10 @@ describe('BitbucketServerAuth', () => {
['PROJECT_ADMIN', 'REPO_READ', 'ACCOUNT_WRITE'],
],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
+ const configApi = new MockConfigApi({});
+
const bitbucketServerAuth = BitbucketServerAuth.create({
+ configApi: configApi,
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});
diff --git a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts
index dc50eff6ab..b25deffb93 100644
--- a/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/bitbucketServer/BitbucketServerAuth.ts
@@ -48,6 +48,7 @@ export default class BitbucketServerAuth {
options: OAuthApiCreateOptions,
): typeof bitbucketServerAuthApiRef.T {
const {
+ configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
@@ -56,6 +57,7 @@ export default class BitbucketServerAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts
index ea7b4ac688..fc77754f6f 100644
--- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.test.ts
@@ -17,6 +17,7 @@
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import GithubAuth from './GithubAuth';
+import { MockConfigApi } from '@backstage/test-utils';
const getSession = jest.fn();
@@ -32,8 +33,11 @@ describe('GithubAuth', () => {
jest.resetAllMocks();
});
+ const configApi = new MockConfigApi({});
+
it('should forward access token request to session manager', async () => {
const githubAuth = GithubAuth.create({
+ configApi: configApi,
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});
diff --git a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts
index 7efe4e95c6..943dfa7da4 100644
--- a/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/github/GithubAuth.ts
@@ -32,6 +32,7 @@ const DEFAULT_PROVIDER = {
export default class GithubAuth {
static create(options: OAuthApiCreateOptions): typeof githubAuthApiRef.T {
const {
+ configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
@@ -40,6 +41,7 @@ export default class GithubAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts
index 3346af9b7c..6dde0c0334 100644
--- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.test.ts
@@ -17,6 +17,7 @@
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
import GitlabAuth from './GitlabAuth';
+import { MockConfigApi } from '@backstage/test-utils';
const getSession = jest.fn();
@@ -39,7 +40,10 @@ describe('GitlabAuth', () => {
],
['read_repository sudo', ['read_repository', 'sudo']],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
+ const configApi = new MockConfigApi({});
+
const gitlabAuth = GitlabAuth.create({
+ configApi: configApi,
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});
diff --git a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts
index 00085de8ac..c39f5a6e23 100644
--- a/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/gitlab/GitlabAuth.ts
@@ -32,6 +32,7 @@ const DEFAULT_PROVIDER = {
export default class GitlabAuth {
static create(options: OAuthApiCreateOptions): typeof gitlabAuthApiRef.T {
const {
+ configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
@@ -40,6 +41,7 @@ export default class GitlabAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts
index f8a1c3a1f5..7897eef42e 100644
--- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.test.ts
@@ -17,6 +17,7 @@
import GoogleAuth from './GoogleAuth';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
+import { MockConfigApi } from '@backstage/test-utils';
const PREFIX = 'https://www.googleapis.com/auth/';
@@ -58,7 +59,10 @@ describe('GoogleAuth', () => {
[`${PREFIX}profile`, [`${PREFIX}profile`]],
[`${PREFIX}openid`, [`${PREFIX}openid`]],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
+ const configApi = new MockConfigApi({});
+
const googleAuth = GoogleAuth.create({
+ configApi: configApi,
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});
diff --git a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts
index 8a528f4511..f4674ab64f 100644
--- a/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/google/GoogleAuth.ts
@@ -34,6 +34,7 @@ const SCOPE_PREFIX = 'https://www.googleapis.com/auth/';
export default class GoogleAuth {
static create(options: OAuthApiCreateOptions): typeof googleAuthApiRef.T {
const {
+ configApi,
discoveryApi,
oauthRequestApi,
environment = 'development',
@@ -46,6 +47,7 @@ export default class GoogleAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
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 be5776609d..30cfde7c6e 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
@@ -32,6 +32,7 @@ const DEFAULT_PROVIDER = {
export default class MicrosoftAuth {
static create(options: OAuthApiCreateOptions): typeof microsoftAuthApiRef.T {
const {
+ configApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
oauthRequestApi,
@@ -46,6 +47,7 @@ export default class MicrosoftAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts
index 63070120ef..6f941379a7 100644
--- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.test.ts
@@ -17,6 +17,7 @@
import OAuth2 from './OAuth2';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
+import { MockConfigApi } from '@backstage/test-utils';
const theFuture = new Date(Date.now() + 3600000);
const thePast = new Date(Date.now() - 10);
@@ -34,12 +35,15 @@ jest.mock('../../../../lib/AuthSessionManager', () => ({
},
}));
+const configApi = new MockConfigApi({});
+
describe('OAuth2', () => {
it('should get refreshed access token', async () => {
getSession = jest.fn().mockResolvedValue({
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const oauth2 = OAuth2.create({
+ configApi: configApi,
scopeTransform: scopeTransform,
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
@@ -58,6 +62,7 @@ describe('OAuth2', () => {
providerInfo: { accessToken: 'access-token', expiresAt: theFuture },
});
const oauth2 = OAuth2.create({
+ configApi: configApi,
scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`),
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
@@ -75,7 +80,9 @@ describe('OAuth2', () => {
getSession = jest.fn().mockResolvedValue({
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
+
const oauth2 = OAuth2.create({
+ configApi: configApi,
scopeTransform: scopeTransform,
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
@@ -94,6 +101,7 @@ describe('OAuth2', () => {
providerInfo: { idToken: 'id-token', expiresAt: theFuture },
});
const oauth2 = OAuth2.create({
+ configApi: configApi,
scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`),
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
@@ -121,6 +129,7 @@ describe('OAuth2', () => {
})
.mockRejectedValue(error);
const oauth2 = OAuth2.create({
+ configApi: configApi,
scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`),
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
@@ -155,6 +164,7 @@ describe('OAuth2', () => {
},
});
const oauth2 = OAuth2.create({
+ configApi: configApi,
scopeTransform: scopes => scopes.map(scope => `my-prefix/${scope}`),
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
diff --git a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts
index 6f88178415..44bd5269ea 100644
--- a/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/oauth2/OAuth2.ts
@@ -72,6 +72,7 @@ export default class OAuth2
{
static create(options: OAuth2CreateOptions) {
const {
+ configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
@@ -81,6 +82,7 @@ export default class OAuth2
} = options;
const connector = new DefaultAuthConnector({
+ configApi,
discoveryApi,
environment,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts
index 6489b326ff..5e7370a947 100644
--- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.test.ts
@@ -17,6 +17,7 @@
import OktaAuth from './OktaAuth';
import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi';
import { UrlPatternDiscovery } from '../../DiscoveryApi';
+import { MockConfigApi } from '@backstage/test-utils';
const PREFIX = 'okta.';
@@ -50,7 +51,9 @@ describe('OktaAuth', () => {
[`${PREFIX}profile`, [`${PREFIX}profile`]],
[`${PREFIX}openid`, [`${PREFIX}openid`]],
])(`should normalize scopes correctly - %p`, (scope, scopes) => {
+ const configApi = new MockConfigApi({});
const auth = OktaAuth.create({
+ configApi: configApi,
oauthRequestApi: new MockOAuthApi(),
discoveryApi: UrlPatternDiscovery.compile('http://example.com'),
});
diff --git a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts
index 42c0ddd0ed..c1921a8f93 100644
--- a/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/okta/OktaAuth.ts
@@ -44,6 +44,7 @@ const OKTA_SCOPE_PREFIX: string = 'okta.';
export default class OktaAuth {
static create(options: OAuthApiCreateOptions): typeof oktaAuthApiRef.T {
const {
+ configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
@@ -52,6 +53,7 @@ export default class OktaAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts
index ea9e2cad28..d8f54e7e50 100644
--- a/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/onelogin/OneLoginAuth.ts
@@ -18,6 +18,7 @@ import {
oneloginAuthApiRef,
OAuthRequestApi,
AuthProviderInfo,
+ ConfigApi,
DiscoveryApi,
} from '@backstage/core-plugin-api';
import { OAuth2 } from '../oauth2';
@@ -27,6 +28,7 @@ import { OAuth2 } from '../oauth2';
* @public
*/
export type OneLoginAuthCreateOptions = {
+ configApi?: ConfigApi;
discoveryApi: DiscoveryApi;
oauthRequestApi: OAuthRequestApi;
environment?: string;
@@ -61,6 +63,7 @@ export default class OneLoginAuth {
options: OneLoginAuthCreateOptions,
): typeof oneloginAuthApiRef.T {
const {
+ configApi,
discoveryApi,
environment = 'development',
provider = DEFAULT_PROVIDER,
@@ -68,6 +71,7 @@ export default class OneLoginAuth {
} = options;
return OAuth2.create({
+ configApi,
discoveryApi,
oauthRequestApi,
provider,
diff --git a/packages/core-app-api/src/apis/implementations/auth/types.ts b/packages/core-app-api/src/apis/implementations/auth/types.ts
index 55d6c19098..7a74fd3a3b 100644
--- a/packages/core-app-api/src/apis/implementations/auth/types.ts
+++ b/packages/core-app-api/src/apis/implementations/auth/types.ts
@@ -16,6 +16,7 @@
import {
AuthProviderInfo,
+ ConfigApi,
DiscoveryApi,
OAuthRequestApi,
} from '@backstage/core-plugin-api';
@@ -37,4 +38,5 @@ export type AuthApiCreateOptions = {
discoveryApi: DiscoveryApi;
environment?: string;
provider?: AuthProviderInfo;
+ configApi?: ConfigApi;
};
diff --git a/packages/core-app-api/src/app/types.ts b/packages/core-app-api/src/app/types.ts
index 75b8fb2a34..3d9005b0c0 100644
--- a/packages/core-app-api/src/app/types.ts
+++ b/packages/core-app-api/src/app/types.ts
@@ -45,7 +45,7 @@ export type BootErrorPageProps = {
*/
export type SignInPageProps = {
/**
- * Set the IdentityApi on successful sign in. This should only be called once.
+ * Set the IdentityApi on successful sign-in. This should only be called once.
*/
onSignInSuccess(identityApi: IdentityApi): void;
};
diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts
index 2b132e09cd..51da9cbfb5 100644
--- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts
+++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts
@@ -21,6 +21,8 @@ import { UrlPatternDiscovery } from '../../apis';
import { setupRequestMockHandlers } from '@backstage/test-utils';
import { setupServer } from 'msw/node';
import { rest } from 'msw';
+import { ConfigReader } from '@backstage/config';
+import { ConfigApi } from '@backstage/core-plugin-api';
jest.mock('../loginPopup', () => {
return {
@@ -28,6 +30,10 @@ jest.mock('../loginPopup', () => {
};
});
+const configApi: ConfigApi = new ConfigReader({
+ enableExperimentalRedirectFlow: false,
+});
+
const defaultOptions = {
discoveryApi: UrlPatternDiscovery.compile('http://my-host/api/{{pluginId}}'),
environment: 'production',
@@ -42,6 +48,7 @@ const defaultOptions = {
scopes: new Set(res.scopes.split(' ')),
expiresAt: new Date(Date.now() + expiresInSeconds * 1000),
}),
+ configApi: configApi,
};
describe('DefaultAuthConnector', () => {
@@ -131,7 +138,7 @@ describe('DefaultAuthConnector', () => {
expect(popupSpy).toHaveBeenCalledTimes(1);
expect(popupSpy.mock.calls[0][0]).toMatchObject({
- url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&origin=http%3A%2F%2Flocalhost&env=production',
+ url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&origin=http%3A%2F%2Flocalhost&flow=popup&env=production',
});
await expect(sessionPromise).resolves.toEqual({
@@ -179,7 +186,7 @@ describe('DefaultAuthConnector', () => {
expect(popupSpy).toHaveBeenCalledTimes(1);
expect(popupSpy.mock.calls[0][0]).toMatchObject({
- url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&origin=http%3A%2F%2Flocalhost&env=production',
+ url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&origin=http%3A%2F%2Flocalhost&flow=popup&env=production',
});
});
});
diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts
index 988c9f17b1..f2c35b0ba3 100644
--- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts
+++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts
@@ -13,16 +13,18 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-
import {
- OAuthRequester,
- OAuthRequestApi,
AuthProviderInfo,
+ ConfigApi,
DiscoveryApi,
+ OAuthRequestApi,
+ OAuthRequester,
} from '@backstage/core-plugin-api';
import { showLoginPopup } from '../loginPopup';
import { AuthConnector, CreateSessionOptions } from './types';
+let warned = false;
+
type Options = {
/**
* DiscoveryApi instance used to locate the auth backend endpoint.
@@ -49,6 +51,10 @@ type Options = {
* Function used to transform an auth response into the session type.
*/
sessionTransform?(response: any): AuthSession | Promise;
+ /**
+ * ConfigApi instance used to configure authentication flow of pop-up or redirect.
+ */
+ configApi?: ConfigApi;
};
function defaultJoinScopes(scopes: Set) {
@@ -69,9 +75,10 @@ export class DefaultAuthConnector
private readonly joinScopesFunc: (scopes: Set) => string;
private readonly authRequester: OAuthRequester;
private readonly sessionTransform: (response: any) => Promise;
-
+ private readonly enableExperimentalRedirectFlow: boolean;
constructor(options: Options) {
const {
+ configApi,
discoveryApi,
environment,
provider,
@@ -80,9 +87,26 @@ export class DefaultAuthConnector
sessionTransform = id => id,
} = options;
+ if (!warned && !configApi) {
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'DEPRECATION WARNING: Authentication providers require a configApi instance to configure the authentication flow. Please provide one to the authentication provider constructor.',
+ );
+ warned = true;
+ }
+
+ this.enableExperimentalRedirectFlow = configApi
+ ? configApi.getOptionalBoolean('enableExperimentalRedirectFlow') ?? false
+ : false;
+
this.authRequester = oauthRequestApi.createAuthRequester({
provider,
- onAuthRequest: scopes => this.showPopup(scopes),
+ onAuthRequest: async scopes => {
+ if (!this.enableExperimentalRedirectFlow) {
+ return this.showPopup(scopes);
+ }
+ return this.executeRedirect(scopes);
+ },
});
this.discoveryApi = discoveryApi;
@@ -94,6 +118,9 @@ export class DefaultAuthConnector
async createSession(options: CreateSessionOptions): Promise {
if (options.instantPopup) {
+ if (this.enableExperimentalRedirectFlow) {
+ return this.executeRedirect(options.scopes);
+ }
return this.showPopup(options.scopes);
}
return this.authRequester(options.scopes);
@@ -155,6 +182,7 @@ export class DefaultAuthConnector
const popupUrl = await this.buildUrl('/start', {
scope,
origin: window.location.origin,
+ flow: 'popup',
});
const payload = await showLoginPopup({
@@ -168,6 +196,19 @@ export class DefaultAuthConnector
return await this.sessionTransform(payload);
}
+ private async executeRedirect(scopes: Set): Promise {
+ const scope = this.joinScopesFunc(scopes);
+ // redirect to auth api
+ window.location.href = await this.buildUrl('/start', {
+ scope,
+ origin: window.location.origin,
+ redirectUrl: window.location.href,
+ flow: 'redirect',
+ });
+ // return a promise that never resolves
+ return new Promise(() => {});
+ }
+
private async buildUrl(
path: string,
query?: { [key: string]: string | boolean | undefined },
diff --git a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
index d31f5e29bf..67d83bb36a 100644
--- a/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
+++ b/packages/core-app-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts
@@ -93,7 +93,7 @@ export class RefreshingAuthSessionManager implements SessionManager {
//
// We skip this check if an instant login popup is requested, as we need to
// stay in a synchronous call stack from the user interaction. The downside
- // is that that the user will sometimes be requested to log in even if they
+ // is that the user will sometimes be requested to log in even if they
// already had an existing session.
if (!this.currentSession && !options.instantPopup) {
try {
diff --git a/packages/core-components/CHANGELOG.md b/packages/core-components/CHANGELOG.md
index a7ca828bb3..b8e29342dc 100644
--- a/packages/core-components/CHANGELOG.md
+++ b/packages/core-components/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/core-components
+## 0.12.6-next.0
+
+### Patch Changes
+
+- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- 7245e744ab1: Fixed the font color on `BackstageHeaderLabel` to respect the active page theme.
+- Updated dependencies
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/version-bridge@1.0.3
+
## 0.12.5
### Patch Changes
diff --git a/packages/core-components/package.json b/packages/core-components/package.json
index 77e91aa751..e210219ef3 100644
--- a/packages/core-components/package.json
+++ b/packages/core-components/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/core-components",
"description": "Core components used by Backstage plugins and apps",
- "version": "0.12.5",
+ "version": "0.12.6-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
@@ -40,7 +40,7 @@
"@material-table/core": "^3.1.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@react-hookz/web": "^20.0.0",
"@types/react-sparklines": "^1.7.0",
"@types/react-text-truncate": "^0.14.0",
diff --git a/packages/core-components/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx b/packages/core-components/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx
index 02b1afe4a1..0159d8f503 100644
--- a/packages/core-components/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx
+++ b/packages/core-components/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx
@@ -24,7 +24,11 @@ import Button from '@material-ui/core/Button';
import React, { useMemo, useState } from 'react';
import useObservable from 'react-use/lib/useObservable';
import LoginRequestListItem from './LoginRequestListItem';
-import { useApi, oauthRequestApiRef } from '@backstage/core-plugin-api';
+import {
+ useApi,
+ configApiRef,
+ oauthRequestApiRef,
+} from '@backstage/core-plugin-api';
import Typography from '@material-ui/core/Typography';
export type OAuthRequestDialogClassKey =
@@ -58,6 +62,11 @@ export function OAuthRequestDialog(_props: {}) {
const classes = useStyles();
const [busy, setBusy] = useState(false);
const oauthRequestApi = useApi(oauthRequestApiRef);
+ const configApi = useApi(configApiRef);
+
+ const authRedirect =
+ configApi.getOptionalBoolean('enableExperimentalRedirectFlow') ?? false;
+
const requests = useObservable(
useMemo(() => oauthRequestApi.authRequest$(), [oauthRequestApi]),
[],
@@ -83,6 +92,11 @@ export function OAuthRequestDialog(_props: {}) {
Login Required
+ {authRedirect ? (
+
+ This will trigger a http redirect to OAuth Login.
+
+ ) : null}
diff --git a/packages/core-components/src/layout/HeaderLabel/HeaderLabel.tsx b/packages/core-components/src/layout/HeaderLabel/HeaderLabel.tsx
index e3b6299f85..0ea2e163a7 100644
--- a/packages/core-components/src/layout/HeaderLabel/HeaderLabel.tsx
+++ b/packages/core-components/src/layout/HeaderLabel/HeaderLabel.tsx
@@ -14,8 +14,9 @@
* limitations under the License.
*/
+import { BackstageTheme } from '@backstage/theme';
import Grid from '@material-ui/core/Grid';
-import { makeStyles } from '@material-ui/core/styles';
+import { alpha, makeStyles } from '@material-ui/core/styles';
import Typography from '@material-ui/core/Typography';
import React from 'react';
import { Link } from '../../components/Link';
@@ -23,13 +24,13 @@ import { Link } from '../../components/Link';
/** @public */
export type HeaderLabelClassKey = 'root' | 'label' | 'value';
-const useStyles = makeStyles(
+const useStyles = makeStyles(
theme => ({
root: {
textAlign: 'left',
},
label: {
- color: theme.palette.common.white,
+ color: theme.page.fontColor,
fontWeight: theme.typography.fontWeightBold,
letterSpacing: 0,
fontSize: theme.typography.fontSize,
@@ -37,7 +38,7 @@ const useStyles = makeStyles(
lineHeight: 1,
},
value: {
- color: 'rgba(255, 255, 255, 0.8)',
+ color: alpha(theme.page.fontColor, 0.8),
fontSize: theme.typography.fontSize,
lineHeight: 1,
},
@@ -50,9 +51,16 @@ type HeaderLabelContentProps = {
className: string;
};
-const HeaderLabelContent = ({ value, className }: HeaderLabelContentProps) => (
- {value}
-);
+const HeaderLabelContent = ({ value, className }: HeaderLabelContentProps) => {
+ return (
+
+ {value}
+
+ );
+};
type HeaderLabelProps = {
label: string;
diff --git a/packages/core-components/src/layout/SignInPage/commonProvider.tsx b/packages/core-components/src/layout/SignInPage/commonProvider.tsx
index 8f426a3907..1827981e6f 100644
--- a/packages/core-components/src/layout/SignInPage/commonProvider.tsx
+++ b/packages/core-components/src/layout/SignInPage/commonProvider.tsx
@@ -29,17 +29,24 @@ import { GridItem } from './styles';
import { ForwardedError } from '@backstage/errors';
import { UserIdentity } from './UserIdentity';
-const Component: ProviderComponent = ({ config, onSignInSuccess }) => {
+const Component: ProviderComponent = ({
+ config,
+ onSignInStarted,
+ onSignInSuccess,
+ onSignInFailure,
+}) => {
const { apiRef, title, message } = config as SignInProviderConfig;
const authApi = useApi(apiRef);
const errorApi = useApi(errorApiRef);
const handleLogin = async () => {
try {
+ onSignInStarted();
const identityResponse = await authApi.getBackstageIdentity({
instantPopup: true,
});
if (!identityResponse) {
+ onSignInFailure();
throw new Error(
`The ${title} provider is not configured to support sign-in`,
);
@@ -55,6 +62,7 @@ const Component: ProviderComponent = ({ config, onSignInSuccess }) => {
}),
);
} catch (error) {
+ onSignInFailure();
errorApi.post(new ForwardedError('Login failed', error));
}
};
diff --git a/packages/core-components/src/layout/SignInPage/customProvider.tsx b/packages/core-components/src/layout/SignInPage/customProvider.tsx
index 101c74d3cd..ad02715447 100644
--- a/packages/core-components/src/layout/SignInPage/customProvider.tsx
+++ b/packages/core-components/src/layout/SignInPage/customProvider.tsx
@@ -61,7 +61,7 @@ const asInputRef = (renderResult: UseFormRegisterReturn) => {
};
};
-const Component: ProviderComponent = ({ onSignInSuccess }) => {
+const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => {
const classes = useFormStyles();
const { register, handleSubmit, formState } = useForm({
mode: 'onChange',
@@ -70,6 +70,7 @@ const Component: ProviderComponent = ({ onSignInSuccess }) => {
const { errors } = formState;
const handleResult = ({ userId, idToken }: Data) => {
+ onSignInStarted();
onSignInSuccess(
UserIdentity.fromLegacy({
userId,
diff --git a/packages/core-components/src/layout/SignInPage/guestProvider.tsx b/packages/core-components/src/layout/SignInPage/guestProvider.tsx
index b2370a98d5..5393c1ea89 100644
--- a/packages/core-components/src/layout/SignInPage/guestProvider.tsx
+++ b/packages/core-components/src/layout/SignInPage/guestProvider.tsx
@@ -22,7 +22,7 @@ import { GridItem } from './styles';
import { ProviderComponent, ProviderLoader, SignInProvider } from './types';
import { GuestUserIdentity } from './GuestUserIdentity';
-const Component: ProviderComponent = ({ onSignInSuccess }) => (
+const Component: ProviderComponent = ({ onSignInStarted, onSignInSuccess }) => (
(
onSignInSuccess(new GuestUserIdentity())}
+ onClick={() => {
+ onSignInStarted();
+ onSignInSuccess(new GuestUserIdentity());
+ }}
>
Enter
diff --git a/packages/core-components/src/layout/SignInPage/providers.tsx b/packages/core-components/src/layout/SignInPage/providers.tsx
index 23d6c95b3c..e456a6948b 100644
--- a/packages/core-components/src/layout/SignInPage/providers.tsx
+++ b/packages/core-components/src/layout/SignInPage/providers.tsx
@@ -134,6 +134,7 @@ export const useSignInProviders = (
.loader(apiHolder, provider.config?.apiRef!)
.then(result => {
if (didCancel) {
+ localStorage.removeItem(PROVIDER_STORAGE_KEY);
return;
}
if (result) {
@@ -143,10 +144,10 @@ export const useSignInProviders = (
}
})
.catch(error => {
+ localStorage.removeItem(PROVIDER_STORAGE_KEY);
if (didCancel) {
return;
}
- localStorage.removeItem(PROVIDER_STORAGE_KEY);
errorApi.post(error);
setLoading(false);
});
@@ -172,16 +173,27 @@ export const useSignInProviders = (
const { Component } = provider.components;
const handleSignInSuccess = (result: IdentityApi) => {
- localStorage.setItem(PROVIDER_STORAGE_KEY, provider.id);
-
handleWrappedResult(result);
};
+ const handleSignInStarted = () => {
+ localStorage.setItem(
+ PROVIDER_STORAGE_KEY,
+ provider?.config?.id || provider.id,
+ );
+ };
+
+ const handleSignInFailure = () => {
+ localStorage.removeItem(PROVIDER_STORAGE_KEY);
+ };
+
return (
);
}),
diff --git a/packages/core-components/src/layout/SignInPage/types.ts b/packages/core-components/src/layout/SignInPage/types.ts
index 8a0430a27d..a7e1ca62c2 100644
--- a/packages/core-components/src/layout/SignInPage/types.ts
+++ b/packages/core-components/src/layout/SignInPage/types.ts
@@ -35,8 +35,21 @@ export type SignInProviderConfig = {
/** @public */
export type IdentityProviders = ('guest' | 'custom' | SignInProviderConfig)[];
+/**
+ * Invoked when the sign-in process has failed.
+ */
+export type onSignInFailure = () => void;
+/**
+ * Invoked when the sign-in process has started.
+ */
+export type onSignInStarted = () => void;
+
export type ProviderComponent = ComponentType<
- SignInPageProps & { config: SignInProviderConfig }
+ SignInPageProps & {
+ config: SignInProviderConfig;
+ onSignInStarted(): void;
+ onSignInFailure(): void;
+ }
>;
export type ProviderLoader = (
diff --git a/packages/create-app/CHANGELOG.md b/packages/create-app/CHANGELOG.md
index 2639ba9a59..3ce499d386 100644
--- a/packages/create-app/CHANGELOG.md
+++ b/packages/create-app/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/create-app
+## 0.4.39-next.0
+
+### Patch Changes
+
+- Bumped create-app version.
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+
## 0.4.38
### Patch Changes
diff --git a/packages/create-app/package.json b/packages/create-app/package.json
index 9f797f5d8c..264ca6e017 100644
--- a/packages/create-app/package.json
+++ b/packages/create-app/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/create-app",
"description": "A CLI that helps you create your own Backstage app",
- "version": "0.4.38",
+ "version": "0.4.39-next.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts
index ef46f07870..fd424a3257 100644
--- a/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts
+++ b/packages/create-app/templates/default-app/packages/backend/src/plugins/scaffolder.ts
@@ -17,5 +17,6 @@ export default async function createPlugin(
reader: env.reader,
catalogClient,
identity: env.identity,
+ permissionApi: env.permissions,
});
}
diff --git a/packages/dev-utils/CHANGELOG.md b/packages/dev-utils/CHANGELOG.md
index 3c6fbaefd5..e151728a1b 100644
--- a/packages/dev-utils/CHANGELOG.md
+++ b/packages/dev-utils/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/dev-utils
+## 1.0.14-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/app-defaults@1.3.0-next.0
+ - @backstage/core-app-api@1.7.0-next.0
+ - @backstage/test-utils@1.3.0-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/integration-react@1.1.12-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 1.0.13
### Patch Changes
diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json
index d8559535d8..0568a44472 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.13",
+ "version": "1.0.14-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 2f7c5d0704..a3c5ffa88e 100644
--- a/packages/e2e-test/CHANGELOG.md
+++ b/packages/e2e-test/CHANGELOG.md
@@ -1,5 +1,14 @@
# e2e-test
+## 0.2.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/create-app@0.4.39-next.0
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.1.5
+
## 0.2.1
### Patch Changes
diff --git a/packages/e2e-test/package.json b/packages/e2e-test/package.json
index d2c11a7e15..833c5a1eca 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.1",
+ "version": "0.2.2-next.0",
"private": true,
"backstage": {
"role": "cli"
diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md
index 49cf9b5df5..5a3b5dc5c4 100644
--- a/packages/eslint-plugin/CHANGELOG.md
+++ b/packages/eslint-plugin/CHANGELOG.md
@@ -1,5 +1,11 @@
# @backstage/eslint-plugin
+## 0.1.3-next.0
+
+### Patch Changes
+
+- 911c25de59c: Add support for auto-fixing missing imports detected by the `no-undeclared-imports` rule.
+
## 0.1.2
### Patch Changes
diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json
index feb03921fd..5a37dcc871 100644
--- a/packages/eslint-plugin/package.json
+++ b/packages/eslint-plugin/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/eslint-plugin",
"description": "Backstage ESLint plugin",
- "version": "0.1.2",
+ "version": "0.1.3-next.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/integration-react/CHANGELOG.md b/packages/integration-react/CHANGELOG.md
index 06568497df..02a8c9b7e4 100644
--- a/packages/integration-react/CHANGELOG.md
+++ b/packages/integration-react/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/integration-react
+## 1.1.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/theme@0.2.18
+
## 1.1.11
### Patch Changes
diff --git a/packages/integration-react/package.json b/packages/integration-react/package.json
index 681542a313..39a8a9908a 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.11",
+ "version": "1.1.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,7 +30,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/packages/repo-tools/CHANGELOG.md b/packages/repo-tools/CHANGELOG.md
index c7577cb711..5893c3d87c 100644
--- a/packages/repo-tools/CHANGELOG.md
+++ b/packages/repo-tools/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/repo-tools
+## 0.1.4-next.0
+
+### Patch Changes
+
+- 9129ca8cabb: Log API report instructions when api-report is missing.
+- Updated dependencies
+ - @backstage/cli-common@0.1.12
+ - @backstage/errors@1.1.5
+
## 0.1.3
### Patch Changes
diff --git a/packages/repo-tools/package.json b/packages/repo-tools/package.json
index 35ea6165d3..fb1d0c4af8 100644
--- a/packages/repo-tools/package.json
+++ b/packages/repo-tools/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/repo-tools",
"description": "CLI for Backstage repo tooling ",
- "version": "0.1.3",
+ "version": "0.1.4-next.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/repo-tools/src/commands/api-reports/api-extractor.ts b/packages/repo-tools/src/commands/api-reports/api-extractor.ts
index 1fe7602617..8194ede7a4 100644
--- a/packages/repo-tools/src/commands/api-reports/api-extractor.ts
+++ b/packages/repo-tools/src/commands/api-reports/api-extractor.ts
@@ -487,6 +487,9 @@ export async function runApiExtraction({
showVerboseMessages: false,
showDiagnostics: false,
messageCallback(message) {
+ if (message.text.includes('The API report file is missing')) {
+ shouldLogInstructions = true;
+ }
if (
message.text.includes(
'You have changed the public API signature for this project.',
@@ -506,7 +509,11 @@ export async function runApiExtraction({
// This release tag validation makes sure that the release tag of known entry points match expectations.
// The root index entrypoint is only allowed @public exports, while /alpha and /beta only allow @alpha and @beta.
- if (validateReleaseTags && !usesExperimentalTypeBuild) {
+ if (
+ validateReleaseTags &&
+ !usesExperimentalTypeBuild &&
+ fs.pathExistsSync(extractorConfig.reportFilePath)
+ ) {
if (['index', 'alpha', 'beta'].includes(name)) {
const report = await fs.readFile(
extractorConfig.reportFilePath,
diff --git a/packages/techdocs-cli-embedded-app/CHANGELOG.md b/packages/techdocs-cli-embedded-app/CHANGELOG.md
index 4b74967ddf..e1c03b9f9c 100644
--- a/packages/techdocs-cli-embedded-app/CHANGELOG.md
+++ b/packages/techdocs-cli-embedded-app/CHANGELOG.md
@@ -1,5 +1,24 @@
# techdocs-cli-embedded-app
+## 0.2.81-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog@1.10.0-next.0
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/app-defaults@1.3.0-next.0
+ - @backstage/core-app-api@1.7.0-next.0
+ - @backstage/test-utils@1.3.0-next.0
+ - @backstage/plugin-techdocs@1.6.1-next.0
+ - @backstage/cli@0.22.6-next.0
+ - @backstage/plugin-techdocs-react@1.1.5-next.0
+ - @backstage/integration-react@1.1.12-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.2.80
### Patch Changes
diff --git a/packages/techdocs-cli-embedded-app/package.json b/packages/techdocs-cli-embedded-app/package.json
index 4400ff082d..eab13938f4 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.80",
+ "version": "0.2.81-next.0",
"private": true,
"backstage": {
"role": "frontend"
diff --git a/packages/techdocs-cli/CHANGELOG.md b/packages/techdocs-cli/CHANGELOG.md
index 3467fe86a5..9c1aaee26a 100644
--- a/packages/techdocs-cli/CHANGELOG.md
+++ b/packages/techdocs-cli/CHANGELOG.md
@@ -1,5 +1,17 @@
# @techdocs/cli
+## 1.4.1-next.0
+
+### Patch Changes
+
+- b348420a804: Adding global-agent to enable the ability to publish through a proxy
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/cli-common@0.1.12
+ - @backstage/plugin-techdocs-node@1.6.1-next.0
+
## 1.4.0
### Minor Changes
diff --git a/packages/techdocs-cli/README.md b/packages/techdocs-cli/README.md
index ae296d2c26..85c0075946 100644
--- a/packages/techdocs-cli/README.md
+++ b/packages/techdocs-cli/README.md
@@ -40,6 +40,14 @@ yarn start
yarn techdocs-cli:dev [...options]
```
+### Connecting behind a proxy
+
+```sh
+# Prior to executing the techdocs-cli command
+export GLOBAL_AGENT_HTTPS_PROXY=${HTTP_PROXY}
+export GLOBAL_AGENT_NO_PROXY=${NO_PROXY}
+```
+
### Using an example docs project
For the purpose of local development, we have created an example documentation project. You are of course also free to create your own local test site - all it takes is a `docs/index.md` and an `mkdocs.yml` in a directory.
diff --git a/packages/techdocs-cli/package.json b/packages/techdocs-cli/package.json
index d18213d490..c8929f3d6a 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.0",
+ "version": "1.4.1-next.0",
"publishConfig": {
"access": "public"
},
@@ -69,6 +69,7 @@
"commander": "^9.1.0",
"dockerode": "^3.3.1",
"fs-extra": "^10.0.1",
+ "global-agent": "^3.0.0",
"http-proxy": "^1.18.1",
"react-dev-utils": "^12.0.0-next.60",
"serve-handler": "^6.1.3",
diff --git a/packages/test-utils/CHANGELOG.md b/packages/test-utils/CHANGELOG.md
index 2d0f57cf12..382cce8c09 100644
--- a/packages/test-utils/CHANGELOG.md
+++ b/packages/test-utils/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/test-utils
+## 1.3.0-next.0
+
+### Minor Changes
+
+- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-app-api@1.7.0-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-permission-react@0.4.11
+ - @backstage/theme@0.2.18
+ - @backstage/types@1.0.2
+ - @backstage/plugin-permission-common@0.7.4
+
## 1.2.6
### Patch Changes
diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json
index 7bd93443e9..7670d43cbd 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.2.6",
+ "version": "1.3.0-next.0",
"publishConfig": {
"access": "public"
},
diff --git a/packages/test-utils/src/testUtils/defaultApis.ts b/packages/test-utils/src/testUtils/defaultApis.ts
index 1ff10bfe6f..2231d28cd0 100644
--- a/packages/test-utils/src/testUtils/defaultApis.ts
+++ b/packages/test-utils/src/testUtils/defaultApis.ts
@@ -89,6 +89,7 @@ export const defaultApis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GoogleAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -103,6 +104,7 @@ export const defaultApis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
MicrosoftAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -117,6 +119,7 @@ export const defaultApis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GithubAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
defaultScopes: ['read:user'],
@@ -132,6 +135,7 @@ export const defaultApis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OktaAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -146,6 +150,7 @@ export const defaultApis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
GitlabAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -160,6 +165,7 @@ export const defaultApis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
OneLoginAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
@@ -174,6 +180,7 @@ export const defaultApis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) =>
BitbucketAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
defaultScopes: ['team'],
@@ -189,6 +196,7 @@ export const defaultApis = [
},
factory: ({ discoveryApi, oauthRequestApi, configApi }) => {
return AtlassianAuth.create({
+ configApi,
discoveryApi,
oauthRequestApi,
environment: configApi.getOptionalString('auth.environment'),
diff --git a/plugins/adr-backend/CHANGELOG.md b/plugins/adr-backend/CHANGELOG.md
index c74dbd116a..53fb3b9f8d 100644
--- a/plugins/adr-backend/CHANGELOG.md
+++ b/plugins/adr-backend/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-adr-backend
+## 0.3.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-adr-common@0.2.7
+ - @backstage/plugin-search-common@1.2.2
+
## 0.3.1
### Patch Changes
diff --git a/plugins/adr-backend/package.json b/plugins/adr-backend/package.json
index e759373e6c..fb0c211284 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.1",
+ "version": "0.3.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/adr-backend/src/index.ts b/plugins/adr-backend/src/index.ts
index 5cc8f50b72..89c88ab3e7 100644
--- a/plugins/adr-backend/src/index.ts
+++ b/plugins/adr-backend/src/index.ts
@@ -15,7 +15,7 @@
*/
/**
- * ADR backend plugin
+ * Architecture Decision Records backend plugin.
*
* @packageDocumentation
*/
diff --git a/plugins/adr/CHANGELOG.md b/plugins/adr/CHANGELOG.md
index b5942d88a4..f67ab9bc20 100644
--- a/plugins/adr/CHANGELOG.md
+++ b/plugins/adr/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/plugin-adr
+## 0.4.2-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-search-react@1.5.2-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/integration-react@1.1.12-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-adr-common@0.2.7
+ - @backstage/plugin-search-common@1.2.2
+
## 0.4.1
### Patch Changes
diff --git a/plugins/adr/package.json b/plugins/adr/package.json
index 7b636f4b1a..453ff62792 100644
--- a/plugins/adr/package.json
+++ b/plugins/adr/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-adr",
- "version": "0.4.1",
+ "version": "0.4.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,7 +33,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-markdown": "^8.0.0",
"react-use": "^17.2.4",
"remark-gfm": "^3.0.1"
diff --git a/plugins/adr/src/index.ts b/plugins/adr/src/index.ts
index f24b3ac531..f405621c4a 100644
--- a/plugins/adr/src/index.ts
+++ b/plugins/adr/src/index.ts
@@ -15,7 +15,7 @@
*/
/**
- * ADR frontend plugin
+ * Architecture Decision Records frontend plugin.
*
* @packageDocumentation
*/
diff --git a/plugins/airbrake-backend/CHANGELOG.md b/plugins/airbrake-backend/CHANGELOG.md
index df44bfa651..29771be91c 100644
--- a/plugins/airbrake-backend/CHANGELOG.md
+++ b/plugins/airbrake-backend/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-airbrake-backend
+## 0.2.17-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+
## 0.2.16
### Patch Changes
diff --git a/plugins/airbrake-backend/package.json b/plugins/airbrake-backend/package.json
index a292f05a14..fa3c29c022 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.16",
+ "version": "0.2.17-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 3efe9aabcd..e0efd2802b 100644
--- a/plugins/airbrake/CHANGELOG.md
+++ b/plugins/airbrake/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-airbrake
+## 0.3.17-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/test-utils@1.3.0-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/dev-utils@1.0.14-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.3.16
### Patch Changes
diff --git a/plugins/airbrake/package.json b/plugins/airbrake/package.json
index 6fb594951b..8249410795 100644
--- a/plugins/airbrake/package.json
+++ b/plugins/airbrake/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-airbrake",
- "version": "0.3.16",
+ "version": "0.3.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/airbrake/src/index.ts b/plugins/airbrake/src/index.ts
index 717d544dc9..6f189e36b8 100644
--- a/plugins/airbrake/src/index.ts
+++ b/plugins/airbrake/src/index.ts
@@ -15,7 +15,7 @@
*/
/**
- * The Airbrake plugin provides connectivity between Backstage and Airbrake (https://airbrake.io/).
+ * The Airbrake plugin provides connectivity between Backstage and {@link https://airbrake.io/ | Airbrake}.
*
* @packageDocumentation
*/
diff --git a/plugins/allure/CHANGELOG.md b/plugins/allure/CHANGELOG.md
index 2e4346de7d..27a470ce2c 100644
--- a/plugins/allure/CHANGELOG.md
+++ b/plugins/allure/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-allure
+## 0.1.33-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.1.32
### Patch Changes
diff --git a/plugins/allure/package.json b/plugins/allure/package.json
index d76a4d455b..986fc62f17 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.32",
+ "version": "0.1.33-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,7 +30,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/analytics-module-ga/CHANGELOG.md b/plugins/analytics-module-ga/CHANGELOG.md
index 6a896c5447..b05b09ab78 100644
--- a/plugins/analytics-module-ga/CHANGELOG.md
+++ b/plugins/analytics-module-ga/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-analytics-module-ga
+## 0.1.28-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/theme@0.2.18
+
## 0.1.27
### Patch Changes
diff --git a/plugins/analytics-module-ga/README.md b/plugins/analytics-module-ga/README.md
index d6e2642e2b..6fdfc91306 100644
--- a/plugins/analytics-module-ga/README.md
+++ b/plugins/analytics-module-ga/README.md
@@ -236,7 +236,7 @@ make and test changes is to do the following:
Code for the isolated version of the plugin can be found inside the [/dev](./dev)
directory. Changes to the plugin are hot-reloaded.
-#### Recommended Dev Config
+### Recommended Dev Config
Paste this into your `app-config.local.yaml` while developing this plugin:
diff --git a/plugins/analytics-module-ga/api-report.md b/plugins/analytics-module-ga/api-report.md
index 557f472f71..f28c919f33 100644
--- a/plugins/analytics-module-ga/api-report.md
+++ b/plugins/analytics-module-ga/api-report.md
@@ -25,6 +25,4 @@ export class GoogleAnalytics implements AnalyticsApi {
},
): GoogleAnalytics;
}
-
-// (No @packageDocumentation comment for this package)
```
diff --git a/plugins/analytics-module-ga/package.json b/plugins/analytics-module-ga/package.json
index fa5ee74367..f77807b138 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.27",
+ "version": "0.1.28-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -28,7 +28,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-ga": "^3.3.0",
"react-use": "^17.2.4"
},
diff --git a/plugins/analytics-module-ga/src/index.ts b/plugins/analytics-module-ga/src/index.ts
index fca8d91658..7b96c761fb 100644
--- a/plugins/analytics-module-ga/src/index.ts
+++ b/plugins/analytics-module-ga/src/index.ts
@@ -14,5 +14,11 @@
* limitations under the License.
*/
+/**
+ * Analytics Module plugin for Google Analytics.
+ *
+ * @packageDocumentation
+ */
+
export { analyticsModuleGA } from './plugin';
export * from './apis/implementations/AnalyticsApi';
diff --git a/plugins/apache-airflow/CHANGELOG.md b/plugins/apache-airflow/CHANGELOG.md
index 58348fbc56..6386aee20b 100644
--- a/plugins/apache-airflow/CHANGELOG.md
+++ b/plugins/apache-airflow/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-apache-airflow
+## 0.2.10-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+
## 0.2.9
### Patch Changes
diff --git a/plugins/apache-airflow/package.json b/plugins/apache-airflow/package.json
index 9856b4608a..ab732c8129 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.9",
+ "version": "0.2.10-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -26,7 +26,7 @@
"@backstage/core-plugin-api": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"cross-fetch": "^3.1.5",
"qs": "^6.10.1",
"react-use": "^17.2.4"
diff --git a/plugins/api-docs/CHANGELOG.md b/plugins/api-docs/CHANGELOG.md
index 8735ac91f8..2c850a0cb3 100644
--- a/plugins/api-docs/CHANGELOG.md
+++ b/plugins/api-docs/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-api-docs
+## 0.9.2-next.0
+
+### Patch Changes
+
+- ca50c3bbea1: Corrected styling of nested objects in AsyncAPI to avoid inappropriate uppercase text transformation of nested objects.
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/plugin-catalog@1.10.0-next.0
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.9.1
### Patch Changes
diff --git a/plugins/api-docs/dev/asyncapi-example-api.yaml b/plugins/api-docs/dev/asyncapi-example-api.yaml
index 725811d0cc..3895006979 100644
--- a/plugins/api-docs/dev/asyncapi-example-api.yaml
+++ b/plugins/api-docs/dev/asyncapi-example-api.yaml
@@ -133,6 +133,8 @@ spec:
description: Light intensity measured in lumens.
sentAt:
$ref: "#/components/schemas/sentAt"
+ sentBy:
+ $ref: "#/components/schemas/sentBy"
turnOnOffPayload:
type: object
properties:
@@ -144,6 +146,8 @@ spec:
description: Whether to turn on or off the light.
sentAt:
$ref: "#/components/schemas/sentAt"
+ sentBy:
+ $ref: "#/components/schemas/sentBy"
dimLightPayload:
type: object
properties:
@@ -154,10 +158,25 @@ spec:
maximum: 100
sentAt:
$ref: "#/components/schemas/sentAt"
+ sentBy:
+ $ref: "#/components/schemas/sentBy"
sentAt:
type: string
format: date-time
description: Date and time when the message was sent.
+ sentBy:
+ type: object
+ description: Description of the message sender.
+ properties:
+ name:
+ type: string
+ description: Name of the sender.
+ type:
+ type: string
+ description: Type of the sender.
+ enum:
+ - user
+ - machine
securitySchemes:
apiKey:
diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json
index 3e1fa380c3..8e24c3ccea 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.1",
+ "version": "0.9.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -41,7 +41,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"graphiql": "^1.8.8",
"graphql": "^16.0.0",
"graphql-ws": "^5.4.1",
diff --git a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx
index fd8948828c..64452b7191 100644
--- a/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx
+++ b/plugins/api-docs/src/components/AsyncApiDefinitionWidget/AsyncApiDefinition.tsx
@@ -76,6 +76,8 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({
...theme.typography.button,
borderRadius: theme.shape.borderRadius,
color: theme.palette.primary.main,
+ // override whatever may be in the theme's typography to ensure consistency with asyncapi
+ textTransform: 'inherit',
},
'& a': {
color: theme.palette.link,
diff --git a/plugins/apollo-explorer/CHANGELOG.md b/plugins/apollo-explorer/CHANGELOG.md
index 8771768cc1..6a7ec3d0f2 100644
--- a/plugins/apollo-explorer/CHANGELOG.md
+++ b/plugins/apollo-explorer/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-apollo-explorer
+## 0.1.10-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/theme@0.2.18
+
## 0.1.9
### Patch Changes
diff --git a/plugins/apollo-explorer/api-report.md b/plugins/apollo-explorer/api-report.md
index 1d3b41eac1..66cc473d0b 100644
--- a/plugins/apollo-explorer/api-report.md
+++ b/plugins/apollo-explorer/api-report.md
@@ -40,6 +40,4 @@ export const apolloExplorerPlugin: BackstagePlugin<
{},
{}
>;
-
-// (No @packageDocumentation comment for this package)
```
diff --git a/plugins/apollo-explorer/package.json b/plugins/apollo-explorer/package.json
index 746f29d393..81ac0f02b9 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.9",
+ "version": "0.1.10-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -28,7 +28,6 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "^4.0.0-alpha.57",
"react-use": "^17.2.4",
"use-deep-compare-effect": "^1.8.1"
},
diff --git a/plugins/apollo-explorer/src/index.ts b/plugins/apollo-explorer/src/index.ts
index 6d07535fc3..565557e79f 100644
--- a/plugins/apollo-explorer/src/index.ts
+++ b/plugins/apollo-explorer/src/index.ts
@@ -13,4 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
+
+/**
+ * Integrates Apollo Explorer graphs as a tool to browse GraphQL API endpoints inside Backstage.
+ *
+ * @packageDocumentation
+ */
export { apolloExplorerPlugin, ApolloExplorerPage } from './plugin';
diff --git a/plugins/app-backend/CHANGELOG.md b/plugins/app-backend/CHANGELOG.md
index 6de2196bb6..85873f3ddb 100644
--- a/plugins/app-backend/CHANGELOG.md
+++ b/plugins/app-backend/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-app-backend
+## 0.3.44-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/config-loader@1.1.9
+ - @backstage/types@1.0.2
+
## 0.3.43
### Patch Changes
diff --git a/plugins/app-backend/package.json b/plugins/app-backend/package.json
index 2c24657363..79a9c70ca5 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.43",
+ "version": "0.3.44-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 69fe145e82..3a68d5a495 100644
--- a/plugins/auth-backend/CHANGELOG.md
+++ b/plugins/auth-backend/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-auth-backend
+## 0.18.2-next.0
+
+### Patch Changes
+
+- d8f774c30df: Enforce the secret visibility of certificates and client secrets in the auth backend. Also, document all known options for each auth plugin.
+- 7908d72e033: Introduce a new global config parameter, `auth.enableExperimentalRedirectFlow`. When enabled, auth will happen with an in-window redirect flow rather than through a popup window.
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.13-next.0
+
## 0.18.1
### Patch Changes
diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md
index 63aa060875..7515083c09 100644
--- a/plugins/auth-backend/api-report.md
+++ b/plugins/auth-backend/api-report.md
@@ -398,6 +398,8 @@ export type OAuthState = {
env: string;
origin?: string;
scope?: string;
+ redirectUrl?: string;
+ flow?: string;
};
// @public
diff --git a/plugins/auth-backend/config.d.ts b/plugins/auth-backend/config.d.ts
index 5aff9df90a..8d76ca8bf5 100644
--- a/plugins/auth-backend/config.d.ts
+++ b/plugins/auth-backend/config.d.ts
@@ -62,30 +62,72 @@ export interface Config {
*/
providers?: {
google?: {
- [authEnv: string]: { [key: string]: string };
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ callbackUrl?: string;
+ };
};
github?: {
- [authEnv: string]: { [key: string]: string };
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ callbackUrl?: string;
+ enterpriseInstanceUrl?: string;
+ };
};
gitlab?: {
- [authEnv: string]: { [key: string]: string };
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ audience?: string;
+ callbackUrl?: string;
+ };
};
saml?: {
entryPoint: string;
logoutUrl?: string;
issuer: string;
+ /**
+ * @visibility secret
+ */
cert: string;
audience?: string;
+ /**
+ * @visibility secret
+ */
privateKey?: string;
authnContext?: string[];
identifierFormat?: string;
+ /**
+ * @visibility secret
+ */
decryptionPvk?: string;
signatureAlgorithm?: 'sha256' | 'sha512';
digestAlgorithm?: string;
acceptedClockSkewMs?: number;
};
okta?: {
- [authEnv: string]: { [key: string]: string };
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ audience: string;
+ authServerId?: string;
+ idp?: string;
+ callbackUrl?: string;
+ };
};
oauth2?: {
[authEnv: string]: {
@@ -101,19 +143,56 @@ export interface Config {
};
};
oidc?: {
- [authEnv: string]: { [key: string]: string };
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ callbackUrl?: string;
+ metadataUrl: string;
+ scope?: string;
+ prompt?: string;
+ };
};
auth0?: {
- [authEnv: string]: { [key: string]: string };
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ domain: string;
+ callbackUrl?: string;
+ audience?: string;
+ connection?: string;
+ connectionScope?: string;
+ };
};
microsoft?: {
- [authEnv: string]: { [key: string]: string };
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ tenantId: string;
+ callbackUrl?: string;
+ };
};
onelogin?: {
- [authEnv: string]: { [key: string]: string };
+ [authEnv: string]: {
+ clientId: string;
+ /**
+ * @visibility secret
+ */
+ clientSecret: string;
+ issuer: string;
+ callbackUrl?: string;
+ };
};
awsalb?: {
- issuer?: string;
+ iss?: string;
region: string;
};
cfaccess?: {
diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json
index 3c0fe10a5b..83a9a55906 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.1",
+ "version": "0.18.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts
index e0d4f0e051..1b6653d97e 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts
@@ -83,56 +83,10 @@ describe('OAuthAdapter', () => {
callbackUrl: 'http://domain.org/auth/test-provider/handler/frame',
};
- it('sets the correct headers in start', async () => {
- const oauthProvider = new OAuthAdapter(
- providerInstance,
- oAuthProviderOptions,
- );
- const mockRequest = {
- query: {
- scope: 'user',
- env: 'development',
- },
- } as unknown as express.Request;
+ const defaultState = { nonce: 'nonce', env: 'development' };
- const mockResponse = {
- cookie: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- setHeader: jest.fn().mockReturnThis(),
- statusCode: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
- await oauthProvider.start(mockRequest, mockResponse);
- // nonce cookie checks
- expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
- expect(mockResponse.cookie).toHaveBeenCalledWith(
- `${oAuthProviderOptions.providerId}-nonce`,
- expect.any(String),
- expect.objectContaining({
- httpOnly: true,
- path: '/auth/test-provider/handler',
- maxAge: TEN_MINUTES_MS,
- domain: 'domain.org',
- sameSite: 'lax',
- secure: false,
- }),
- );
- // redirect checks
- expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
- expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
- expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
- expect(mockResponse.statusCode).toEqual(301);
- expect(mockResponse.end).toHaveBeenCalledTimes(1);
- });
-
- it('sets the refresh cookie if refresh is enabled', async () => {
- const oauthProvider = new OAuthAdapter(providerInstance, {
- ...oAuthProviderOptions,
- isOriginAllowed: () => false,
- });
-
- const state = { nonce: 'nonce', env: 'development' };
- const mockRequest = {
+ const createEncodedQueryMockRequest = (state: any) => {
+ return {
cookies: {
'test-provider-nonce': 'nonce',
},
@@ -140,12 +94,69 @@ describe('OAuthAdapter', () => {
state: encodeState(state),
},
} as unknown as express.Request;
+ };
- const mockResponse = {
- cookie: jest.fn().mockReturnThis(),
- setHeader: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
+ const mockResponse = {
+ cookie: jest.fn().mockReturnThis(),
+ end: jest.fn().mockReturnThis(),
+ setHeader: jest.fn().mockReturnThis(),
+ statusCode: jest.fn().mockReturnThis(),
+ redirect: jest.fn().mockReturnThis(),
+ status: jest.fn().mockReturnThis(),
+ json: jest.fn().mockReturnThis(),
+ } as unknown as express.Response;
+
+ const mockStartRequest = {
+ query: {
+ scope: 'user',
+ env: 'development',
+ },
+ } as unknown as express.Request;
+
+ const expectedStartAuthCookieData = {
+ httpOnly: true,
+ path: '/auth/test-provider/handler',
+ maxAge: TEN_MINUTES_MS,
+ domain: 'domain.org',
+ sameSite: 'lax',
+ secure: false,
+ };
+
+ it('sets the correct headers in start', async () => {
+ const oauthProvider = new OAuthAdapter(
+ providerInstance,
+ oAuthProviderOptions,
+ );
+
+ await oauthProvider.start(mockStartRequest, mockResponse);
+ // nonce cookie checks
+ expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
+ expect(mockResponse.cookie).toHaveBeenCalledWith(
+ `${oAuthProviderOptions.providerId}-nonce`,
+ expect.any(String),
+ expect.objectContaining(expectedStartAuthCookieData),
+ );
+ expect(mockResponse.setHeader).toHaveBeenCalledTimes(2);
+ expect(mockResponse.setHeader).toHaveBeenCalledWith('Location', '/url');
+ expect(mockResponse.setHeader).toHaveBeenCalledWith('Content-Length', '0');
+ expect(mockResponse.statusCode).toEqual(301);
+ expect(mockResponse.end).toHaveBeenCalledTimes(1);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
+ });
+
+ const refreshCookieData = {
+ ...expectedStartAuthCookieData,
+ path: '/auth/test-provider',
+ maxAge: THOUSAND_DAYS_MS,
+ };
+
+ it('sets the refresh cookie if refresh is enabled', async () => {
+ const oauthProvider = new OAuthAdapter(providerInstance, {
+ ...oAuthProviderOptions,
+ isOriginAllowed: () => false,
+ });
+
+ const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).toHaveBeenCalledTimes(1);
@@ -153,15 +164,26 @@ describe('OAuthAdapter', () => {
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
- expect.objectContaining({
- httpOnly: true,
- path: '/auth/test-provider',
- maxAge: THOUSAND_DAYS_MS,
- domain: 'domain.org',
- secure: false,
- sameSite: 'lax',
- }),
+ expect.objectContaining(refreshCookieData),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
+ });
+
+ it('sets the refresh cookie if refresh is enabled with redirect', async () => {
+ const oauthProvider = new OAuthAdapter(providerInstance, {
+ ...oAuthProviderOptions,
+ isOriginAllowed: () => false,
+ });
+
+ const state = {
+ ...defaultState,
+ redirectUrl: 'http://localhost:3000',
+ flow: 'redirect',
+ };
+ const mockRequest = createEncodedQueryMockRequest(state);
+
+ await oauthProvider.frameHandler(mockRequest, mockResponse);
+ expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
});
it('persists scope through cookie if enabled', async () => {
@@ -179,32 +201,15 @@ describe('OAuthAdapter', () => {
});
// First we test the /start request, making sure state is set
- const mockStartReq = {
- query: {
- scope: 'user',
- env: 'development',
- },
- } as unknown as express.Request;
- const mockStartRes = {
- cookie: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- setHeader: jest.fn().mockReturnThis(),
- statusCode: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
- await oauthProvider.start(mockStartReq, mockStartRes);
+ await oauthProvider.start(mockStartRequest, mockResponse);
expect(handlers.start).toHaveBeenCalledTimes(1);
expect(handlers.start).toHaveBeenCalledWith({
- query: {
- scope: 'user',
- env: 'development',
- },
+ ...mockStartRequest,
scope: 'user',
state: {
nonce: expect.any(String),
env: 'development',
- origin: undefined,
scope: 'user',
},
});
@@ -223,6 +228,7 @@ describe('OAuthAdapter', () => {
cookie: jest.fn().mockReturnThis(),
setHeader: jest.fn().mockReturnThis(),
end: jest.fn().mockReturnThis(),
+ redirect: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.frameHandler(mockHandleReq, mockHandleRes);
@@ -230,17 +236,11 @@ describe('OAuthAdapter', () => {
expect(mockHandleRes.cookie).toHaveBeenCalledWith(
'test-provider-granted-scope',
'user',
- expect.objectContaining({
- httpOnly: true,
- path: '/auth/test-provider',
- maxAge: THOUSAND_DAYS_MS,
- domain: 'domain.org',
- secure: false,
- sameSite: 'lax',
- }),
+ expect.objectContaining(refreshCookieData),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
- // Them make sure scopes are forwarded correctly during refresh
+ // Then make sure scopes are forwarded correctly during refresh
const mockRefreshReq = {
query: { scope: 'ignore-me' },
cookies: {
@@ -252,6 +252,7 @@ describe('OAuthAdapter', () => {
const mockRefreshRes = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
+ redirect: jest.fn().mockReturnThis(),
} as unknown as express.Response;
await oauthProvider.refresh(mockRefreshReq, mockRefreshRes);
expect(handlers.refresh).toHaveBeenCalledTimes(1);
@@ -261,8 +262,18 @@ describe('OAuthAdapter', () => {
refreshToken: 'refresh-token',
}),
);
+ expect(mockRefreshRes.redirect).not.toHaveBeenCalled();
});
+ const mockRequestWithHeader = {
+ header: () => 'XMLHttpRequest',
+ cookies: {
+ 'test-provider-refresh-token': 'token',
+ },
+ query: {},
+ get: jest.fn(),
+ } as unknown as express.Request;
+
it('removes refresh cookie and calls logout handler when logging out', async () => {
const logoutSpy = jest.spyOn(providerInstance, 'logout');
const oauthProvider = new OAuthAdapter(providerInstance, {
@@ -270,22 +281,8 @@ describe('OAuthAdapter', () => {
isOriginAllowed: () => false,
});
- const mockRequest = {
- cookies: {
- 'test-provider-refresh-token': 'token',
- },
- header: () => 'XMLHttpRequest',
- get: jest.fn(),
- } as unknown as express.Request;
-
- const mockResponse = {
- cookie: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- status: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
- await oauthProvider.logout(mockRequest, mockResponse);
- expect(mockRequest.get).toHaveBeenCalledTimes(1);
+ await oauthProvider.logout(mockRequestWithHeader, mockResponse);
+ expect(mockRequestWithHeader.get).toHaveBeenCalledTimes(1);
expect(logoutSpy).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
@@ -294,6 +291,7 @@ describe('OAuthAdapter', () => {
expect.objectContaining({ path: '/auth/test-provider' }),
);
expect(mockResponse.end).toHaveBeenCalledTimes(1);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('gets new access-token when refreshing', async () => {
@@ -302,20 +300,7 @@ describe('OAuthAdapter', () => {
isOriginAllowed: () => false,
});
- const mockRequest = {
- header: () => 'XMLHttpRequest',
- cookies: {
- 'test-provider-refresh-token': 'token',
- },
- query: {},
- } as unknown as express.Request;
-
- const mockResponse = {
- json: jest.fn().mockReturnThis(),
- status: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
- await oauthProvider.refresh(mockRequest, mockResponse);
+ await oauthProvider.refresh(mockRequestWithHeader, mockResponse);
expect(mockResponse.json).toHaveBeenCalledTimes(1);
expect(mockResponse.json).toHaveBeenCalledWith({
...mockResponseData,
@@ -328,6 +313,7 @@ describe('OAuthAdapter', () => {
},
},
});
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets new access-token when old cookie exists', async () => {
@@ -337,20 +323,12 @@ describe('OAuthAdapter', () => {
});
const mockRequest = {
- header: () => 'XMLHttpRequest',
+ ...mockRequestWithHeader,
cookies: {
'test-provider-refresh-token': 'old-token',
},
- query: {},
- get: jest.fn(),
} as unknown as express.Request;
- const mockResponse = {
- json: jest.fn().mockReturnThis(),
- status: jest.fn().mockReturnThis(),
- cookie: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
await oauthProvider.refresh(mockRequest, mockResponse);
expect(mockRequest.get).toHaveBeenCalledTimes(1);
expect(mockCookieConfigurer).toHaveBeenCalledTimes(1);
@@ -358,15 +336,9 @@ describe('OAuthAdapter', () => {
expect(mockResponse.cookie).toHaveBeenCalledWith(
'test-provider-refresh-token',
'token',
- expect.objectContaining({
- httpOnly: true,
- path: '/auth/test-provider',
- maxAge: THOUSAND_DAYS_MS,
- domain: 'domain.org',
- secure: false,
- sameSite: 'lax',
- }),
+ expect.objectContaining(refreshCookieData),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
});
it('sets the correct nonce cookie configuration', async () => {
@@ -380,162 +352,89 @@ describe('OAuthAdapter', () => {
...oAuthProviderOptions,
});
- const mockRequest = {
- query: {
- scope: 'user',
- env: 'development',
- origin: 'http://domain.org',
- },
- } as unknown as express.Request;
-
- const mockResponse = {
- cookie: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- setHeader: jest.fn().mockReturnThis(),
- statusCode: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
- await oauthProvider.start(mockRequest, mockResponse);
+ await oauthProvider.start(mockStartRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
- expect.objectContaining({
- httpOnly: true,
- domain: 'domain.org',
- maxAge: TEN_MINUTES_MS,
- path: '/auth/test-provider/handler',
- secure: false,
- sameSite: 'lax',
- }),
+ expect.objectContaining(expectedStartAuthCookieData),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
});
- it('sets the correct nonce cookie configuration using origin from request', async () => {
- const config = {
- baseUrl: 'http://domain.org/auth',
- appUrl: 'http://domain.org',
- isOriginAllowed: () => false,
- };
+ const config = {
+ baseUrl: 'http://domain.org/auth',
+ appUrl: 'http://domain.org',
+ isOriginAllowed: () => false,
+ };
+ const mockStartRequestWithOrigin = {
+ query: {
+ scope: 'user',
+ env: 'development',
+ origin: 'http://other.domain',
+ },
+ } as unknown as express.Request;
+
+ it('sets the correct nonce cookie configuration using origin from request', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
- const mockRequest = {
- query: {
- scope: 'user',
- env: 'development',
- origin: 'http://other.domain',
- },
- } as unknown as express.Request;
-
- const mockResponse = {
- cookie: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- setHeader: jest.fn().mockReturnThis(),
- statusCode: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
- await oauthProvider.start(mockRequest, mockResponse);
+ await oauthProvider.start(mockStartRequestWithOrigin, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
`${oAuthProviderOptions.providerId}-nonce`,
expect.any(String),
expect.objectContaining({
- httpOnly: true,
- domain: 'domain.org',
- maxAge: TEN_MINUTES_MS,
- path: '/auth/test-provider/handler',
+ ...expectedStartAuthCookieData,
secure: true,
sameSite: 'none',
}),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
});
- it('sets the correct cookie configuration using an secure callbackUrl', async () => {
- const config = {
- baseUrl: 'https://domain.org/auth',
- appUrl: 'http://domain.org',
- isOriginAllowed: () => false,
- };
+ const secureCookieData = {
+ ...refreshCookieData,
+ secure: true,
+ sameSite: 'lax',
+ maxAge: THOUSAND_DAYS_MS,
+ };
+ it('sets the correct cookie configuration using an secure callbackUrl', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
- const state = {
- nonce: 'nonce',
- env: 'development',
- };
-
- const mockRequest = {
- cookies: {
- 'test-provider-nonce': 'nonce',
- },
- query: {
- state: encodeState(state),
- },
- } as unknown as express.Request;
-
- const mockResponse = {
- cookie: jest.fn().mockReturnThis(),
- setHeader: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
+ const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
- expect.objectContaining({
- httpOnly: true,
- domain: 'domain.org',
- path: '/auth/test-provider',
- secure: true,
- sameSite: 'lax',
- }),
+ expect.objectContaining(secureCookieData),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
});
- it('sets the correct cookie configuration when on different domains and secure', async () => {
- const config = {
- baseUrl: 'https://domain.org/auth',
- appUrl: 'http://domain.org',
- isOriginAllowed: () => false,
- };
+ const secureSameSiteNoneCookieData = {
+ ...secureCookieData,
+ sameSite: 'none',
+ };
+ it('sets the correct cookie configuration when on different domains and secure', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://authdomain.org/auth/test-provider/handler/frame',
});
- const state = {
- nonce: 'nonce',
- env: 'development',
- };
-
- const mockRequest = {
- cookies: {
- 'test-provider-nonce': 'nonce',
- },
- query: {
- state: encodeState(state),
- },
- } as unknown as express.Request;
-
- const mockResponse = {
- cookie: jest.fn().mockReturnThis(),
- setHeader: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
+ const mockRequest = createEncodedQueryMockRequest(defaultState);
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
@@ -543,106 +442,108 @@ describe('OAuthAdapter', () => {
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
expect.objectContaining({
- httpOnly: true,
+ ...secureSameSiteNoneCookieData,
domain: 'authdomain.org',
- path: '/auth/test-provider',
- secure: true,
- sameSite: 'none',
}),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
});
+ const configOriginAllowed = {
+ ...config,
+ isOriginAllowed: () => true,
+ };
+
it('sets the correct cookie configuration using origin from state', async () => {
- const config = {
- baseUrl: 'https://domain.org/auth',
- appUrl: 'http://domain.org',
- isOriginAllowed: () => true,
- };
+ const oauthProvider = OAuthAdapter.fromConfig(
+ configOriginAllowed,
+ providerInstance,
+ {
+ ...oAuthProviderOptions,
+ callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
+ },
+ );
- const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
- ...oAuthProviderOptions,
- callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
- });
-
- const state = {
- nonce: 'nonce',
- env: 'development',
+ const mockRequest = createEncodedQueryMockRequest({
+ ...defaultState,
origin: 'http://other.domain',
- };
-
- const mockRequest = {
- cookies: {
- 'test-provider-nonce': 'nonce',
- },
- query: {
- state: encodeState(state),
- },
- } as unknown as express.Request;
-
- const mockResponse = {
- cookie: jest.fn().mockReturnThis(),
- setHeader: jest.fn().mockReturnThis(),
- end: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
+ });
await oauthProvider.frameHandler(mockRequest, mockResponse);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
expect.stringContaining('test-provider-refresh-token'),
expect.stringContaining('token'),
- expect.objectContaining({
- httpOnly: true,
- domain: 'domain.org',
- path: '/auth/test-provider',
- secure: true,
- sameSite: 'none',
- }),
+ expect.objectContaining(secureSameSiteNoneCookieData),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
});
- it('sets the correct cookie configuration using origin from header', async () => {
- const config = {
- baseUrl: 'https://domain.org/auth',
- appUrl: 'http://domain.org',
- isOriginAllowed: () => false,
- };
+ const mockRequestWithGetMockReturn = {
+ header: () => 'XMLHttpRequest',
+ cookies: {
+ 'test-provider-refresh-token': 'old-token',
+ },
+ query: {},
+ get: jest.fn().mockReturnValue('http://other.domain'),
+ } as unknown as express.Request;
+ it('sets the correct cookie configuration using origin from header', async () => {
const oauthProvider = OAuthAdapter.fromConfig(config, providerInstance, {
...oAuthProviderOptions,
callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
});
- const mockRequest = {
- header: () => 'XMLHttpRequest',
- cookies: {
- 'test-provider-refresh-token': 'old-token',
- },
- query: {},
- get: jest.fn().mockReturnValue('http://other.domain'),
- } as unknown as express.Request;
-
- const mockResponse = {
- json: jest.fn().mockReturnThis(),
- status: jest.fn().mockReturnThis(),
- cookie: jest.fn().mockReturnThis(),
- } as unknown as express.Response;
-
- await oauthProvider.refresh(mockRequest, mockResponse);
- expect(mockRequest.get).toHaveBeenCalledTimes(1);
+ await oauthProvider.refresh(mockRequestWithGetMockReturn, mockResponse);
+ expect(mockRequestWithGetMockReturn.get).toHaveBeenCalledTimes(1);
expect(mockCookieConfigurer).not.toHaveBeenCalled();
expect(mockResponse.cookie).toHaveBeenCalledTimes(1);
expect(mockResponse.cookie).toHaveBeenCalledWith(
'test-provider-refresh-token',
'token',
- expect.objectContaining({
- httpOnly: true,
- path: '/auth/test-provider',
- maxAge: THOUSAND_DAYS_MS,
- domain: 'domain.org',
- secure: true,
- sameSite: 'none',
- }),
+ expect.objectContaining(secureSameSiteNoneCookieData),
);
+ expect(mockResponse.redirect).not.toHaveBeenCalled();
+ });
+
+ it('executed a response redirect when flow query string is set to "redirect"', async () => {
+ const handlers = {
+ start: jest.fn(async (_req: { state: OAuthState }) => ({
+ url: '/url',
+ status: 301,
+ })),
+ handler: jest.fn(async () => ({ response: mockResponseData })),
+ refresh: jest.fn(async () => ({ response: mockResponseData })),
+ };
+ const configWithNoPopupEnabled = {
+ ...configOriginAllowed,
+ };
+ const oauthProvider = OAuthAdapter.fromConfig(
+ configWithNoPopupEnabled,
+ handlers,
+ {
+ ...oAuthProviderOptions,
+ callbackUrl: 'https://domain.org/auth/test-provider/handler/frame',
+ },
+ );
+
+ const state = {
+ ...defaultState,
+ origin: 'http://other.domain',
+ redirectUrl: 'http://domain.org',
+ flow: 'redirect',
+ };
+
+ const mockRequest = {
+ ...createEncodedQueryMockRequest(state),
+ get: jest.fn().mockReturnValue('http://other.domain'),
+ } as unknown as express.Request;
+
+ await oauthProvider.frameHandler(mockRequest, mockResponse);
+ expect(mockRequest.get).not.toHaveBeenCalled();
+ expect(mockCookieConfigurer).not.toHaveBeenCalled();
+ expect(mockResponse.cookie).not.toHaveBeenCalled();
+ expect(mockResponse.redirect).toHaveBeenCalledTimes(1);
+ expect(mockResponse.redirect).toHaveBeenCalledWith('http://domain.org');
});
});
diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
index 85104e9608..5f51b2d301 100644
--- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
+++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts
@@ -33,7 +33,11 @@ import {
NotAllowedError,
} from '@backstage/errors';
import { defaultCookieConfigurer, readState, verifyNonce } from './helpers';
-import { postMessageResponse, ensuresXRequestedWith } from '../flow';
+import {
+ postMessageResponse,
+ ensuresXRequestedWith,
+ WebMessageResponse,
+} from '../flow';
import {
OAuthHandlers,
OAuthStartRequest,
@@ -98,6 +102,8 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
const scope = req.query.scope?.toString() ?? '';
const env = req.query.env?.toString();
const origin = req.query.origin?.toString();
+ const redirectUrl = req.query.redirectUrl?.toString();
+ const flow = req.query.flow?.toString();
if (!env) {
throw new InputError('No env provided in request query parameters');
@@ -109,7 +115,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
// set a nonce cookie before redirecting to oauth provider
this.setNonceCookie(res, nonce, cookieConfig);
- const state: OAuthState = { nonce, env, origin };
+ const state: OAuthState = { nonce, env, origin, redirectUrl, flow };
// If scopes are persisted then we pass them through the state so that we
// can set the cookie on successful auth
@@ -169,11 +175,21 @@ export class OAuthAdapter implements AuthProviderRouteHandlers {
const identity = await this.populateIdentity(response.backstageIdentity);
- // post message back to popup if successful
- return postMessageResponse(res, appOrigin, {
+ const responseObj: WebMessageResponse = {
type: 'authorization_response',
response: { ...response, backstageIdentity: identity },
- });
+ };
+
+ if (state.flow === 'redirect') {
+ if (!state.redirectUrl) {
+ throw new InputError(
+ 'No redirectUrl provided in request query parameters',
+ );
+ }
+ res.redirect(state.redirectUrl);
+ }
+ // post message back to popup if successful
+ return postMessageResponse(res, appOrigin, responseObj);
} catch (error) {
const { name, message } = isError(error)
? error
diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts
index 3902c3f026..e960af2988 100644
--- a/plugins/auth-backend/src/lib/oauth/types.ts
+++ b/plugins/auth-backend/src/lib/oauth/types.ts
@@ -90,6 +90,8 @@ export type OAuthState = {
env: string;
origin?: string;
scope?: string;
+ redirectUrl?: string;
+ flow?: string;
};
/** @public */
diff --git a/plugins/auth-node/CHANGELOG.md b/plugins/auth-node/CHANGELOG.md
index 6c629ad208..210658c09d 100644
--- a/plugins/auth-node/CHANGELOG.md
+++ b/plugins/auth-node/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-auth-node
+## 0.2.13-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 0.2.12
### Patch Changes
diff --git a/plugins/auth-node/package.json b/plugins/auth-node/package.json
index 690f67b868..6e3a74c66d 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.12",
+ "version": "0.2.13-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 a59697c5b0..6aae0be627 100644
--- a/plugins/azure-devops-backend/CHANGELOG.md
+++ b/plugins/azure-devops-backend/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-azure-devops-backend
+## 0.3.23-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-azure-devops-common@0.3.0
+
## 0.3.22
### Patch Changes
diff --git a/plugins/azure-devops-backend/package.json b/plugins/azure-devops-backend/package.json
index 8b2e3caf57..dcafd05680 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.22",
+ "version": "0.3.23-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 9682fe8de8..de9bb877d4 100644
--- a/plugins/azure-devops/CHANGELOG.md
+++ b/plugins/azure-devops/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-azure-devops
+## 0.2.8-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-azure-devops-common@0.3.0
+
## 0.2.7
### Patch Changes
diff --git a/plugins/azure-devops/package.json b/plugins/azure-devops/package.json
index 01e898978f..40104c5036 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.7",
+ "version": "0.2.8-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -37,7 +37,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"humanize-duration": "^3.27.0",
"luxon": "^3.0.0",
"react-use": "^17.2.4"
diff --git a/plugins/azure-sites-backend/CHANGELOG.md b/plugins/azure-sites-backend/CHANGELOG.md
index d09e308269..5fda3502c5 100644
--- a/plugins/azure-sites-backend/CHANGELOG.md
+++ b/plugins/azure-sites-backend/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-azure-sites-backend
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-azure-sites-common@0.1.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/azure-sites-backend/package.json b/plugins/azure-sites-backend/package.json
index 0289ed01bd..77b6b3091c 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.5",
+ "version": "0.1.6-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 99f3c57cda..9c4b55e67f 100644
--- a/plugins/azure-sites/CHANGELOG.md
+++ b/plugins/azure-sites/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-azure-sites
+## 0.1.6-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-azure-sites-common@0.1.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/azure-sites/package.json b/plugins/azure-sites/package.json
index 418d4ff578..6c8acb0fda 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.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -40,7 +40,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"luxon": "^3.0.0",
"react-use": "^17.2.4"
},
diff --git a/plugins/badges-backend/CHANGELOG.md b/plugins/badges-backend/CHANGELOG.md
index 4d3242f09d..88fb467687 100644
--- a/plugins/badges-backend/CHANGELOG.md
+++ b/plugins/badges-backend/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-badges-backend
+## 0.1.38-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+
## 0.1.37
### Patch Changes
diff --git a/plugins/badges-backend/package.json b/plugins/badges-backend/package.json
index 88dac005e0..15caf0ec10 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.37",
+ "version": "0.1.38-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/badges/CHANGELOG.md b/plugins/badges/CHANGELOG.md
index 6f3aaa3897..da109890b7 100644
--- a/plugins/badges/CHANGELOG.md
+++ b/plugins/badges/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-badges
+## 0.2.41-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.2.40
### Patch Changes
diff --git a/plugins/badges/package.json b/plugins/badges/package.json
index 337d36c6cf..51ed87a34a 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.40",
+ "version": "0.2.41-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -37,7 +37,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/bazaar-backend/CHANGELOG.md b/plugins/bazaar-backend/CHANGELOG.md
index 29aaf96358..da7151291a 100644
--- a/plugins/bazaar-backend/CHANGELOG.md
+++ b/plugins/bazaar-backend/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-bazaar-backend
+## 0.2.7-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-auth-node@0.2.13-next.0
+
## 0.2.6
### Patch Changes
diff --git a/plugins/bazaar-backend/package.json b/plugins/bazaar-backend/package.json
index 97c61156d4..78ddc51153 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.6",
+ "version": "0.2.7-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 ff9fe6e97e..139a8af90c 100644
--- a/plugins/bazaar/CHANGELOG.md
+++ b/plugins/bazaar/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/plugin-bazaar
+## 0.2.7-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/plugin-catalog@1.10.0-next.0
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/cli@0.22.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.2.6
### Patch Changes
diff --git a/plugins/bazaar/package.json b/plugins/bazaar/package.json
index e1cf6a81b7..ef455587da 100644
--- a/plugins/bazaar/package.json
+++ b/plugins/bazaar/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-bazaar",
- "version": "0.2.6",
+ "version": "0.2.7-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -34,7 +34,7 @@
"@date-io/luxon": "1.x",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@material-ui/pickers": "^3.3.10",
"@testing-library/jest-dom": "^5.10.1",
"luxon": "^3.0.0",
diff --git a/plugins/bitrise/CHANGELOG.md b/plugins/bitrise/CHANGELOG.md
index cd4b948675..4a92237aaa 100644
--- a/plugins/bitrise/CHANGELOG.md
+++ b/plugins/bitrise/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-bitrise
+## 0.1.44-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.1.43
### Patch Changes
diff --git a/plugins/bitrise/package.json b/plugins/bitrise/package.json
index 17f7faf835..9cdbeda3cd 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.43",
+ "version": "0.1.44-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,7 +30,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"cross-fetch": "^3.1.5",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
diff --git a/plugins/catalog-backend-module-aws/CHANGELOG.md b/plugins/catalog-backend-module-aws/CHANGELOG.md
index 7b556b2d53..1aab4dac82 100644
--- a/plugins/catalog-backend-module-aws/CHANGELOG.md
+++ b/plugins/catalog-backend-module-aws/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/plugin-catalog-backend-module-aws
+## 0.1.18-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/plugin-kubernetes-common@0.6.2-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.1.17
### Patch Changes
diff --git a/plugins/catalog-backend-module-aws/package.json b/plugins/catalog-backend-module-aws/package.json
index c0e2463cfd..0380c59585 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.17",
+ "version": "0.1.18-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend-module-azure/CHANGELOG.md b/plugins/catalog-backend-module-azure/CHANGELOG.md
index 30ccea2776..3a7d09070f 100644
--- a/plugins/catalog-backend-module-azure/CHANGELOG.md
+++ b/plugins/catalog-backend-module-azure/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-catalog-backend-module-azure
+## 0.1.15-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.1.14
### Patch Changes
diff --git a/plugins/catalog-backend-module-azure/package.json b/plugins/catalog-backend-module-azure/package.json
index 4015eb07f2..4ed831fcfd 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.14",
+ "version": "0.1.15-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 90d939638c..4a289a393f 100644
--- a/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md
+++ b/plugins/catalog-backend-module-bitbucket-cloud/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/plugin-catalog-backend-module-bitbucket-cloud
+## 0.1.11-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/plugin-bitbucket-cloud-common@0.2.4
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.1.10
### Patch Changes
diff --git a/plugins/catalog-backend-module-bitbucket-cloud/package.json b/plugins/catalog-backend-module-bitbucket-cloud/package.json
index af3b21d2d0..6e9aed31be 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.10",
+ "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-server/CHANGELOG.md b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md
index 216b7f6a9a..12e3d2e7d8 100644
--- a/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md
+++ b/plugins/catalog-backend-module-bitbucket-server/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-catalog-backend-module-bitbucket-server
+## 0.1.9-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.1.8
### Patch Changes
diff --git a/plugins/catalog-backend-module-bitbucket-server/package.json b/plugins/catalog-backend-module-bitbucket-server/package.json
index 6cf06b432e..9b680a80ca 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.8",
+ "version": "0.1.9-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 936303e285..c10530f41a 100644
--- a/plugins/catalog-backend-module-bitbucket/CHANGELOG.md
+++ b/plugins/catalog-backend-module-bitbucket/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-catalog-backend-module-bitbucket
+## 0.2.11-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-bitbucket-cloud-common@0.2.4
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.2.10
### Patch Changes
diff --git a/plugins/catalog-backend-module-bitbucket/package.json b/plugins/catalog-backend-module-bitbucket/package.json
index 48fad40034..92c0adda4a 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.10",
+ "version": "0.2.11-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 35913b542f..98f0959dfd 100644
--- a/plugins/catalog-backend-module-gerrit/CHANGELOG.md
+++ b/plugins/catalog-backend-module-gerrit/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-catalog-backend-module-gerrit
+## 0.1.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.1.11
### Patch Changes
diff --git a/plugins/catalog-backend-module-gerrit/package.json b/plugins/catalog-backend-module-gerrit/package.json
index 244bcdf1ed..3d66f715cc 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.11",
+ "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-github/CHANGELOG.md b/plugins/catalog-backend-module-github/CHANGELOG.md
index 22d4e10583..44760980cb 100644
--- a/plugins/catalog-backend-module-github/CHANGELOG.md
+++ b/plugins/catalog-backend-module-github/CHANGELOG.md
@@ -1,5 +1,24 @@
# @backstage/plugin-catalog-backend-module-github
+## 0.2.7-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.8.1-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.2.6
### Patch Changes
diff --git a/plugins/catalog-backend-module-github/package.json b/plugins/catalog-backend-module-github/package.json
index 5c9f7f67e1..ba7233e70f 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.6",
+ "version": "0.2.7-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 624ea5c3c9..0345d4ba3e 100644
--- a/plugins/catalog-backend-module-github/src/lib/github.test.ts
+++ b/plugins/catalog-backend-module-github/src/lib/github.test.ts
@@ -16,11 +16,10 @@
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { GroupEntity, UserEntity } from '@backstage/catalog-model';
-import { graphql } from '@octokit/graphql';
+import { graphql as graphqlOctokit } from '@octokit/graphql';
import { graphql as graphqlMsw } from 'msw';
import { setupServer } from 'msw/node';
import { TeamTransformer, UserTransformer } from './defaultTransformers';
-
import {
getOrganizationTeams,
getOrganizationUsers,
@@ -35,11 +34,9 @@ import {
} from './github';
import fetch from 'node-fetch';
-// Workaround for Node.js 18, where native fetch is available, but not yet picked up by msw
-// TODO(Rugvip): remove once https://github.com/mswjs/msw/issues/1388 is resolved
-(global as any).fetch = fetch;
-
describe('github', () => {
+ const graphql = graphqlOctokit.defaults({ request: { fetch } });
+
const server = setupServer();
setupRequestMockHandlers(server);
diff --git a/plugins/catalog-backend-module-gitlab/CHANGELOG.md b/plugins/catalog-backend-module-gitlab/CHANGELOG.md
index 2bde8b2635..02a274469d 100644
--- a/plugins/catalog-backend-module-gitlab/CHANGELOG.md
+++ b/plugins/catalog-backend-module-gitlab/CHANGELOG.md
@@ -1,5 +1,34 @@
# @backstage/plugin-catalog-backend-module-gitlab
+## 0.2.0-next.0
+
+### Minor Changes
+
+- f64345108a0: **BREAKING**: The configuration of the `GitlabDiscoveryEntityProvider` has changed as follows:
+
+ - The configuration key `branch` is now used to define the branch from which the catalog-info should be discovered.
+ - The old configuration key `branch` is now called `fallbackBranch`. This value specifies which branch should be used
+ if no default branch is defined on the project itself.
+
+ To migrate to the new configuration value, rename `branch` to `fallbackBranch`
+
+### Patch Changes
+
+- 7b1b7bfdb7b: The gitlab org data integration now makes use of the GraphQL API to determine
+ the relationships between imported User and Group entities, effectively making
+ this integration usable without an administrator account's Personal Access
+ Token.
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.1.14
### Patch Changes
diff --git a/plugins/catalog-backend-module-gitlab/package.json b/plugins/catalog-backend-module-gitlab/package.json
index b6437cd46b..3bdef6958d 100644
--- a/plugins/catalog-backend-module-gitlab/package.json
+++ b/plugins/catalog-backend-module-gitlab/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-backend-module-gitlab",
"description": "A Backstage catalog backend module that helps integrate towards GitLab",
- "version": "0.1.14",
+ "version": "0.2.0-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.test.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts
index 513a5b937f..122aae7f54 100644
--- a/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts
+++ b/plugins/catalog-backend-module-gitlab/src/lib/client.test.ts
@@ -18,9 +18,10 @@ import { ConfigReader } from '@backstage/config';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { readGitLabIntegrationConfig } from '@backstage/integration';
import { getVoidLogger } from '@backstage/backend-common';
-import { rest } from 'msw';
-import { setupServer, SetupServerApi } from 'msw/node';
+import { graphql, rest } from 'msw';
+import { setupServer, SetupServer } from 'msw/node';
import { GitLabClient, paginated } from './client';
+import { GitLabGroup, GitLabUser } from './types';
const server = setupServer();
setupRequestMockHandlers(server);
@@ -30,12 +31,13 @@ const MOCK_CONFIG = readGitLabIntegrationConfig(
host: 'example.com',
token: 'test-token',
apiBaseUrl: 'https://example.com/api/v4',
+ baseUrl: 'https://example.com',
}),
);
const FAKE_PAGED_ENDPOINT = `/some-endpoint`;
const FAKE_PAGED_URL = `${MOCK_CONFIG.apiBaseUrl}${FAKE_PAGED_ENDPOINT}`;
-function setupFakeFourPageURL(srv: SetupServerApi, url: string) {
+function setupFakeFourPageURL(srv: SetupServer, url: string) {
srv.use(
rest.get(url, (req, res, ctx) => {
const page = req.url.searchParams.get('page');
@@ -55,7 +57,7 @@ function setupFakeFourPageURL(srv: SetupServerApi, url: string) {
}
function setupFakeGroupProjectsEndpoint(
- srv: SetupServerApi,
+ srv: SetupServer,
apiBaseUrl: string,
groupID: string,
) {
@@ -77,7 +79,7 @@ function setupFakeGroupProjectsEndpoint(
}
function setupFakeInstanceProjectsEndpoint(
- srv: SetupServerApi,
+ srv: SetupServer,
apiBaseUrl: string,
) {
srv.use(
@@ -103,7 +105,7 @@ function setupFakeInstanceProjectsEndpoint(
);
}
-function setupFakeHasFileEndpoint(srv: SetupServerApi, apiBaseUrl: string) {
+function setupFakeHasFileEndpoint(srv: SetupServer, apiBaseUrl: string) {
srv.use(
rest.head(
`${apiBaseUrl}/projects/group%2Frepo/repository/files/catalog-info.yaml`,
@@ -259,6 +261,228 @@ describe('GitLabClient', () => {
expect(allProjects).toHaveLength(2);
});
});
+
+ it('listUsers gets all users in the instance', async () => {
+ server.use(
+ rest.get(`${MOCK_CONFIG.apiBaseUrl}/users`, (_, res, ctx) =>
+ res(
+ ctx.set('x-next-page', ''),
+ ctx.json([
+ {
+ id: 1,
+ username: 'test1',
+ name: 'Test Testit',
+ state: 'active',
+ avatar_url: 'https://secure.gravatar.com/',
+ web_url: 'https://gitlab.example/test1',
+ created_at: '2023-01-19T07:27:03.333Z',
+ bio: '',
+ location: null,
+ public_email: null,
+ skype: '',
+ linkedin: '',
+ twitter: '',
+ website_url: '',
+ organization: null,
+ job_title: '',
+ pronouns: null,
+ bot: false,
+ work_information: null,
+ followers: 0,
+ following: 0,
+ is_followed: false,
+ local_time: null,
+ last_sign_in_at: '2023-01-19T07:27:49.601Z',
+ confirmed_at: '2023-01-19T07:27:02.905Z',
+ last_activity_on: '2023-01-19',
+ email: 'test@example.com',
+ theme_id: 1,
+ color_scheme_id: 1,
+ projects_limit: 100000,
+ current_sign_in_at: '2023-01-19T09:09:10.676Z',
+ identities: [],
+ can_create_group: true,
+ can_create_project: true,
+ two_factor_enabled: false,
+ external: false,
+ private_profile: false,
+ commit_email: 'test@example.com',
+ is_admin: false,
+ note: '',
+ },
+ ]),
+ ),
+ ),
+ );
+ const client = new GitLabClient({
+ config: MOCK_CONFIG,
+ logger: getVoidLogger(),
+ });
+
+ const allUsers: GitLabUser[] = [];
+ for await (const user of paginated(
+ options => client.listUsers(options),
+ {},
+ )) {
+ allUsers.push(user);
+ }
+
+ expect(allUsers).toMatchObject([
+ {
+ id: 1,
+ username: 'test1',
+ email: 'test@example.com',
+ name: 'Test Testit',
+ state: 'active',
+ web_url: 'https://gitlab.example/test1',
+ avatar_url: 'https://secure.gravatar.com/',
+ },
+ ]);
+ });
+
+ it('listGroups gets all groups in the instance', async () => {
+ server.use(
+ rest.get(`${MOCK_CONFIG.apiBaseUrl}/groups`, (_, res, ctx) =>
+ res(
+ ctx.set('x-next-page', ''),
+ ctx.json([
+ {
+ id: 1,
+ web_url: 'https://gitlab.example/groups/group1',
+ name: 'group1',
+ path: 'group1',
+ description: '',
+ visibility: 'internal',
+ share_with_group_lock: false,
+ require_two_factor_authentication: false,
+ two_factor_grace_period: 48,
+ project_creation_level: 'developer',
+ auto_devops_enabled: null,
+ subgroup_creation_level: 'owner',
+ emails_disabled: null,
+ mentions_disabled: null,
+ lfs_enabled: true,
+ default_branch_protection: 2,
+ avatar_url: null,
+ request_access_enabled: false,
+ full_name: '8020',
+ full_path: '8020',
+ created_at: '2017-06-19T06:42:34.160Z',
+ parent_id: null,
+ },
+ ]),
+ ),
+ ),
+ );
+ const client = new GitLabClient({
+ config: MOCK_CONFIG,
+ logger: getVoidLogger(),
+ });
+
+ const allGroups: GitLabGroup[] = [];
+ for await (const group of paginated(
+ options => client.listGroups(options),
+ {},
+ )) {
+ allGroups.push(group);
+ }
+
+ expect(allGroups).toMatchObject([
+ {
+ id: 1,
+ name: 'group1',
+ full_path: '8020',
+ description: '',
+ parent_id: null,
+ },
+ ]);
+ });
+
+ describe('getGroupMembers', () => {
+ it('gets member IDs', async () => {
+ server.use(
+ graphql
+ .link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
+ .operation((_, res, ctx) =>
+ res(
+ ctx.data({
+ group: {
+ groupMembers: {
+ nodes: [{ user: { id: 'gid://gitlab/User/1' } }],
+ pageInfo: {
+ endCursor: 'end',
+ hasNextPage: false,
+ },
+ },
+ },
+ }),
+ ),
+ ),
+ );
+ const client = new GitLabClient({
+ config: MOCK_CONFIG,
+ logger: getVoidLogger(),
+ });
+
+ const members = await client.getGroupMembers('group1');
+
+ expect(members).toEqual([1]);
+ });
+
+ it('rejects when GraphQL returns errors', async () => {
+ server.use(
+ graphql
+ .link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
+ .operation((_, res, ctx) =>
+ res(
+ ctx.errors([
+ { message: 'Unexpected end of document', locations: [] },
+ ]),
+ ),
+ ),
+ );
+ const client = new GitLabClient({
+ config: MOCK_CONFIG,
+ logger: getVoidLogger(),
+ });
+
+ await expect(() => client.getGroupMembers('group1')).rejects.toThrow(
+ 'GraphQL errors: [{"message":"Unexpected end of document","locations":[]}]',
+ );
+ });
+
+ it('traverses multi-page results', async () => {
+ server.use(
+ graphql
+ .link(`${MOCK_CONFIG.baseUrl}/api/graphql`)
+ .operation((req, res, ctx) =>
+ res(
+ ctx.data({
+ group: {
+ groupMembers: {
+ nodes: req.variables.endCursor
+ ? [{ user: { id: 'gid://gitlab/User/2' } }]
+ : [{ user: { id: 'gid://gitlab/User/1' } }],
+ pageInfo: {
+ endCursor: req.variables.endCursor ? 'end' : 'next',
+ hasNextPage: !req.variables.endCursor,
+ },
+ },
+ },
+ }),
+ ),
+ ),
+ );
+ const client = new GitLabClient({
+ config: MOCK_CONFIG,
+ logger: getVoidLogger(),
+ });
+
+ const members = await client.getGroupMembers('group1');
+
+ expect(members).toEqual([1, 2]);
+ });
+ });
});
describe('paginated', () => {
diff --git a/plugins/catalog-backend-module-gitlab/src/lib/client.ts b/plugins/catalog-backend-module-gitlab/src/lib/client.ts
index bad0eb2a86..8f8c67895c 100644
--- a/plugins/catalog-backend-module-gitlab/src/lib/client.ts
+++ b/plugins/catalog-backend-module-gitlab/src/lib/client.ts
@@ -20,7 +20,7 @@ import {
GitLabIntegrationConfig,
} from '@backstage/integration';
import { Logger } from 'winston';
-import { GitLabGroup, GitLabMembership, GitLabUser } from './types';
+import { GitLabGroup, GitLabGroupMembersResponse, GitLabUser } from './types';
export type CommonListOptions = {
[key: string]: string | number | boolean | undefined;
@@ -78,16 +78,11 @@ export class GitLabClient {
async listUsers(
options?: UserListOptions,
): Promise> {
- let requestOptions = options;
-
- if (!requestOptions) {
- requestOptions = {};
- }
-
- requestOptions.without_project_bots = true;
- requestOptions.exclude_internal = true;
-
- return this.pagedRequest(`/users?`, requestOptions);
+ return this.pagedRequest(`/users?`, {
+ ...options,
+ without_project_bots: true,
+ exclude_internal: true,
+ });
}
async listGroups(
@@ -96,30 +91,51 @@ export class GitLabClient {
return this.pagedRequest(`/groups`, options);
}
- async getUserMemberships(userId: number): Promise {
- const endpoint: string = `/users/${encodeURIComponent(userId)}/memberships`;
- const request = new URL(`${this.config.apiBaseUrl}${endpoint}`);
- request.searchParams.append('per_page', '100');
-
- const response = await fetch(request.toString(), {
- headers: getGitLabRequestOptions(this.config).headers,
- method: 'GET',
- });
-
- if (!response.ok) {
- if (response.status >= 500) {
- this.logger.debug(
- `Unexpected response when fetching ${request.toString()}. Expected 200 but got ${
- response.status
- } - ${response.statusText}`,
- );
+ async getGroupMembers(groupPath: string): Promise {
+ const memberIds = [];
+ let hasNextPage: boolean = false;
+ let endCursor: string | null = null;
+ do {
+ const response: GitLabGroupMembersResponse = await fetch(
+ `${this.config.baseUrl}/api/graphql`,
+ {
+ method: 'POST',
+ headers: {
+ ...getGitLabRequestOptions(this.config).headers,
+ ['Content-Type']: 'application/json',
+ },
+ body: JSON.stringify({
+ variables: { group: groupPath, endCursor },
+ query: `query($group: ID!, $endCursor: String) {
+ group(fullPath: $group) {
+ groupMembers(first: 100, relations: [DIRECT], after: $endCursor) {
+ nodes {
+ user {
+ id
+ }
+ }
+ pageInfo {
+ endCursor
+ hasNextPage
+ }
+ }
+ }
+ }`,
+ }),
+ },
+ ).then(r => r.json());
+ if (response.errors) {
+ throw new Error(`GraphQL errors: ${JSON.stringify(response.errors)}`);
}
- return [];
- }
-
- return response.json().then(items => {
- return items as GitLabMembership[];
- });
+ memberIds.push(
+ ...response.data.group.groupMembers.nodes.map(
+ (node: { user: { id: string } }) =>
+ Number(node.user.id.replace(/^gid:\/\/gitlab\/User\//, '')),
+ ),
+ );
+ ({ hasNextPage, endCursor } = response.data.group.groupMembers.pageInfo);
+ } while (hasNextPage);
+ return memberIds;
}
/**
diff --git a/plugins/catalog-backend-module-gitlab/src/lib/types.ts b/plugins/catalog-backend-module-gitlab/src/lib/types.ts
index c3fc8a4ede..c27007861b 100644
--- a/plugins/catalog-backend-module-gitlab/src/lib/types.ts
+++ b/plugins/catalog-backend-module-gitlab/src/lib/types.ts
@@ -50,11 +50,19 @@ export type GitLabGroup = {
parent_id?: number;
};
-export type GitLabMembership = {
- source_id: number;
- source_name: string;
- source_type: string;
- access_level: number;
+export type GitLabGroupMembersResponse = {
+ errors: { message: string }[];
+ data: {
+ group: {
+ groupMembers: {
+ nodes: { user: { id: string } }[];
+ pageInfo: {
+ endCursor: string;
+ hasNextPage: boolean;
+ };
+ };
+ };
+ };
};
export type GitlabProviderConfig = {
@@ -62,11 +70,12 @@ export type GitlabProviderConfig = {
group: string;
id: string;
/**
- * @deprecated use `fallbackBranch` instead
+ * The name of the branch to be used, to discover catalog files.
*/
branch?: string;
/**
- * If there is no default branch defined at the project, this fallback is used to discover catalog files.
+ * If no `branch` is configured and there is no default branch defined at the project as well, this fallback is used
+ * to discover catalog files.
* Defaults to: `master`
*/
fallbackBranch: string;
diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts
index 42fa1b471c..75a4c6414c 100644
--- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts
+++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.test.ts
@@ -449,4 +449,111 @@ describe('GitlabDiscoveryEntityProvider', () => {
'GitlabDiscoveryEntityProvider:test-id',
);
});
+
+ it('should filter found projects based on the branch', async () => {
+ const config = new ConfigReader({
+ integrations: {
+ gitlab: [
+ {
+ host: 'test-gitlab',
+ apiBaseUrl: 'https://api.gitlab.example/api/v4',
+ token: '1234',
+ },
+ ],
+ },
+ catalog: {
+ providers: {
+ gitlab: {
+ 'test-id': {
+ host: 'test-gitlab',
+ branch: 'test',
+ },
+ },
+ },
+ },
+ });
+ const schedule = new PersistingTaskRunner();
+ const entityProviderConnection: EntityProviderConnection = {
+ applyMutation: jest.fn(),
+ refresh: jest.fn(),
+ };
+ const provider = GitlabDiscoveryEntityProvider.fromConfig(config, {
+ logger,
+ schedule,
+ })[0];
+
+ server.use(
+ rest.get(
+ `https://api.gitlab.example/api/v4/projects`,
+ (_req, res, ctx) => {
+ const response = [
+ {
+ id: 123,
+ default_branch: 'master',
+ archived: false,
+ last_activity_at: new Date().toString(),
+ web_url: 'https://api.gitlab.example/test-group/test-repo',
+ path_with_namespace: 'test-group/test-repo',
+ },
+ {
+ id: 124,
+ default_branch: 'master',
+ archived: false,
+ last_activity_at: new Date().toString(),
+ web_url: 'https://api.gitlab.example/john/example',
+ path_with_namespace: 'john/example',
+ },
+ ];
+ return res(ctx.json(response));
+ },
+ ),
+ rest.head(
+ 'https://api.gitlab.example/api/v4/projects/test-group%2Ftest-repo/repository/files/catalog-info.yaml',
+ (_, res, ctx) => {
+ return res(ctx.status(404, 'Not Found'));
+ },
+ ),
+ rest.head(
+ 'https://api.gitlab.example/api/v4/projects/john%2Fexample/repository/files/catalog-info.yaml',
+ (req, res, ctx) => {
+ if (req.url.searchParams.get('ref') === 'test') {
+ return res(ctx.status(200));
+ }
+ return res(ctx.status(404, 'Not Found'));
+ },
+ ),
+ );
+
+ await provider.connect(entityProviderConnection);
+
+ await provider.refresh(logger);
+
+ expect(entityProviderConnection.applyMutation).toHaveBeenCalledWith({
+ type: 'full',
+ entities: [
+ {
+ entity: {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Location',
+ metadata: {
+ annotations: {
+ 'backstage.io/managed-by-location':
+ 'url:https://api.gitlab.example/john/example/-/blob/test/catalog-info.yaml',
+ 'backstage.io/managed-by-origin-location':
+ 'url:https://api.gitlab.example/john/example/-/blob/test/catalog-info.yaml',
+ },
+ name: 'generated-232185d858fee049986d202c10316d634e76a3d1',
+ },
+ spec: {
+ presence: 'optional',
+ target:
+ 'https://api.gitlab.example/john/example/-/blob/test/catalog-info.yaml',
+ type: 'url',
+ },
+ },
+ locationKey: 'GitlabDiscoveryEntityProvider:test-id',
+ },
+ ],
+ });
+ });
});
diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts
index ece10ee8c6..6001a32a4a 100644
--- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts
+++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabDiscoveryEntityProvider.ts
@@ -61,10 +61,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider {
throw new Error('Either schedule or scheduler must be provided.');
}
- const providerConfigs = readGitlabConfigs(
- config,
- options.logger.child({ target: 'GitlabDiscoveryEntityProvider' }),
- );
+ const providerConfigs = readGitlabConfigs(config);
const integrations = ScmIntegrations.fromConfig(config).gitlab;
const providers: GitlabDiscoveryEntityProvider[] = [];
@@ -181,6 +178,7 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider {
}
if (
+ !this.config.branch &&
this.config.fallbackBranch === '*' &&
project.default_branch === undefined
) {
@@ -188,7 +186,9 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider {
}
const project_branch =
- project.default_branch ?? this.config.fallbackBranch;
+ this.config.branch ??
+ project.default_branch ??
+ this.config.fallbackBranch;
const projectHasFile: boolean = await client.hasFile(
project.path_with_namespace ?? '',
@@ -211,7 +211,10 @@ export class GitlabDiscoveryEntityProvider implements EntityProvider {
}
private createLocationSpec(project: GitLabProject): LocationSpec {
- const project_branch = project.default_branch ?? this.config.fallbackBranch;
+ const project_branch =
+ this.config.branch ??
+ project.default_branch ??
+ this.config.fallbackBranch;
return {
type: 'url',
target: `${project.web_url}/-/blob/${project_branch}/${this.config.catalogFile}`,
diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts
index 5a323a8dec..3d6ff0a91a 100644
--- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts
+++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.test.ts
@@ -23,7 +23,7 @@ import {
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { ConfigReader } from '@backstage/config';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
-import { rest } from 'msw';
+import { graphql, rest } from 'msw';
import { setupServer } from 'msw/node';
import { GitlabOrgDiscoveryEntityProvider } from './GitlabOrgDiscoveryEntityProvider';
@@ -318,20 +318,26 @@ describe('GitlabOrgDiscoveryEntityProvider', () => {
];
return res(ctx.json(response));
}),
- rest.get(
- `https://api.gitlab.example/api/v4/users/1/memberships`,
- (_req, res, ctx) => {
- const response = [
- {
- source_id: 2,
- source_name: 'Group 2',
- source_type: 'Namespace',
- access_level: 50,
- },
- ];
- return res(ctx.json(response));
- },
- ),
+ graphql
+ .link('https://test-gitlab/api/graphql')
+ .operation(async (req, res, ctx) =>
+ res(
+ ctx.data({
+ group: {
+ groupMembers: {
+ nodes:
+ req.variables.group === 'group1/group2'
+ ? [{ user: { id: 'gid://gitlab/User/1' } }]
+ : [],
+ pageInfo: {
+ endCursor: 'end',
+ hasNextPage: false,
+ },
+ },
+ },
+ }),
+ ),
+ ),
);
await provider.connect(entityProviderConnection);
diff --git a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts
index acfa784c2e..1fe607afbb 100644
--- a/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts
+++ b/plugins/catalog-backend-module-gitlab/src/providers/GitlabOrgDiscoveryEntityProvider.ts
@@ -72,10 +72,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
throw new Error('Either schedule or scheduler must be provided.');
}
- const providerConfigs = readGitlabConfigs(
- config,
- options.logger.child({ target: 'GitlabOrgDiscoveryEntityProvider' }),
- );
+ const providerConfigs = readGitlabConfigs(config);
const integrations = ScmIntegrations.fromConfig(config).gitlab;
const providers: GitlabOrgDiscoveryEntityProvider[] = [];
@@ -185,7 +182,7 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
},
);
- const idMappedGroup: { [groupId: number]: GitLabGroup } = {};
+ const idMappedUser: { [userId: number]: GitLabUser } = {};
const res: Result = {
scanned: 0,
@@ -197,6 +194,21 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
matches: [],
};
+ for await (const user of users) {
+ if (!this.config.userPattern.test(user.email ?? user.username ?? '')) {
+ continue;
+ }
+
+ res.scanned++;
+
+ if (user.state !== 'active') {
+ continue;
+ }
+
+ idMappedUser[user.id] = user;
+ res.matches.push(user);
+ }
+
for await (const group of groups) {
if (!this.config.groupPattern.test(group.full_path ?? '')) {
continue;
@@ -212,35 +224,12 @@ export class GitlabOrgDiscoveryEntityProvider implements EntityProvider {
groupRes.scanned++;
groupRes.matches.push(group);
- idMappedGroup[group.id] = group;
- }
-
- for await (const user of users) {
- if (!this.config.userPattern.test(user.email ?? user.username ?? '')) {
- continue;
- }
-
- res.scanned++;
-
- if (user.state !== 'active') {
- continue;
- }
-
- const memberships = await client.getUserMemberships(user.id);
- const userGroups: GitLabGroup[] = [];
-
- for (const i of memberships) {
- if (
- i.source_type === 'Namespace' &&
- idMappedGroup.hasOwnProperty(i.source_id)
- ) {
- userGroups.push(idMappedGroup[i.source_id]);
+ for (const id of await client.getGroupMembers(group.full_path)) {
+ const user = idMappedUser[id];
+ if (user) {
+ user.groups = (user.groups ?? []).concat(group);
}
}
-
- user.groups = userGroups;
-
- res.matches.push(user);
}
const groupsWithUsers = groupRes.matches.filter(group => {
diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts
index c9b35e78c2..1035538939 100644
--- a/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts
+++ b/plugins/catalog-backend-module-gitlab/src/providers/config.test.ts
@@ -17,9 +17,6 @@
import { ConfigReader } from '@backstage/config';
import { Duration } from 'luxon';
import { readGitlabConfigs } from './config';
-import { getVoidLogger } from '@backstage/backend-common';
-
-const logger = getVoidLogger();
describe('config', () => {
it('empty gitlab config', () => {
@@ -29,7 +26,7 @@ describe('config', () => {
},
});
- const result = readGitlabConfigs(config, logger);
+ const result = readGitlabConfigs(config);
expect(result).toHaveLength(0);
});
@@ -47,12 +44,13 @@ describe('config', () => {
},
});
- const result = readGitlabConfigs(config, logger);
+ const result = readGitlabConfigs(config);
expect(result).toHaveLength(1);
result.forEach(r =>
expect(r).toStrictEqual({
id: 'test',
group: 'group',
+ branch: undefined,
fallbackBranch: 'master',
host: 'host',
catalogFile: 'catalog-info.yaml',
@@ -74,6 +72,7 @@ describe('config', () => {
group: 'group',
host: 'host',
branch: 'not-master',
+ fallbackBranch: 'main',
entityFilename: 'custom-file.yaml',
},
},
@@ -81,13 +80,14 @@ describe('config', () => {
},
});
- const result = readGitlabConfigs(config, logger);
+ const result = readGitlabConfigs(config);
expect(result).toHaveLength(1);
result.forEach(r =>
expect(r).toStrictEqual({
id: 'test',
group: 'group',
- fallbackBranch: 'not-master',
+ branch: 'not-master',
+ fallbackBranch: 'main',
host: 'host',
catalogFile: 'custom-file.yaml',
projectPattern: /[\s\S]*/,
@@ -119,12 +119,13 @@ describe('config', () => {
},
});
- const result = readGitlabConfigs(config, logger);
+ const result = readGitlabConfigs(config);
expect(result).toHaveLength(1);
result.forEach(r =>
expect(r).toStrictEqual({
id: 'test',
group: 'group',
+ branch: undefined,
fallbackBranch: 'master',
host: 'host',
catalogFile: 'catalog-info.yaml',
@@ -158,7 +159,7 @@ describe('config', () => {
},
});
- expect(() => readGitlabConfigs(config, logger)).toThrow(
+ expect(() => readGitlabConfigs(config)).toThrow(
"Missing required config value at 'catalog.providers.gitlab.test.host'",
);
});
@@ -177,7 +178,7 @@ describe('config', () => {
},
});
- const result = readGitlabConfigs(config, logger);
+ const result = readGitlabConfigs(config);
expect(result).toHaveLength(1);
expect(result[0].group).toEqual('');
});
diff --git a/plugins/catalog-backend-module-gitlab/src/providers/config.ts b/plugins/catalog-backend-module-gitlab/src/providers/config.ts
index e5cf5a6298..cd15c4825c 100644
--- a/plugins/catalog-backend-module-gitlab/src/providers/config.ts
+++ b/plugins/catalog-backend-module-gitlab/src/providers/config.ts
@@ -17,7 +17,6 @@
import { readTaskScheduleDefinitionFromConfig } from '@backstage/backend-tasks';
import { Config } from '@backstage/config';
import { GitlabProviderConfig } from '../lib';
-import { Logger } from 'winston';
/**
* Extracts the gitlab config from a config object
@@ -26,25 +25,12 @@ import { Logger } from 'winston';
*
* @param id - The provider key
* @param config - The config object to extract from
- * @param logger - The logger
*/
-function readGitlabConfig(
- id: string,
- config: Config,
- logger: Logger,
-): GitlabProviderConfig {
+function readGitlabConfig(id: string, config: Config): GitlabProviderConfig {
const group = config.getOptionalString('group') ?? '';
const host = config.getString('host');
const branch = config.getOptionalString('branch');
-
- if (branch) {
- logger.warn(
- 'The configuration key `branch` has been deprecated in favor of the configuration key `fallbackBranch`.',
- );
- }
-
- const fallbackBranch =
- config.getOptionalString('fallbackBranch') ?? branch ?? 'master';
+ const fallbackBranch = config.getOptionalString('fallbackBranch') ?? 'master';
const catalogFile =
config.getOptionalString('entityFilename') ?? 'catalog-info.yaml';
const projectPattern = new RegExp(
@@ -65,6 +51,7 @@ function readGitlabConfig(
return {
id,
group,
+ branch,
fallbackBranch,
host,
catalogFile,
@@ -82,12 +69,8 @@ function readGitlabConfig(
* @public
*
* @param config - The config object to extract from
- * @param logger - The logger
*/
-export function readGitlabConfigs(
- config: Config,
- logger: Logger,
-): GitlabProviderConfig[] {
+export function readGitlabConfigs(config: Config): GitlabProviderConfig[] {
const configs: GitlabProviderConfig[] = [];
const providerConfigs = config.getOptionalConfig('catalog.providers.gitlab');
@@ -97,13 +80,7 @@ export function readGitlabConfigs(
}
for (const id of providerConfigs.keys()) {
- configs.push(
- readGitlabConfig(
- id,
- providerConfigs.getConfig(id),
- logger.child({ target: id }),
- ),
- );
+ configs.push(readGitlabConfig(id, providerConfigs.getConfig(id)));
}
return configs;
diff --git a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
index d411c63ed2..5bf522c41b 100644
--- a/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
+++ b/plugins/catalog-backend-module-incremental-ingestion/CHANGELOG.md
@@ -1,5 +1,21 @@
# @backstage/plugin-catalog-backend-module-incremental-ingestion
+## 0.3.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.8.1-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+ - @backstage/plugin-permission-common@0.7.4
+
## 0.3.0
### Minor Changes
diff --git a/plugins/catalog-backend-module-incremental-ingestion/package.json b/plugins/catalog-backend-module-incremental-ingestion/package.json
index df86610dce..66e939dbe3 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.0",
+ "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-ldap/CHANGELOG.md b/plugins/catalog-backend-module-ldap/CHANGELOG.md
index 99503f83bf..a89c835ef2 100644
--- a/plugins/catalog-backend-module-ldap/CHANGELOG.md
+++ b/plugins/catalog-backend-module-ldap/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-catalog-backend-module-ldap
+## 0.5.11-next.0
+
+### Patch Changes
+
+- c9a0fdcd2c8: Fix deprecated types.
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.5.10
### Patch Changes
diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md
index 088e34fc34..997ad55560 100644
--- a/plugins/catalog-backend-module-ldap/api-report.md
+++ b/plugins/catalog-backend-module-ldap/api-report.md
@@ -11,7 +11,7 @@ import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { GroupEntity } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/types';
-import { LocationSpec } from '@backstage/plugin-catalog-node';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
import { Logger } from 'winston';
import { SearchEntry } from 'ldapjs';
import { SearchOptions } from 'ldapjs';
diff --git a/plugins/catalog-backend-module-ldap/package.json b/plugins/catalog-backend-module-ldap/package.json
index d483baa914..6986f7a84a 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.10",
+ "version": "0.5.11-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -36,6 +36,7 @@
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
+ "@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/types": "workspace:^",
"@types/ldapjs": "^2.2.0",
diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts
index 9c5442ca0c..00578ac3a4 100644
--- a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts
+++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgReaderProcessor.ts
@@ -27,9 +27,9 @@ import {
import {
CatalogProcessor,
CatalogProcessorEmit,
- LocationSpec,
processingResult,
} from '@backstage/plugin-catalog-node';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
/**
* Extracts teams and users out of an LDAP server.
diff --git a/plugins/catalog-backend-module-msgraph/CHANGELOG.md b/plugins/catalog-backend-module-msgraph/CHANGELOG.md
index 37393dfd7b..9eed88aa13 100644
--- a/plugins/catalog-backend-module-msgraph/CHANGELOG.md
+++ b/plugins/catalog-backend-module-msgraph/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-catalog-backend-module-msgraph
+## 0.5.3-next.0
+
+### Patch Changes
+
+- c9a0fdcd2c8: Fix deprecated types.
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.5.2
### Patch Changes
diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md
index bde8c06fbb..3e4d8d99bc 100644
--- a/plugins/catalog-backend-module-msgraph/api-report.md
+++ b/plugins/catalog-backend-module-msgraph/api-report.md
@@ -9,7 +9,7 @@ import { Config } from '@backstage/config';
import { EntityProvider } from '@backstage/plugin-catalog-node';
import { EntityProviderConnection } from '@backstage/plugin-catalog-node';
import { GroupEntity } from '@backstage/catalog-model';
-import { LocationSpec } from '@backstage/plugin-catalog-node';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
import { Logger } from 'winston';
import * as MicrosoftGraph from '@microsoft/microsoft-graph-types';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
diff --git a/plugins/catalog-backend-module-msgraph/package.json b/plugins/catalog-backend-module-msgraph/package.json
index 3201e0b8d4..15f272d48f 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.2",
+ "version": "0.5.3-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -51,6 +51,7 @@
"@backstage/backend-tasks": "workspace:^",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
+ "@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@microsoft/microsoft-graph-types": "^2.6.0",
"@types/node-fetch": "^2.5.12",
diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts
index 2beef51c31..d9119a3c4d 100644
--- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts
+++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts
@@ -18,9 +18,9 @@ import { Config } from '@backstage/config';
import {
CatalogProcessor,
CatalogProcessorEmit,
- LocationSpec,
processingResult,
} from '@backstage/plugin-catalog-node';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
import { Logger } from 'winston';
import {
GroupTransformer,
diff --git a/plugins/catalog-backend-module-openapi/CHANGELOG.md b/plugins/catalog-backend-module-openapi/CHANGELOG.md
index 414f7960cd..680e86263f 100644
--- a/plugins/catalog-backend-module-openapi/CHANGELOG.md
+++ b/plugins/catalog-backend-module-openapi/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-catalog-backend-module-openapi
+## 0.1.10-next.0
+
+### Patch Changes
+
+- c9a0fdcd2c8: Fix deprecated types.
+- Updated dependencies
+ - @backstage/plugin-catalog-backend@1.8.1-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-model@1.2.1
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.1.9
### Patch Changes
diff --git a/plugins/catalog-backend-module-openapi/api-report.md b/plugins/catalog-backend-module-openapi/api-report.md
index c2af9cbe01..455816645a 100644
--- a/plugins/catalog-backend-module-openapi/api-report.md
+++ b/plugins/catalog-backend-module-openapi/api-report.md
@@ -3,11 +3,11 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
-import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
+import { CatalogProcessor } from '@backstage/plugin-catalog-node';
import { Config } from '@backstage/config';
import { Entity } from '@backstage/catalog-model';
import { JsonValue } from '@backstage/types';
-import { LocationSpec } from '@backstage/plugin-catalog-backend';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
import { Logger } from 'winston';
import { PlaceholderResolverParams } from '@backstage/plugin-catalog-backend';
import { ScmIntegrations } from '@backstage/integration';
diff --git a/plugins/catalog-backend-module-openapi/package.json b/plugins/catalog-backend-module-openapi/package.json
index 6bb9d66ec5..fd4a8fef4d 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.9",
+ "version": "0.1.10-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -38,6 +38,7 @@
"@backstage/config": "workspace:^",
"@backstage/integration": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
+ "@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
"@backstage/types": "workspace:^",
"winston": "^3.2.1",
diff --git a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts
index 9dcadad61d..6b00519b2f 100644
--- a/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts
+++ b/plugins/catalog-backend-module-openapi/src/OpenApiRefProcessor.ts
@@ -17,10 +17,8 @@ import { UrlReader } from '@backstage/backend-common';
import { Entity } from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
-import {
- CatalogProcessor,
- LocationSpec,
-} from '@backstage/plugin-catalog-backend';
+import { CatalogProcessor } from '@backstage/plugin-catalog-node';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
import { bundleFileWithRefs } from './lib';
import { Logger } from 'winston';
diff --git a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
index d3ccdb6782..50c805985e 100644
--- a/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
+++ b/plugins/catalog-backend-module-puppetdb/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-catalog-backend-module-puppetdb
+## 0.1.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 0.1.0
### Minor Changes
diff --git a/plugins/catalog-backend-module-puppetdb/package.json b/plugins/catalog-backend-module-puppetdb/package.json
index 3334e3d204..f3df2dfdc8 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.0",
+ "version": "0.1.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/catalog-backend/CHANGELOG.md b/plugins/catalog-backend/CHANGELOG.md
index 0a3c5853e0..e450baf77b 100644
--- a/plugins/catalog-backend/CHANGELOG.md
+++ b/plugins/catalog-backend/CHANGELOG.md
@@ -1,5 +1,27 @@
# @backstage/plugin-catalog-backend
+## 1.8.1-next.0
+
+### Patch Changes
+
+- c9a0fdcd2c8: Fix deprecated types.
+- 899ebfd8e02: Add full text search support to the `by-query` endpoint.
+- Updated dependencies
+ - @backstage/plugin-scaffolder-common@1.2.7-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+ - @backstage/plugin-permission-common@0.7.4
+ - @backstage/plugin-permission-node@0.7.7-next.0
+ - @backstage/plugin-search-common@1.2.2
+
## 1.8.0
### Minor Changes
diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md
index 5e7b2dee02..d491f391d5 100644
--- a/plugins/catalog-backend/api-report.md
+++ b/plugins/catalog-backend/api-report.md
@@ -11,6 +11,7 @@ import { AnalyzeLocationGenerateEntity as AnalyzeLocationGenerateEntity_2 } from
import { AnalyzeLocationRequest as AnalyzeLocationRequest_2 } from '@backstage/plugin-catalog-common';
import { AnalyzeLocationResponse as AnalyzeLocationResponse_2 } from '@backstage/plugin-catalog-common';
import { CatalogApi } from '@backstage/catalog-client';
+import type { CatalogCollatorEntityTransformer as CatalogCollatorEntityTransformer_2 } from '@backstage/plugin-search-backend-module-catalog';
import { CatalogEntityDocument } from '@backstage/plugin-catalog-common';
import { CatalogProcessor as CatalogProcessor_2 } from '@backstage/plugin-catalog-node';
import { CatalogProcessorCache as CatalogProcessorCache_2 } from '@backstage/plugin-catalog-node';
@@ -23,8 +24,9 @@ import { CatalogProcessorRefreshKeysResult as CatalogProcessorRefreshKeysResult_
import { CatalogProcessorRelationResult as CatalogProcessorRelationResult_2 } from '@backstage/plugin-catalog-node';
import { CatalogProcessorResult as CatalogProcessorResult_2 } from '@backstage/plugin-catalog-node';
import { Config } from '@backstage/config';
+import { DefaultCatalogCollatorFactory as DefaultCatalogCollatorFactory_2 } from '@backstage/plugin-search-backend-module-catalog';
+import type { DefaultCatalogCollatorFactoryOptions as DefaultCatalogCollatorFactoryOptions_2 } from '@backstage/plugin-search-backend-module-catalog';
import { DeferredEntity as DeferredEntity_2 } from '@backstage/plugin-catalog-node';
-import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
import { Entity } from '@backstage/catalog-model';
import { EntityPolicy } from '@backstage/catalog-model';
import { EntityProvider as EntityProvider_2 } from '@backstage/plugin-catalog-node';
@@ -44,7 +46,6 @@ import { PermissionRule } from '@backstage/plugin-permission-node';
import { PermissionRuleParams } from '@backstage/plugin-permission-common';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
-import { Readable } from 'stream';
import { Router } from 'express';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { TokenManager } from '@backstage/backend-common';
@@ -160,10 +161,9 @@ export class CatalogBuilder {
useLegacySingleProcessorValidation(): this;
}
-// @public (undocumented)
-export type CatalogCollatorEntityTransformer = (
- entity: Entity,
-) => Omit;
+// @public @deprecated (undocumented)
+export type CatalogCollatorEntityTransformer =
+ CatalogCollatorEntityTransformer_2;
// @public (undocumented)
export type CatalogEnvironment = {
@@ -286,34 +286,15 @@ export class DefaultCatalogCollator {
readonly visibilityPermission: Permission;
}
-// @public (undocumented)
-export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer;
+// @public @deprecated (undocumented)
+export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer_2;
-// @public (undocumented)
-export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory {
- // (undocumented)
- static fromConfig(
- _config: Config,
- options: DefaultCatalogCollatorFactoryOptions,
- ): DefaultCatalogCollatorFactory;
- // (undocumented)
- getCollator(): Promise;
- // (undocumented)
- readonly type = 'software-catalog';
- // (undocumented)
- readonly visibilityPermission: Permission;
-}
+// @public @deprecated (undocumented)
+export const DefaultCatalogCollatorFactory: typeof DefaultCatalogCollatorFactory_2;
-// @public (undocumented)
-export type DefaultCatalogCollatorFactoryOptions = {
- discovery: PluginEndpointDiscovery;
- tokenManager: TokenManager;
- locationTemplate?: string;
- filter?: GetEntitiesRequest['filter'];
- batchSize?: number;
- catalogClient?: CatalogApi;
- entityTransformer?: CatalogCollatorEntityTransformer;
-};
+// @public @deprecated (undocumented)
+export type DefaultCatalogCollatorFactoryOptions =
+ DefaultCatalogCollatorFactoryOptions_2;
// @public @deprecated (undocumented)
export type DeferredEntity = DeferredEntity_2;
diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json
index 1e837fd9ba..023d709d81 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.8.0",
+ "version": "1.8.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -57,6 +57,7 @@
"@backstage/plugin-permission-common": "workspace:^",
"@backstage/plugin-permission-node": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
+ "@backstage/plugin-search-backend-module-catalog": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"@backstage/types": "workspace:^",
"@opentelemetry/api": "^1.3.0",
diff --git a/plugins/catalog-backend/src/index.ts b/plugins/catalog-backend/src/index.ts
index b3d2bc95c7..eca155ce46 100644
--- a/plugins/catalog-backend/src/index.ts
+++ b/plugins/catalog-backend/src/index.ts
@@ -27,3 +27,40 @@ export * from './processing';
export * from './search';
export * from './service';
export * from './deprecated';
+
+import {
+ DefaultCatalogCollatorFactory as _DefaultCatalogCollatorFactory,
+ defaultCatalogCollatorEntityTransformer as _defaultCatalogCollatorEntityTransformer,
+} from '@backstage/plugin-search-backend-module-catalog';
+
+/**
+ * @public
+ * @deprecated import from `@backstage/search-backend-module-catalog` instead
+ */
+export const DefaultCatalogCollatorFactory = _DefaultCatalogCollatorFactory;
+
+/**
+ * @public
+ * @deprecated import from `@backstage/search-backend-module-catalog` instead
+ */
+export const defaultCatalogCollatorEntityTransformer =
+ _defaultCatalogCollatorEntityTransformer;
+
+import type {
+ DefaultCatalogCollatorFactoryOptions as _DefaultCatalogCollatorFactoryOptions,
+ CatalogCollatorEntityTransformer as _CatalogCollatorEntityTransformer,
+} from '@backstage/plugin-search-backend-module-catalog';
+
+/**
+ * @public
+ * @deprecated import from `@backstage/search-backend-module-catalog` instead
+ */
+export type DefaultCatalogCollatorFactoryOptions =
+ _DefaultCatalogCollatorFactoryOptions;
+
+/**
+ * @public
+ * @deprecated import from `@backstage/search-backend-module-catalog` instead
+ */
+export type CatalogCollatorEntityTransformer =
+ _CatalogCollatorEntityTransformer;
diff --git a/plugins/catalog-backend/src/migrations.test.ts b/plugins/catalog-backend/src/migrations.test.ts
index f1ed40dbbe..33f88e9aa7 100644
--- a/plugins/catalog-backend/src/migrations.test.ts
+++ b/plugins/catalog-backend/src/migrations.test.ts
@@ -44,6 +44,67 @@ describe('migrations', () => {
ids: ['MYSQL_8', 'POSTGRES_13', 'POSTGRES_9', 'SQLITE_3'],
});
+ it.each(databases.eachSupportedId())(
+ 'latest version correctly cascades deletions, %p',
+ async databaseId => {
+ const knex = await databases.init(databaseId);
+ await knex.migrate.latest({ directory: migrationsDir });
+
+ await knex
+ .insert({
+ entity_id: 'i1',
+ entity_ref: 'k:ns/n1',
+ unprocessed_entity: '{}',
+ errors: '[]',
+ next_update_at: new Date(),
+ last_discovery_at: new Date(),
+ })
+ .into('refresh_state');
+ await knex
+ .insert({
+ entity_id: 'i2',
+ entity_ref: 'k:ns/n2',
+ unprocessed_entity: '{}',
+ errors: '[]',
+ next_update_at: new Date(),
+ last_discovery_at: new Date(),
+ })
+ .into('refresh_state');
+ await knex
+ .insert({ entity_id: 'i1', key: 'k1', value: 'v1' })
+ .into('search');
+ await knex
+ .insert({
+ source_entity_ref: 'k:ns/n1',
+ target_entity_ref: 'k:ns/n2',
+ })
+ .into('refresh_state_references');
+ await knex
+ .insert({
+ originating_entity_id: 'i1',
+ source_entity_ref: 'k:ns/n1',
+ target_entity_ref: 'k:ns/n2',
+ type: 't',
+ })
+ .into('relations');
+ await knex
+ .insert({
+ entity_id: 'i1',
+ hash: 'h',
+ stitch_ticket: '',
+ final_entity: '{}',
+ })
+ .into('final_entities');
+
+ await knex.delete().from('refresh_state').where({ entity_id: 'i1' });
+
+ await expect(knex('search')).resolves.toEqual([]);
+ await expect(knex('refresh_state_references')).resolves.toEqual([]);
+ await expect(knex('relations')).resolves.toEqual([]);
+ await expect(knex('final_entities')).resolves.toEqual([]);
+ },
+ );
+
it.each(databases.eachSupportedId())(
'20221109192547_search_add_original_value_column.js, %p',
async databaseId => {
diff --git a/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts
index 68c073eb0a..5e000f87d6 100644
--- a/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts
+++ b/plugins/catalog-backend/src/modules/core/LocationEntityProcessor.test.ts
@@ -21,7 +21,7 @@ import {
} from '@backstage/integration';
import path from 'path';
import { toAbsoluteUrl } from './LocationEntityProcessor';
-import { LocationSpec } from '@backstage/plugin-catalog-node';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
describe('LocationEntityProcessor', () => {
describe('toAbsoluteUrl', () => {
diff --git a/plugins/catalog-backend/src/search/index.ts b/plugins/catalog-backend/src/search/index.ts
index 3603ab2e60..50966e4c5b 100644
--- a/plugins/catalog-backend/src/search/index.ts
+++ b/plugins/catalog-backend/src/search/index.ts
@@ -14,11 +14,6 @@
* limitations under the License.
*/
-export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory';
-export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory';
-export type { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer';
-export { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer';
-
/**
* todo(backstage/techdocs-core): stop exporting this in a future release.
*/
diff --git a/plugins/catalog-backend/src/service/CatalogBuilder.ts b/plugins/catalog-backend/src/service/CatalogBuilder.ts
index 7da5d69767..782ad1bf3c 100644
--- a/plugins/catalog-backend/src/service/CatalogBuilder.ts
+++ b/plugins/catalog-backend/src/service/CatalogBuilder.ts
@@ -599,16 +599,28 @@ export class CatalogBuilder {
...this.placeholderResolvers,
};
- // These are always there no matter what
+ // The placeholder is always there no matter what
const processors: CatalogProcessor[] = [
new PlaceholderProcessor({
resolvers: placeholderResolvers,
reader,
integrations,
}),
- new BuiltinKindsEntityProcessor(),
];
+ const builtinKindsEntityProcessor = new BuiltinKindsEntityProcessor();
+ // If the user adds a processor named 'BuiltinKindsEntityProcessor',
+ // skip inclusion of the catalog-backend version.
+ if (
+ !this.processors.some(
+ processor =>
+ processor.getProcessorName() ===
+ builtinKindsEntityProcessor.getProcessorName(),
+ )
+ ) {
+ processors.push(builtinKindsEntityProcessor);
+ }
+
// These are only added unless the user replaced them all
if (!this.processorsReplace) {
processors.push(...this.getDefaultProcessors());
diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
index 275bd1c42c..25cdc8d826 100644
--- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
+++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts
@@ -1119,6 +1119,193 @@ describe('DefaultEntitiesCatalog', () => {
},
);
+ it.each(databases.eachSupportedId())(
+ 'should filter the text results when sortOrder is not provided, %p',
+ async databaseId => {
+ await createDatabase(databaseId);
+
+ const names = ['lion', 'cat', 'atcatss', 'dog', 'dogcat', 'aa', 's'];
+ const entities: Entity[] = names.map((name, index) =>
+ // Need a stable search since default filtering is by uid, and those get generated on the fly
+ // during the test case.
+ entityFrom(`${index}`, { uid: `id${index}`, title: name }),
+ );
+
+ const notFoundEntities: Entity[] = [
+ {
+ apiVersion: 'a',
+ kind: 'k',
+ metadata: { name: 'something' },
+ spec: {},
+ },
+ {
+ apiVersion: 'a',
+ kind: 'k',
+ metadata: { name: 'something else' },
+ spec: {},
+ },
+ ];
+
+ await Promise.all(
+ entities.concat(notFoundEntities).map(e => addEntityToSearch(e)),
+ );
+
+ const catalog = new DefaultEntitiesCatalog({
+ database: knex,
+ logger: getVoidLogger(),
+ stitcher,
+ });
+
+ const filter = {
+ key: 'spec.should_include_this',
+ };
+
+ const request: QueryEntitiesInitialRequest = {
+ filter,
+ limit: 100,
+ fullTextFilter: { term: 'cAt ', fields: ['metadata.title'] },
+ };
+ const response = await catalog.queryEntities(request);
+ expect(response.items).toEqual([
+ entityFrom('1', { uid: 'id1', title: 'cat' }),
+ entityFrom('2', { uid: 'id2', title: 'atcatss' }),
+ entityFrom('4', { uid: 'id4', title: 'dogcat' }),
+ ]);
+ expect(response.pageInfo.nextCursor).toBeUndefined();
+ expect(response.pageInfo.prevCursor).toBeUndefined();
+ expect(response.totalItems).toBe(3);
+
+ const paginatedResponse = await catalog.queryEntities({
+ ...request,
+ limit: 2,
+ });
+ expect(paginatedResponse.items).toEqual([
+ entityFrom('1', { uid: 'id1', title: 'cat' }),
+ entityFrom('2', { uid: 'id2', title: 'atcatss' }),
+ ]);
+ expect(paginatedResponse.pageInfo.nextCursor).not.toBeUndefined();
+ expect(paginatedResponse.pageInfo.prevCursor).toBeUndefined();
+ expect(paginatedResponse.totalItems).toBe(3);
+
+ const paginatedResponseNext = await catalog.queryEntities({
+ cursor: paginatedResponse.pageInfo.nextCursor!,
+ });
+ expect(paginatedResponseNext.items).toEqual([
+ entityFrom('4', { uid: 'id4', title: 'dogcat' }),
+ ]);
+ expect(paginatedResponseNext.pageInfo.nextCursor).toBeUndefined();
+ expect(paginatedResponseNext.pageInfo.prevCursor).not.toBeUndefined();
+ expect(paginatedResponseNext.totalItems).toBe(3);
+
+ const paginatedResponsePrev = await catalog.queryEntities({
+ cursor: paginatedResponseNext.pageInfo.prevCursor!,
+ });
+ expect(paginatedResponsePrev).toMatchObject(paginatedResponse);
+ },
+ );
+
+ it.each(databases.eachSupportedId())(
+ 'should filter the text results by multiple search fields if provided, %p',
+ async databaseId => {
+ await createDatabase(databaseId);
+
+ const defs = [
+ {
+ title: 'lion',
+ name: 'KingOfTheJungle',
+ },
+ { title: 'cat', name: 'NotKingOfTheJungle' },
+ { title: 'atcatss', name: 'NotACatKing' },
+ { title: 'king', name: '123' },
+ { title: 'dogcat', name: 'dogcat' },
+ { title: 'aa', name: 'test123' },
+ { title: 's', name: 'idk' },
+ ];
+ const entities: Entity[] = defs.map(({ title, name }, index) =>
+ // Need a stable search since default filtering is by uid, and those get generated on the fly
+ // during the test case.
+ entityFrom(name, { uid: `id${index}`, title }),
+ );
+
+ const notFoundEntities: Entity[] = [
+ {
+ apiVersion: 'a',
+ kind: 'k',
+ metadata: { name: 'something' },
+ spec: {},
+ },
+ {
+ apiVersion: 'a',
+ kind: 'k',
+ metadata: { name: 'something else' },
+ spec: {},
+ },
+ ];
+
+ await Promise.all(
+ entities.concat(notFoundEntities).map(e => addEntityToSearch(e)),
+ );
+
+ const catalog = new DefaultEntitiesCatalog({
+ database: knex,
+ logger: getVoidLogger(),
+ stitcher,
+ });
+
+ const filter = {
+ key: 'spec.should_include_this',
+ };
+
+ const request: QueryEntitiesInitialRequest = {
+ filter,
+ limit: 100,
+ fullTextFilter: {
+ term: 'KiNg ',
+ fields: ['metadata.title', 'metadata.name'],
+ },
+ };
+ const response = await catalog.queryEntities(request);
+
+ expect(response.items).toEqual([
+ entityFrom('KingOfTheJungle', { uid: 'id0', title: 'lion' }),
+ entityFrom('NotKingOfTheJungle', { uid: 'id1', title: 'cat' }),
+ entityFrom('NotACatKing', { uid: 'id2', title: 'atcatss' }),
+ entityFrom('123', { uid: 'id3', title: 'king' }),
+ ]);
+ expect(response.pageInfo.nextCursor).toBeUndefined();
+ expect(response.pageInfo.prevCursor).toBeUndefined();
+ expect(response.totalItems).toBe(4);
+
+ const paginatedResponse = await catalog.queryEntities({
+ ...request,
+ limit: 2,
+ });
+ expect(paginatedResponse.items).toEqual([
+ entityFrom('KingOfTheJungle', { uid: 'id0', title: 'lion' }),
+ entityFrom('NotKingOfTheJungle', { uid: 'id1', title: 'cat' }),
+ ]);
+ expect(paginatedResponse.pageInfo.nextCursor).not.toBeUndefined();
+ expect(paginatedResponse.pageInfo.prevCursor).toBeUndefined();
+ expect(paginatedResponse.totalItems).toBe(4);
+
+ const paginatedResponseNext = await catalog.queryEntities({
+ cursor: paginatedResponse.pageInfo.nextCursor!,
+ });
+ expect(paginatedResponseNext.items).toEqual([
+ entityFrom('NotACatKing', { uid: 'id2', title: 'atcatss' }),
+ entityFrom('123', { uid: 'id3', title: 'king' }),
+ ]);
+ expect(paginatedResponseNext.pageInfo.nextCursor).toBeUndefined();
+ expect(paginatedResponseNext.pageInfo.prevCursor).not.toBeUndefined();
+ expect(paginatedResponseNext.totalItems).toBe(4);
+
+ const paginatedResponsePrev = await catalog.queryEntities({
+ cursor: paginatedResponseNext.pageInfo.prevCursor!,
+ });
+ expect(paginatedResponsePrev).toMatchObject(paginatedResponse);
+ },
+ );
+
it.each(databases.eachSupportedId())(
'should include totalItems and empty entities in the response in case limit is zero, %p',
async databaseId => {
@@ -1577,7 +1764,11 @@ describe('DefaultEntitiesCatalog', () => {
function entityFrom(
name: string,
- { uid, namespace }: { uid?: string; namespace?: string } = {},
+ {
+ uid,
+ namespace,
+ title,
+ }: { uid?: string; namespace?: string; title?: string } = {},
) {
return {
apiVersion: 'a',
@@ -1586,6 +1777,7 @@ function entityFrom(
name,
...(!!namespace && { namespace }),
...(!!uid && { uid }),
+ ...(!!title && { title }),
},
spec: { should_include_this: 'yes' },
};
diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
index 4e6109423e..ba165f96af 100644
--- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
+++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts
@@ -382,11 +382,30 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog {
}
const normalizedFullTextFilterTerm = cursor.fullTextFilter?.term?.trim();
+ const textFilterFields = cursor.fullTextFilter?.fields ?? [sortField.field];
if (normalizedFullTextFilterTerm) {
- dbQuery.andWhereRaw(
- 'value like ?',
- `%${normalizedFullTextFilterTerm.toLocaleLowerCase('en-US')}%`,
- );
+ if (
+ textFilterFields.length === 1 &&
+ textFilterFields[0] === sortField.field
+ ) {
+ // If there is one item, apply the like query to the top level query which is already
+ // filtered based on the singular sortField.
+ dbQuery.andWhereRaw(
+ 'value like ?',
+ `%${normalizedFullTextFilterTerm.toLocaleLowerCase('en-US')}%`,
+ );
+ } else {
+ const matchQuery = db('search')
+ .select('search.entity_id')
+ .whereIn('key', textFilterFields)
+ .andWhere(function keyFilter() {
+ this.andWhereRaw(
+ 'value like ?',
+ `%${normalizedFullTextFilterTerm.toLocaleLowerCase('en-US')}%`,
+ );
+ });
+ dbQuery.andWhere('search.entity_id', 'in', matchQuery);
+ }
}
const countQuery = dbQuery.clone();
diff --git a/plugins/catalog-customized/CHANGELOG.md b/plugins/catalog-customized/CHANGELOG.md
index c155405317..b71628d43c 100644
--- a/plugins/catalog-customized/CHANGELOG.md
+++ b/plugins/catalog-customized/CHANGELOG.md
@@ -1,5 +1,13 @@
# @internal/plugin-catalog-customized
+## 0.0.9-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-catalog@1.10.0-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+
## 0.0.8
### Patch Changes
diff --git a/plugins/catalog-customized/package.json b/plugins/catalog-customized/package.json
index 59159bfe84..0cca05de5a 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.8",
+ "version": "0.0.9-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 04d714d024..5975f7d319 100644
--- a/plugins/catalog-graph/CHANGELOG.md
+++ b/plugins/catalog-graph/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-catalog-graph
+## 0.2.29-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.2.28
### Patch Changes
diff --git a/plugins/catalog-graph/package.json b/plugins/catalog-graph/package.json
index 1ae663f32c..f5cc5f7619 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.28",
+ "version": "0.2.29-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,7 +30,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"classnames": "^2.3.1",
"lodash": "^4.17.15",
"p-limit": "^3.1.0",
diff --git a/plugins/catalog-graphql/CHANGELOG.md b/plugins/catalog-graphql/CHANGELOG.md
index 740ed8905a..041191fcef 100644
--- a/plugins/catalog-graphql/CHANGELOG.md
+++ b/plugins/catalog-graphql/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-catalog-graphql
+## 0.3.20-next.0
+
+### Patch Changes
+
+- e47e69eadf0: Updated dependency `@apollo/server` to `^4.0.0`.
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/types@1.0.2
+
## 0.3.19
### Patch Changes
diff --git a/plugins/catalog-graphql/package.json b/plugins/catalog-graphql/package.json
index 274828db86..11b899f528 100644
--- a/plugins/catalog-graphql/package.json
+++ b/plugins/catalog-graphql/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-catalog-graphql",
"description": "An experimental Backstage catalog GraphQL module",
- "version": "0.3.19",
+ "version": "0.3.20-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -34,10 +34,11 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
+ "@apollo/client": "^3.0.0",
+ "@apollo/server": "^4.0.0",
"@backstage/catalog-model": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/types": "workspace:^",
- "apollo-server": "^3.0.0",
"graphql": "^16.0.0",
"graphql-modules": "^2.0.0",
"graphql-tag": "^2.11.0",
diff --git a/plugins/catalog-graphql/src/graphql/module.test.ts b/plugins/catalog-graphql/src/graphql/module.test.ts
index 5490c2030a..d4589a2625 100644
--- a/plugins/catalog-graphql/src/graphql/module.test.ts
+++ b/plugins/catalog-graphql/src/graphql/module.test.ts
@@ -22,7 +22,7 @@ import { ConfigReader } from '@backstage/config';
import { ReaderEntity } from '../service/client';
import { createLogger } from 'winston';
import { setupRequestMockHandlers } from '@backstage/test-utils';
-import { gql } from 'apollo-server';
+import { gql } from '@apollo/client';
import { Module, createApplication, testkit } from 'graphql-modules';
describe('Catalog Module', () => {
diff --git a/plugins/catalog-import/CHANGELOG.md b/plugins/catalog-import/CHANGELOG.md
index d29890d836..2298180278 100644
--- a/plugins/catalog-import/CHANGELOG.md
+++ b/plugins/catalog-import/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/plugin-catalog-import
+## 0.9.7-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/integration-react@1.1.12-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-catalog-common@1.0.12
+
## 0.9.6
### Patch Changes
diff --git a/plugins/catalog-import/package.json b/plugins/catalog-import/package.json
index a9e38367b9..d27a6118bd 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.6",
+ "version": "0.9.7-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -44,7 +44,7 @@
"@backstage/plugin-catalog-react": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@octokit/rest": "^19.0.3",
"git-url-parse": "^13.0.0",
"js-base64": "^3.6.0",
diff --git a/plugins/catalog-node/CHANGELOG.md b/plugins/catalog-node/CHANGELOG.md
index 71333f339c..e38a38d596 100644
--- a/plugins/catalog-node/CHANGELOG.md
+++ b/plugins/catalog-node/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-catalog-node
+## 1.3.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.12
+
## 1.3.4
### Patch Changes
diff --git a/plugins/catalog-node/package.json b/plugins/catalog-node/package.json
index e95258f59b..31b7032462 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.4",
+ "version": "1.3.5-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 1436ea2b2f..f7a96b5f07 100644
--- a/plugins/catalog-react/CHANGELOG.md
+++ b/plugins/catalog-react/CHANGELOG.md
@@ -1,5 +1,24 @@
# @backstage/plugin-catalog-react
+## 1.4.1-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/integration@1.4.3
+ - @backstage/plugin-permission-react@0.4.11
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/types@1.0.2
+ - @backstage/version-bridge@1.0.3
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-permission-common@0.7.4
+
## 1.4.0
### Minor Changes
diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json
index a905dea1f6..166da09118 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.4.0",
+ "version": "1.4.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -59,7 +59,7 @@
"@backstage/version-bridge": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"classnames": "^2.2.6",
"jwt-decode": "^3.1.0",
"lodash": "^4.17.21",
diff --git a/plugins/catalog/CHANGELOG.md b/plugins/catalog/CHANGELOG.md
index 239a7e6057..d7cd7cbe3a 100644
--- a/plugins/catalog/CHANGELOG.md
+++ b/plugins/catalog/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/plugin-catalog
+## 1.10.0-next.0
+
+### Minor Changes
+
+- 4dbf3d3e4da: Added a new EntitySwitch isResourceType to allow different views depending on Resource type
+- fc6cab4eb48: Added `isEntityWith` condition helper for `EntitySwitch` case statements.
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-search-react@1.5.2-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/integration-react@1.1.12-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-search-common@1.2.2
+
## 1.9.0
### Minor Changes
diff --git a/plugins/catalog/api-report.md b/plugins/catalog/api-report.md
index d891ebb7c8..ca91296913 100644
--- a/plugins/catalog/api-report.md
+++ b/plugins/catalog/api-report.md
@@ -355,6 +355,14 @@ export const EntityListContainer: (props: {
// @public
export function EntityOrphanWarning(): JSX.Element;
+// @public (undocumented)
+export interface EntityPredicates {
+ // (undocumented)
+ kind?: string | string[];
+ // (undocumented)
+ type?: string | string[];
+}
+
// @public
export function EntityProcessingErrorsPanel(): JSX.Element | null;
@@ -433,6 +441,11 @@ export function isComponentType(
types: string | string[],
): (entity: Entity) => boolean;
+// @public
+export function isEntityWith(
+ predicate: EntityPredicates,
+): (entity: Entity) => boolean;
+
// @public
export function isKind(kinds: string | string[]): (entity: Entity) => boolean;
@@ -444,6 +457,11 @@ export function isNamespace(
// @public
export function isOrphan(entity: Entity): boolean;
+// @public
+export function isResourceType(
+ types: string | string[],
+): (entity: Entity) => boolean;
+
// @public (undocumented)
export type PluginCatalogComponentsNameToClassKey = {
PluginCatalogEntityLinksEmptyState: EntityLinksEmptyStateClassKey;
diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json
index 782ab6051a..49242b8724 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.9.0",
+ "version": "1.10.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -46,7 +46,7 @@
"@backstage/types": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"history": "^5.0.0",
"lodash": "^4.17.21",
"pluralize": "^8.0.0",
diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.test.ts b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts
index dfed546fd8..66408aec18 100644
--- a/plugins/catalog/src/components/EntitySwitch/conditions.test.ts
+++ b/plugins/catalog/src/components/EntitySwitch/conditions.test.ts
@@ -15,7 +15,34 @@
*/
import { Entity } from '@backstage/catalog-model';
-import { isComponentType, isKind, isNamespace } from './conditions';
+import {
+ isComponentType,
+ isKind,
+ isNamespace,
+ isResourceType,
+ isEntityWith,
+} from './conditions';
+
+const kubernetesClusterResource: Entity = {
+ apiVersion: '',
+ kind: 'Resource',
+ metadata: { name: 'aKubernetesCluster' },
+ spec: { type: 'kubernetes-cluster' },
+};
+
+const databaseResource: Entity = {
+ apiVersion: '',
+ kind: 'Resource',
+ metadata: { name: 'aDatabase' },
+ spec: { type: 'database' },
+};
+
+const notResource: Entity = {
+ apiVersion: '',
+ kind: 'not-Resource',
+ metadata: { name: 'aService' },
+ spec: { type: 'service' },
+};
const serviceComponent: Entity = {
apiVersion: '',
@@ -38,6 +65,13 @@ const notComponent: Entity = {
spec: { type: 'service' },
};
+const missingSpecType: Entity = {
+ apiVersion: '',
+ kind: 'another-type',
+ metadata: { name: 'anEntity' },
+ spec: {},
+};
+
const apiKind: Entity = {
apiVersion: '',
kind: 'api',
@@ -59,6 +93,26 @@ const bNamespace: Entity = {
spec: { type: 'service' },
};
+describe('isResourceType', () => {
+ it('should false on non resource kinds', () => {
+ const checkEntity = isResourceType('kubernetes-cluster');
+
+ expect(checkEntity(notResource)).not.toBeTruthy();
+ });
+ it('should check for the intended type', () => {
+ const checkEntity = isResourceType('kubernetes-cluster');
+
+ expect(checkEntity(databaseResource)).not.toBeTruthy();
+ expect(checkEntity(kubernetesClusterResource)).toBeTruthy();
+ });
+ it('should check for multiple types', () => {
+ const checkEntity = isResourceType(['database', 'kubernetes-cluster']);
+
+ expect(checkEntity(databaseResource)).toBeTruthy();
+ expect(checkEntity(kubernetesClusterResource)).toBeTruthy();
+ });
+});
+
describe('isComponentType', () => {
it('should false on non component kinds', () => {
const checkEntity = isComponentType('service');
@@ -79,6 +133,25 @@ describe('isComponentType', () => {
});
});
+describe('isEntityWith', () => {
+ it('allows for a kind-only check (empty type array)', () => {
+ const checkEntity = isEntityWith({ kind: 'api', type: [] });
+ expect(checkEntity(apiKind)).toBeTruthy();
+ });
+ it('handles missing spec.type field', () => {
+ const checkEntity = isEntityWith({ kind: 'another-type', type: 'service' });
+ expect(checkEntity(missingSpecType)).not.toBeTruthy();
+ });
+ it('allows a check against no criteria', () => {
+ const checkEntity = isEntityWith({});
+ expect(checkEntity(apiKind)).toBeTruthy();
+ });
+ it('allows a check against empty criteria', () => {
+ const checkEntity = isEntityWith({ kind: [], type: [] });
+ expect(checkEntity(apiKind)).toBeTruthy();
+ });
+});
+
describe('isKind', () => {
it('should check for the intended kind', () => {
const checkEntity = isKind('component');
diff --git a/plugins/catalog/src/components/EntitySwitch/conditions.ts b/plugins/catalog/src/components/EntitySwitch/conditions.ts
index 86fd38a900..f834804141 100644
--- a/plugins/catalog/src/components/EntitySwitch/conditions.ts
+++ b/plugins/catalog/src/components/EntitySwitch/conditions.ts
@@ -14,18 +14,26 @@
* limitations under the License.
*/
-import { ComponentEntity, Entity } from '@backstage/catalog-model';
+import { Entity } from '@backstage/catalog-model';
-function strCmp(a: string | undefined, b: string | undefined): boolean {
+/** @public */
+export interface EntityPredicates {
+ kind?: string | string[];
+ type?: string | string[];
+}
+
+function strCmp(a: unknown, b: string | undefined): boolean {
return Boolean(
- a && a?.toLocaleLowerCase('en-US') === b?.toLocaleLowerCase('en-US'),
+ a &&
+ typeof a === 'string' &&
+ a?.toLocaleLowerCase('en-US') === b?.toLocaleLowerCase('en-US'),
);
}
-function strCmpAll(value: string | undefined, cmpValues: string | string[]) {
+function strCmpAll(value: unknown, cmpValues: string | string[]) {
return typeof cmpValues === 'string'
? strCmp(value, cmpValues)
- : cmpValues.some(cmpVal => strCmp(value, cmpVal));
+ : cmpValues.length === 0 || cmpValues.some(cmpVal => strCmp(value, cmpVal));
}
/**
@@ -33,7 +41,7 @@ function strCmpAll(value: string | undefined, cmpValues: string | string[]) {
* @public
*/
export function isKind(kinds: string | string[]) {
- return (entity: Entity) => strCmpAll(entity.kind, kinds);
+ return isEntityWith({ kind: kinds });
}
/**
@@ -41,12 +49,33 @@ export function isKind(kinds: string | string[]) {
* @public
*/
export function isComponentType(types: string | string[]) {
+ return isEntityWith({ kind: 'component', type: types });
+}
+
+/**
+ * For use in EntitySwitch.Case. Matches if the entity is a Resource of a given spec.type.
+ * @public
+ */
+export function isResourceType(types: string | string[]) {
+ return isEntityWith({ kind: 'resource', type: types });
+}
+
+/**
+ * For use in EntitySwitch.Case. Matches if the entity is the specified kind and type (if present).
+ * @public
+ */
+export function isEntityWith(predicate: EntityPredicates) {
return (entity: Entity) => {
- if (!strCmp(entity.kind, 'component')) {
+ if (predicate.kind && !strCmpAll(entity.kind, predicate.kind)) {
return false;
}
- const componentEntity = entity as ComponentEntity;
- return strCmpAll(componentEntity.spec.type, types);
+
+ if (predicate.type && !strCmpAll(entity.spec?.type, predicate.type)) {
+ return false;
+ }
+
+ // there's no type check, return true
+ return true;
};
}
diff --git a/plugins/catalog/src/components/EntitySwitch/index.ts b/plugins/catalog/src/components/EntitySwitch/index.ts
index 6cccf4be6e..8848f98866 100644
--- a/plugins/catalog/src/components/EntitySwitch/index.ts
+++ b/plugins/catalog/src/components/EntitySwitch/index.ts
@@ -16,4 +16,11 @@
export { EntitySwitch } from './EntitySwitch';
export type { EntitySwitchProps, EntitySwitchCaseProps } from './EntitySwitch';
-export { isKind, isNamespace, isComponentType } from './conditions';
+export type { EntityPredicates } from './conditions';
+export {
+ isKind,
+ isNamespace,
+ isComponentType,
+ isResourceType,
+ isEntityWith,
+} from './conditions';
diff --git a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
index a8519377a6..f68f624292 100644
--- a/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
+++ b/plugins/cicd-statistics-module-gitlab/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-cicd-statistics-module-gitlab
+## 0.1.13-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-cicd-statistics@0.1.19-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+
## 0.1.12
### Patch Changes
diff --git a/plugins/cicd-statistics-module-gitlab/package.json b/plugins/cicd-statistics-module-gitlab/package.json
index 8144319eaa..3cef2e0ff7 100644
--- a/plugins/cicd-statistics-module-gitlab/package.json
+++ b/plugins/cicd-statistics-module-gitlab/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-cicd-statistics-module-gitlab",
"description": "CI/CD Statistics plugin module; Gitlab CICD",
- "version": "0.1.12",
+ "version": "0.1.13-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/cicd-statistics/CHANGELOG.md b/plugins/cicd-statistics/CHANGELOG.md
index bc91b2611b..86c2579da9 100644
--- a/plugins/cicd-statistics/CHANGELOG.md
+++ b/plugins/cicd-statistics/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-cicd-statistics
+## 0.1.19-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+
## 0.1.18
### Patch Changes
diff --git a/plugins/cicd-statistics/package.json b/plugins/cicd-statistics/package.json
index 967aa89f7c..1a531cbf88 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.18",
+ "version": "0.1.19-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -43,7 +43,7 @@
"@date-io/luxon": "^1.3.13",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@material-ui/pickers": "^3.3.10",
"already": "^3.2.0",
"humanize-duration": "^3.27.0",
diff --git a/plugins/circleci/CHANGELOG.md b/plugins/circleci/CHANGELOG.md
index 93dd50f6be..7dc02638c4 100644
--- a/plugins/circleci/CHANGELOG.md
+++ b/plugins/circleci/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-circleci
+## 0.3.17-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.3.16
### Patch Changes
diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json
index 6f093a24b3..5f74d66245 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.16",
+ "version": "0.3.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -40,7 +40,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"circleci-api": "^4.0.0",
"humanize-duration": "^3.27.0",
"lodash": "^4.17.21",
diff --git a/plugins/circleci/src/index.ts b/plugins/circleci/src/index.ts
index e20f5c7d6b..442f774b68 100644
--- a/plugins/circleci/src/index.ts
+++ b/plugins/circleci/src/index.ts
@@ -15,7 +15,7 @@
*/
/**
- * A Backstage plugin that integrates towards Circle CI
+ * A Backstage plugin that integrates towards CircleCI.
*
* @packageDocumentation
*/
diff --git a/plugins/cloudbuild/CHANGELOG.md b/plugins/cloudbuild/CHANGELOG.md
index 935cb3e7bb..e1131349cf 100644
--- a/plugins/cloudbuild/CHANGELOG.md
+++ b/plugins/cloudbuild/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-cloudbuild
+## 0.3.17-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.3.16
### Patch Changes
diff --git a/plugins/cloudbuild/package.json b/plugins/cloudbuild/package.json
index 153c0a69b6..87b76043d2 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.16",
+ "version": "0.3.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -40,7 +40,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"luxon": "^3.0.0",
"qs": "^6.9.4",
"react-use": "^17.2.4"
diff --git a/plugins/code-climate/CHANGELOG.md b/plugins/code-climate/CHANGELOG.md
index 59599c528e..f84df1f4fd 100644
--- a/plugins/code-climate/CHANGELOG.md
+++ b/plugins/code-climate/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-code-climate
+## 0.1.17-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.1.16
### Patch Changes
diff --git a/plugins/code-climate/package.json b/plugins/code-climate/package.json
index 9ba3f290cf..1b75d7c59d 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.16",
+ "version": "0.1.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"humanize-duration": "^3.27.1",
"luxon": "^3.0.0",
"react-use": "^17.2.4"
diff --git a/plugins/code-coverage-backend/CHANGELOG.md b/plugins/code-coverage-backend/CHANGELOG.md
index 65a9ce5a60..9f498b3687 100644
--- a/plugins/code-coverage-backend/CHANGELOG.md
+++ b/plugins/code-coverage-backend/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-code-coverage-backend
+## 0.2.10-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+
## 0.2.9
### Patch Changes
diff --git a/plugins/code-coverage-backend/package.json b/plugins/code-coverage-backend/package.json
index affbfb79b9..dd4dac88c8 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.9",
+ "version": "0.2.10-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 8782089171..dfa39648c7 100644
--- a/plugins/code-coverage/CHANGELOG.md
+++ b/plugins/code-coverage/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-code-coverage
+## 0.2.10-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.2.9
### Patch Changes
diff --git a/plugins/code-coverage/package.json b/plugins/code-coverage/package.json
index b51862702c..e09a20355e 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.9",
+ "version": "0.2.10-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@material-ui/styles": "^4.11.0",
"highlight.js": "^10.6.0",
"luxon": "^3.0.0",
diff --git a/plugins/codescene/CHANGELOG.md b/plugins/codescene/CHANGELOG.md
index b6f69daacb..f854b3f22d 100644
--- a/plugins/codescene/CHANGELOG.md
+++ b/plugins/codescene/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-codescene
+## 0.1.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.1.11
### Patch Changes
diff --git a/plugins/codescene/package.json b/plugins/codescene/package.json
index f7d132affa..3e688063fc 100644
--- a/plugins/codescene/package.json
+++ b/plugins/codescene/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-codescene",
- "version": "0.1.11",
+ "version": "0.1.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "^4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"rc-progress": "3.4.1",
"react-use": "^17.2.4"
},
diff --git a/plugins/config-schema/CHANGELOG.md b/plugins/config-schema/CHANGELOG.md
index e78a82a633..e4e81e46c0 100644
--- a/plugins/config-schema/CHANGELOG.md
+++ b/plugins/config-schema/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-config-schema
+## 0.1.40-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/types@1.0.2
+
## 0.1.39
### Patch Changes
diff --git a/plugins/config-schema/package.json b/plugins/config-schema/package.json
index cb543cd3a2..13222fd55d 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.39",
+ "version": "0.1.40-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
"@backstage/types": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"jsonschema": "^1.2.6",
"react-use": "^17.2.4",
"zen-observable": "^0.10.0"
diff --git a/plugins/cost-insights/CHANGELOG.md b/plugins/cost-insights/CHANGELOG.md
index 2cf77c9aa9..8d459fb000 100644
--- a/plugins/cost-insights/CHANGELOG.md
+++ b/plugins/cost-insights/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-cost-insights
+## 0.12.6-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-cost-insights-common@0.1.1
+
## 0.12.5
### Patch Changes
diff --git a/plugins/cost-insights/package.json b/plugins/cost-insights/package.json
index 6d35d006a9..aef2c7635e 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.5",
+ "version": "0.12.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -41,7 +41,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@material-ui/styles": "^4.9.6",
"@types/recharts": "^1.8.14",
"classnames": "^2.2.6",
diff --git a/plugins/dynatrace/CHANGELOG.md b/plugins/dynatrace/CHANGELOG.md
index 451da29bf0..46fde4973e 100644
--- a/plugins/dynatrace/CHANGELOG.md
+++ b/plugins/dynatrace/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-dynatrace
+## 3.0.1-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 3.0.0
### Patch Changes
diff --git a/plugins/dynatrace/README.md b/plugins/dynatrace/README.md
index df6bb75f10..b07fe985fe 100644
--- a/plugins/dynatrace/README.md
+++ b/plugins/dynatrace/README.md
@@ -49,7 +49,7 @@ const serviceEntityPage = (
+ >
+
+
-
+
)
```
diff --git a/plugins/dynatrace/api-report.md b/plugins/dynatrace/api-report.md
index 1677318a9f..1dded736ad 100644
--- a/plugins/dynatrace/api-report.md
+++ b/plugins/dynatrace/api-report.md
@@ -16,6 +16,4 @@ export const DynatraceTab: () => JSX.Element;
// @public
export const isDynatraceAvailable: (entity: Entity) => boolean;
-
-// (No @packageDocumentation comment for this package)
```
diff --git a/plugins/dynatrace/package.json b/plugins/dynatrace/package.json
index b4a9e76c59..d01e7eafad 100644
--- a/plugins/dynatrace/package.json
+++ b/plugins/dynatrace/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-dynatrace",
- "version": "3.0.0",
+ "version": "3.0.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,7 +33,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/dynatrace/src/index.ts b/plugins/dynatrace/src/index.ts
index 0c5da219a5..90688a544f 100644
--- a/plugins/dynatrace/src/index.ts
+++ b/plugins/dynatrace/src/index.ts
@@ -14,4 +14,10 @@
* limitations under the License.
*/
+/**
+ * A Backstage plugin that integrates towards Dynatrace.
+ *
+ * @packageDocumentation
+ */
+
export { dynatracePlugin, DynatraceTab, isDynatraceAvailable } from './plugin';
diff --git a/plugins/entity-feedback-backend/CHANGELOG.md b/plugins/entity-feedback-backend/CHANGELOG.md
index 2bf4189393..f076d9e130 100644
--- a/plugins/entity-feedback-backend/CHANGELOG.md
+++ b/plugins/entity-feedback-backend/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-entity-feedback-backend
+## 0.1.2-next.0
+
+### Patch Changes
+
+- 7eba760e6f6: Added an endpoint to fetch anonymous aggregated results from an entity
+- Updated dependencies
+ - @backstage/plugin-entity-feedback-common@0.1.1-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/plugin-auth-node@0.2.13-next.0
+
## 0.1.1
### Patch Changes
diff --git a/plugins/entity-feedback-backend/package.json b/plugins/entity-feedback-backend/package.json
index 48ab43af9f..aa88dea67f 100644
--- a/plugins/entity-feedback-backend/package.json
+++ b/plugins/entity-feedback-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-entity-feedback-backend",
- "version": "0.1.1",
+ "version": "0.1.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/entity-feedback-backend/src/service/router.test.ts b/plugins/entity-feedback-backend/src/service/router.test.ts
index 74fae8248b..5df0c5cf9d 100644
--- a/plugins/entity-feedback-backend/src/service/router.test.ts
+++ b/plugins/entity-feedback-backend/src/service/router.test.ts
@@ -258,6 +258,22 @@ describe('createRouter', () => {
});
});
+ describe('GET /ratings/:entityRef/aggregate', () => {
+ it('should get aggregated ratings for an entity correctly', async () => {
+ const response = await request(app)
+ .get('/ratings/component%3Adefault%2Fservice/aggregate')
+ .send();
+ expect(mockDbHandler.getRatings).toHaveBeenCalledWith(
+ 'component:default/service',
+ );
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({
+ DISLIKE: 1,
+ LIKE: 2,
+ });
+ });
+ });
+
describe('POST /responses/:entityRef', () => {
it('should record a response correctly', async () => {
const body = { response: 'blah', comments: 'feedback', consent: true };
diff --git a/plugins/entity-feedback-backend/src/service/router.ts b/plugins/entity-feedback-backend/src/service/router.ts
index e1243cedaf..ea759c1639 100644
--- a/plugins/entity-feedback-backend/src/service/router.ts
+++ b/plugins/entity-feedback-backend/src/service/router.ts
@@ -25,7 +25,10 @@ import {
getBearerTokenFromAuthorizationHeader,
IdentityApi,
} from '@backstage/plugin-auth-node';
-import { EntityRatingsData } from '@backstage/plugin-entity-feedback-common';
+import {
+ EntityRatingsData,
+ Ratings,
+} from '@backstage/plugin-entity-feedback-common';
import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
@@ -167,6 +170,19 @@ export async function createRouter(
res.json(ratings.filter(r => accessibleEntityRefs.includes(r.userRef)));
});
+ router.get('/ratings/:entityRef/aggregate', async (req, res) => {
+ const entityRatings = (
+ await dbHandler.getRatings(req.params.entityRef)
+ ).reduce((ratings: Ratings, { rating }) => {
+ ratings[rating] = ratings[rating] ?? 0;
+ ratings[rating]++;
+
+ return ratings;
+ }, {});
+
+ res.json(entityRatings);
+ });
+
router.post('/responses/:entityRef', async (req, res) => {
const user = await identity.getIdentity({ request: req });
const { response, comments, consent } = req.body;
diff --git a/plugins/entity-feedback-common/CHANGELOG.md b/plugins/entity-feedback-common/CHANGELOG.md
index e646a4322d..ef373767ca 100644
--- a/plugins/entity-feedback-common/CHANGELOG.md
+++ b/plugins/entity-feedback-common/CHANGELOG.md
@@ -1,5 +1,11 @@
# @backstage/plugin-entity-feedback-common
+## 0.1.1-next.0
+
+### Patch Changes
+
+- 7eba760e6f6: Added an endpoint to fetch anonymous aggregated results from an entity
+
## 0.1.0
### Minor Changes
diff --git a/plugins/entity-feedback-common/api-report.md b/plugins/entity-feedback-common/api-report.md
index 118e6c78f2..f31bb0d041 100644
--- a/plugins/entity-feedback-common/api-report.md
+++ b/plugins/entity-feedback-common/api-report.md
@@ -10,9 +10,7 @@ export interface EntityRatingsData {
// (undocumented)
entityTitle?: string;
// (undocumented)
- ratings: {
- [ratingValue: string]: number;
- };
+ ratings: Ratings;
}
// @public (undocumented)
@@ -38,4 +36,10 @@ export interface Rating {
// (undocumented)
userRef: string;
}
+
+// @public (undocumented)
+export interface Ratings {
+ // (undocumented)
+ [ratingValue: string]: number;
+}
```
diff --git a/plugins/entity-feedback-common/package.json b/plugins/entity-feedback-common/package.json
index 45a5a2b131..b1fbb3f686 100644
--- a/plugins/entity-feedback-common/package.json
+++ b/plugins/entity-feedback-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-entity-feedback-common",
"description": "Common functionalities for the entity-feedback plugin",
- "version": "0.1.0",
+ "version": "0.1.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/entity-feedback-common/src/index.ts b/plugins/entity-feedback-common/src/index.ts
index eea289a899..247a5a5d54 100644
--- a/plugins/entity-feedback-common/src/index.ts
+++ b/plugins/entity-feedback-common/src/index.ts
@@ -40,13 +40,18 @@ export interface FeedbackResponse {
userRef: string;
}
+/**
+ * @public
+ */
+export interface Ratings {
+ [ratingValue: string]: number;
+}
+
/**
* @public
*/
export interface EntityRatingsData {
entityRef: string;
entityTitle?: string;
- ratings: {
- [ratingValue: string]: number;
- };
+ ratings: Ratings;
}
diff --git a/plugins/entity-feedback/CHANGELOG.md b/plugins/entity-feedback/CHANGELOG.md
index e19b5e17b1..1fa0d920ab 100644
--- a/plugins/entity-feedback/CHANGELOG.md
+++ b/plugins/entity-feedback/CHANGELOG.md
@@ -1,5 +1,23 @@
# @backstage/plugin-entity-feedback
+## 0.2.0-next.0
+
+### Minor Changes
+
+- 7eba760e6f6: Added an endpoint to fetch anonymous aggregated results from an entity
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/plugin-entity-feedback-common@0.1.1-next.0
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.1.1
### Patch Changes
diff --git a/plugins/entity-feedback/api-report.md b/plugins/entity-feedback/api-report.md
index 06b0166b0d..696536aef4 100644
--- a/plugins/entity-feedback/api-report.md
+++ b/plugins/entity-feedback/api-report.md
@@ -13,6 +13,7 @@ import { EntityRatingsData } from '@backstage/plugin-entity-feedback-common';
import { FeedbackResponse } from '@backstage/plugin-entity-feedback-common';
import { FetchApi } from '@backstage/core-plugin-api';
import { Rating } from '@backstage/plugin-entity-feedback-common';
+import { Ratings } from '@backstage/plugin-entity-feedback-common';
import { ReactNode } from 'react';
import { RouteRef } from '@backstage/core-plugin-api';
@@ -22,6 +23,7 @@ export interface EntityFeedbackApi {
getAllRatings(): Promise;
// (undocumented)
getOwnedRatings(ownerRef: string): Promise;
+ getRatingAggregates(entityRef: string): Promise;
// (undocumented)
getRatings(entityRef: string): Promise[]>;
// (undocumented)
@@ -48,6 +50,8 @@ export class EntityFeedbackClient implements EntityFeedbackApi {
// (undocumented)
getOwnedRatings(ownerRef: string): Promise;
// (undocumented)
+ getRatingAggregates(entityRef: string): Promise;
+ // (undocumented)
getRatings(entityRef: string): Promise[]>;
// (undocumented)
getResponses(
diff --git a/plugins/entity-feedback/package.json b/plugins/entity-feedback/package.json
index ba88e02b70..bac96f561c 100644
--- a/plugins/entity-feedback/package.json
+++ b/plugins/entity-feedback/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-entity-feedback",
- "version": "0.1.1",
+ "version": "0.2.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -36,7 +36,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/entity-feedback/src/api/EntityFeedbackApi.ts b/plugins/entity-feedback/src/api/EntityFeedbackApi.ts
index d2f1a46f55..7ec6a447f0 100644
--- a/plugins/entity-feedback/src/api/EntityFeedbackApi.ts
+++ b/plugins/entity-feedback/src/api/EntityFeedbackApi.ts
@@ -19,6 +19,7 @@ import {
EntityRatingsData,
FeedbackResponse,
Rating,
+ Ratings,
} from '@backstage/plugin-entity-feedback-common';
/**
@@ -40,6 +41,11 @@ export interface EntityFeedbackApi {
getRatings(entityRef: string): Promise[]>;
+ /**
+ * Returns anonymized aggregated results for one entityRef
+ */
+ getRatingAggregates(entityRef: string): Promise;
+
recordResponse(
entityRef: string,
response: Omit,
diff --git a/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts b/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts
index 694fd66b06..8ef6810115 100644
--- a/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts
+++ b/plugins/entity-feedback/src/api/EntityFeedbackClient.test.ts
@@ -121,6 +121,24 @@ describe('EntityFeedbackClient', () => {
expect(response).toEqual(ratings);
});
+ it('getRatingAggregates', async () => {
+ const ratings = { LIKE: 3, DISLIKE: 5 };
+
+ server.use(
+ rest.get(
+ `${mockBaseUrl}/ratings/${encodeURIComponent(
+ 'component:default/service',
+ )}/aggregate`,
+ (_, res, ctx) => res(ctx.json(ratings)),
+ ),
+ );
+
+ const response = await client.getRatingAggregates(
+ 'component:default/service',
+ );
+ expect(response).toEqual(ratings);
+ });
+
it('recordResponse', async () => {
expect.assertions(1);
const response = {
diff --git a/plugins/entity-feedback/src/api/EntityFeedbackClient.ts b/plugins/entity-feedback/src/api/EntityFeedbackClient.ts
index 34856473c8..f911cdc7f8 100644
--- a/plugins/entity-feedback/src/api/EntityFeedbackClient.ts
+++ b/plugins/entity-feedback/src/api/EntityFeedbackClient.ts
@@ -20,6 +20,7 @@ import {
EntityRatingsData,
FeedbackResponse,
Rating,
+ Ratings,
} from '@backstage/plugin-entity-feedback-common';
import { EntityFeedbackApi } from './EntityFeedbackApi';
@@ -97,6 +98,22 @@ export class EntityFeedbackClient implements EntityFeedbackApi {
return resp.json();
}
+ async getRatingAggregates(entityRef: string): Promise {
+ const baseUrl = await this.discoveryApi.getBaseUrl('entity-feedback');
+ const resp = await this.fetchApi.fetch(
+ `${baseUrl}/ratings/${encodeURIComponent(entityRef)}/aggregate`,
+ {
+ method: 'GET',
+ },
+ );
+
+ if (!resp.ok) {
+ throw await ResponseError.fromResponse(resp);
+ }
+
+ return resp.json();
+ }
+
async recordResponse(
entityRef: string,
response: Omit,
diff --git a/plugins/entity-validation/CHANGELOG.md b/plugins/entity-validation/CHANGELOG.md
index a714918b45..dc7db0eaec 100644
--- a/plugins/entity-validation/CHANGELOG.md
+++ b/plugins/entity-validation/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-entity-validation
+## 0.1.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-catalog-common@1.0.12
+
## 0.1.1
### Patch Changes
diff --git a/plugins/entity-validation/package.json b/plugins/entity-validation/package.json
index c7cd754547..811f9ab3c8 100644
--- a/plugins/entity-validation/package.json
+++ b/plugins/entity-validation/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-entity-validation",
- "version": "0.1.1",
+ "version": "0.1.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -40,7 +40,7 @@
"@codemirror/view": "^6.0.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "^4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@react-hookz/web": "^20.0.0",
"@uiw/react-codemirror": "^4.9.3",
"lodash": "^4.17.21",
diff --git a/plugins/events-backend-module-aws-sqs/CHANGELOG.md b/plugins/events-backend-module-aws-sqs/CHANGELOG.md
index ac076e463b..2dc726e160 100644
--- a/plugins/events-backend-module-aws-sqs/CHANGELOG.md
+++ b/plugins/events-backend-module-aws-sqs/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-events-backend-module-aws-sqs
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/types@1.0.2
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/events-backend-module-aws-sqs/package.json b/plugins/events-backend-module-aws-sqs/package.json
index 046d3363d2..a90dafadde 100644
--- a/plugins/events-backend-module-aws-sqs/package.json
+++ b/plugins/events-backend-module-aws-sqs/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-events-backend-module-aws-sqs",
- "version": "0.1.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-backend-module-azure/CHANGELOG.md b/plugins/events-backend-module-azure/CHANGELOG.md
index cf3de9b750..35560f1156 100644
--- a/plugins/events-backend-module-azure/CHANGELOG.md
+++ b/plugins/events-backend-module-azure/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-events-backend-module-azure
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/events-backend-module-azure/package.json b/plugins/events-backend-module-azure/package.json
index 6ac861253a..2b66bd1421 100644
--- a/plugins/events-backend-module-azure/package.json
+++ b/plugins/events-backend-module-azure/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-events-backend-module-azure",
- "version": "0.1.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md
index be247852e9..8189dc2b99 100644
--- a/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md
+++ b/plugins/events-backend-module-bitbucket-cloud/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-events-backend-module-bitbucket-cloud
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/events-backend-module-bitbucket-cloud/package.json b/plugins/events-backend-module-bitbucket-cloud/package.json
index 5a908876cb..e575d325ff 100644
--- a/plugins/events-backend-module-bitbucket-cloud/package.json
+++ b/plugins/events-backend-module-bitbucket-cloud/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-events-backend-module-bitbucket-cloud",
- "version": "0.1.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-backend-module-gerrit/CHANGELOG.md b/plugins/events-backend-module-gerrit/CHANGELOG.md
index 7388fe6c96..82bf56ef32 100644
--- a/plugins/events-backend-module-gerrit/CHANGELOG.md
+++ b/plugins/events-backend-module-gerrit/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-events-backend-module-gerrit
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/events-backend-module-gerrit/package.json b/plugins/events-backend-module-gerrit/package.json
index 3666179cd1..95612d2aab 100644
--- a/plugins/events-backend-module-gerrit/package.json
+++ b/plugins/events-backend-module-gerrit/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-events-backend-module-gerrit",
- "version": "0.1.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-backend-module-github/CHANGELOG.md b/plugins/events-backend-module-github/CHANGELOG.md
index f20041f8d8..d281e432df 100644
--- a/plugins/events-backend-module-github/CHANGELOG.md
+++ b/plugins/events-backend-module-github/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-events-backend-module-github
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/events-backend-module-github/package.json b/plugins/events-backend-module-github/package.json
index cbe8713836..7ec3660013 100644
--- a/plugins/events-backend-module-github/package.json
+++ b/plugins/events-backend-module-github/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-events-backend-module-github",
- "version": "0.1.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-backend-module-gitlab/CHANGELOG.md b/plugins/events-backend-module-gitlab/CHANGELOG.md
index 6d9895360d..9a47340948 100644
--- a/plugins/events-backend-module-gitlab/CHANGELOG.md
+++ b/plugins/events-backend-module-gitlab/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-events-backend-module-gitlab
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/events-backend-module-gitlab/package.json b/plugins/events-backend-module-gitlab/package.json
index 420bbb782e..3729fa4f24 100644
--- a/plugins/events-backend-module-gitlab/package.json
+++ b/plugins/events-backend-module-gitlab/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-events-backend-module-gitlab",
- "version": "0.1.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-backend-test-utils/CHANGELOG.md b/plugins/events-backend-test-utils/CHANGELOG.md
index dad7faf979..04e373d809 100644
--- a/plugins/events-backend-test-utils/CHANGELOG.md
+++ b/plugins/events-backend-test-utils/CHANGELOG.md
@@ -1,5 +1,12 @@
# @backstage/plugin-events-backend-test-utils
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.1.5
### Patch Changes
diff --git a/plugins/events-backend-test-utils/package.json b/plugins/events-backend-test-utils/package.json
index 77cecd0219..6f7ec7c54d 100644
--- a/plugins/events-backend-test-utils/package.json
+++ b/plugins/events-backend-test-utils/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-events-backend-test-utils",
"description": "The plugin-events-backend-test-utils for @backstage/plugin-events-node",
- "version": "0.1.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-backend/CHANGELOG.md b/plugins/events-backend/CHANGELOG.md
index 0b72628ccf..af29213c08 100644
--- a/plugins/events-backend/CHANGELOG.md
+++ b/plugins/events-backend/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-events-backend
+## 0.2.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/plugin-events-node@0.2.5-next.0
+
## 0.2.4
### Patch Changes
diff --git a/plugins/events-backend/package.json b/plugins/events-backend/package.json
index d2abad4cb8..7fda105e15 100644
--- a/plugins/events-backend/package.json
+++ b/plugins/events-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-events-backend",
- "version": "0.2.4",
+ "version": "0.2.5-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/events-node/CHANGELOG.md b/plugins/events-node/CHANGELOG.md
index 9b1599c25a..955ad8c4e0 100644
--- a/plugins/events-node/CHANGELOG.md
+++ b/plugins/events-node/CHANGELOG.md
@@ -1,5 +1,12 @@
# @backstage/plugin-events-node
+## 0.2.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-plugin-api@0.5.1-next.0
+
## 0.2.4
### Patch Changes
diff --git a/plugins/events-node/package.json b/plugins/events-node/package.json
index 22c64dd012..930cd439e3 100644
--- a/plugins/events-node/package.json
+++ b/plugins/events-node/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-events-node",
"description": "The plugin-events-node module for @backstage/plugin-events-backend",
- "version": "0.2.4",
+ "version": "0.2.5-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/example-todo-list-backend/CHANGELOG.md b/plugins/example-todo-list-backend/CHANGELOG.md
index 65488cd80e..c0fd1fcb21 100644
--- a/plugins/example-todo-list-backend/CHANGELOG.md
+++ b/plugins/example-todo-list-backend/CHANGELOG.md
@@ -1,5 +1,16 @@
# @internal/plugin-todo-list-backend
+## 1.0.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-auth-node@0.2.13-next.0
+
## 1.0.11
### Patch Changes
diff --git a/plugins/example-todo-list-backend/package.json b/plugins/example-todo-list-backend/package.json
index 8baa490b31..d5a4a116e0 100644
--- a/plugins/example-todo-list-backend/package.json
+++ b/plugins/example-todo-list-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@internal/plugin-todo-list-backend",
- "version": "1.0.11",
+ "version": "1.0.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/example-todo-list/CHANGELOG.md b/plugins/example-todo-list/CHANGELOG.md
index 022a8e8fb3..87fc367b86 100644
--- a/plugins/example-todo-list/CHANGELOG.md
+++ b/plugins/example-todo-list/CHANGELOG.md
@@ -1,5 +1,14 @@
# @internal/plugin-todo-list
+## 1.0.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/theme@0.2.18
+
## 1.0.11
### Patch Changes
diff --git a/plugins/example-todo-list/package.json b/plugins/example-todo-list/package.json
index 9c90b71b52..27e05d7144 100644
--- a/plugins/example-todo-list/package.json
+++ b/plugins/example-todo-list/package.json
@@ -1,6 +1,6 @@
{
"name": "@internal/plugin-todo-list",
- "version": "1.0.11",
+ "version": "1.0.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -28,7 +28,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/explore-backend/CHANGELOG.md b/plugins/explore-backend/CHANGELOG.md
index be73174a06..950430d91f 100644
--- a/plugins/explore-backend/CHANGELOG.md
+++ b/plugins/explore-backend/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-explore-backend
+## 0.0.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-explore-common@0.0.1
+ - @backstage/plugin-search-common@1.2.2
+
## 0.0.5
### Patch Changes
diff --git a/plugins/explore-backend/api-report.md b/plugins/explore-backend/api-report.md
index cc112ed3cc..e3933e350b 100644
--- a/plugins/explore-backend/api-report.md
+++ b/plugins/explore-backend/api-report.md
@@ -3,18 +3,14 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
-///
-
-import { Config } from '@backstage/config';
-import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
import { ExploreTool } from '@backstage/plugin-explore-common';
import express from 'express';
import { GetExploreToolsRequest } from '@backstage/plugin-explore-common';
import { GetExploreToolsResponse } from '@backstage/plugin-explore-common';
-import { IndexableDocument } from '@backstage/plugin-search-common';
import { Logger } from 'winston';
-import { PluginEndpointDiscovery } from '@backstage/backend-common';
-import { Readable } from 'stream';
+import type { ToolDocument as ToolDocument_2 } from '@backstage/plugin-search-backend-module-explore';
+import { ToolDocumentCollatorFactory as ToolDocumentCollatorFactory_2 } from '@backstage/plugin-search-backend-module-explore';
+import type { ToolDocumentCollatorFactoryOptions as ToolDocumentCollatorFactoryOptions_2 } from '@backstage/plugin-search-backend-module-explore';
// @public (undocumented)
export function createRouter(options: RouterOptions): Promise;
@@ -40,27 +36,13 @@ export class StaticExploreToolProvider implements ExploreToolProvider {
getTools(request: GetExploreToolsRequest): Promise;
}
-// @public
-export interface ToolDocument extends IndexableDocument, ExploreTool {}
+// @public @deprecated (undocumented)
+export type ToolDocument = ToolDocument_2;
-// @public
-export class ToolDocumentCollatorFactory implements DocumentCollatorFactory {
- // (undocumented)
- execute(): AsyncGenerator;
- // (undocumented)
- static fromConfig(
- _config: Config,
- options: ToolDocumentCollatorFactoryOptions,
- ): ToolDocumentCollatorFactory;
- // (undocumented)
- getCollator(): Promise;
- // (undocumented)
- readonly type: string;
-}
+// @public @deprecated (undocumented)
+export const ToolDocumentCollatorFactory: typeof ToolDocumentCollatorFactory_2;
-// @public
-export type ToolDocumentCollatorFactoryOptions = {
- discovery: PluginEndpointDiscovery;
- logger: Logger;
-};
+// @public @deprecated (undocumented)
+export type ToolDocumentCollatorFactoryOptions =
+ ToolDocumentCollatorFactoryOptions_2;
```
diff --git a/plugins/explore-backend/package.json b/plugins/explore-backend/package.json
index 00629c8da4..30ab27bf07 100644
--- a/plugins/explore-backend/package.json
+++ b/plugins/explore-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-explore-backend",
- "version": "0.0.5",
+ "version": "0.0.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -25,6 +25,7 @@
"@backstage/backend-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-explore-common": "workspace:^",
+ "@backstage/plugin-search-backend-module-explore": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
"@types/express": "*",
"express": "^4.18.1",
diff --git a/plugins/explore-backend/src/index.ts b/plugins/explore-backend/src/index.ts
index a0e7216621..ca501db96b 100644
--- a/plugins/explore-backend/src/index.ts
+++ b/plugins/explore-backend/src/index.ts
@@ -20,7 +20,6 @@
* @packageDocumentation
*/
-export * from './search';
export * from './service';
export * from './tools';
@@ -28,3 +27,28 @@ export * from './tools';
* @internal Example only - do not use in production
*/
export { exampleTools } from './example/exampleTools';
+
+import { ToolDocumentCollatorFactory as _ToolDocumentCollatorFactory } from '@backstage/plugin-search-backend-module-explore';
+import type {
+ ToolDocument as _ToolDocument,
+ ToolDocumentCollatorFactoryOptions as _ToolDocumentCollatorFactoryOptions,
+} from '@backstage/plugin-search-backend-module-explore';
+
+/**
+ * @public
+ * @deprecated import from `@backstage/search-backend-module-explore` instead
+ */
+export const ToolDocumentCollatorFactory = _ToolDocumentCollatorFactory;
+
+/**
+ * @public
+ * @deprecated import from `@backstage/search-backend-module-explore` instead
+ */
+export type ToolDocument = _ToolDocument;
+
+/**
+ * @public
+ * @deprecated import from `@backstage/search-backend-module-explore` instead
+ */
+export type ToolDocumentCollatorFactoryOptions =
+ _ToolDocumentCollatorFactoryOptions;
diff --git a/plugins/explore/CHANGELOG.md b/plugins/explore/CHANGELOG.md
index 46cf23bc90..f5cbde7200 100644
--- a/plugins/explore/CHANGELOG.md
+++ b/plugins/explore/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/plugin-explore
+## 0.4.2-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-search-react@1.5.2-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/plugin-explore-react@0.0.27
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-explore-common@0.0.1
+ - @backstage/plugin-search-common@1.2.2
+
## 0.4.1
### Patch Changes
diff --git a/plugins/explore/package.json b/plugins/explore/package.json
index d14a276ecc..eb5e1da5b3 100644
--- a/plugins/explore/package.json
+++ b/plugins/explore/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-explore",
"description": "A Backstage plugin for building an exploration page of your software ecosystem",
- "version": "0.4.1",
+ "version": "0.4.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -44,7 +44,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"classnames": "^2.2.6",
"react-use": "^17.2.4"
},
diff --git a/plugins/firehydrant/CHANGELOG.md b/plugins/firehydrant/CHANGELOG.md
index 2987daa5ff..50440aba5d 100644
--- a/plugins/firehydrant/CHANGELOG.md
+++ b/plugins/firehydrant/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-firehydrant
+## 0.2.1-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.2.0
### Minor Changes
diff --git a/plugins/firehydrant/package.json b/plugins/firehydrant/package.json
index 0aacc34906..2268ca77a1 100644
--- a/plugins/firehydrant/package.json
+++ b/plugins/firehydrant/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-firehydrant",
"description": "A Backstage plugin that integrates towards FireHydrant",
- "version": "0.2.0",
+ "version": "0.2.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -30,7 +30,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"luxon": "^3.0.0",
"react-use": "^17.2.4"
},
diff --git a/plugins/fossa/CHANGELOG.md b/plugins/fossa/CHANGELOG.md
index 7c713d6730..3cb8c21d6c 100644
--- a/plugins/fossa/CHANGELOG.md
+++ b/plugins/fossa/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-fossa
+## 0.2.49-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.2.48
### Patch Changes
diff --git a/plugins/fossa/package.json b/plugins/fossa/package.json
index 8d4b8bb7aa..f7d14a79aa 100644
--- a/plugins/fossa/package.json
+++ b/plugins/fossa/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-fossa",
"description": "A Backstage plugin that integrates towards FOSSA",
- "version": "0.2.48",
+ "version": "0.2.49-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -41,7 +41,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"cross-fetch": "^3.1.5",
"luxon": "^3.0.0",
"p-limit": "^3.0.2",
diff --git a/plugins/gcalendar/CHANGELOG.md b/plugins/gcalendar/CHANGELOG.md
index d9d1021f02..e13f9f04eb 100644
--- a/plugins/gcalendar/CHANGELOG.md
+++ b/plugins/gcalendar/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-gcalendar
+## 0.3.13-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.3.12
### Patch Changes
diff --git a/plugins/gcalendar/package.json b/plugins/gcalendar/package.json
index 603936d75a..5a0508b41c 100644
--- a/plugins/gcalendar/package.json
+++ b/plugins/gcalendar/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-gcalendar",
- "version": "0.3.12",
+ "version": "0.3.13-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -28,7 +28,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@maxim_mazurok/gapi.client.calendar": "^3.0.20220408",
"@tanstack/react-query": "^4.1.3",
"classnames": "^2.3.1",
diff --git a/plugins/gcp-projects/CHANGELOG.md b/plugins/gcp-projects/CHANGELOG.md
index b501db40d7..b952be109f 100644
--- a/plugins/gcp-projects/CHANGELOG.md
+++ b/plugins/gcp-projects/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-gcp-projects
+## 0.3.36-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/theme@0.2.18
+
## 0.3.35
### Patch Changes
diff --git a/plugins/gcp-projects/package.json b/plugins/gcp-projects/package.json
index 162aff640c..516a72cbfa 100644
--- a/plugins/gcp-projects/package.json
+++ b/plugins/gcp-projects/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-gcp-projects",
"description": "A Backstage plugin that helps you manage projects in GCP",
- "version": "0.3.35",
+ "version": "0.3.36-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -38,7 +38,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@react-hookz/web": "^20.0.0"
},
"peerDependencies": {
diff --git a/plugins/git-release-manager/CHANGELOG.md b/plugins/git-release-manager/CHANGELOG.md
index cec4a630bc..34e2e430ae 100644
--- a/plugins/git-release-manager/CHANGELOG.md
+++ b/plugins/git-release-manager/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-git-release-manager
+## 0.3.30-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/integration@1.4.3
+ - @backstage/theme@0.2.18
+
## 0.3.29
### Patch Changes
diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json
index 53fca75a4f..b43ee73925 100644
--- a/plugins/git-release-manager/package.json
+++ b/plugins/git-release-manager/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-git-release-manager",
"description": "A Backstage plugin that helps you manage releases in git",
- "version": "0.3.29",
+ "version": "0.3.30-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@octokit/rest": "^19.0.3",
"luxon": "^3.0.0",
"qs": "^6.10.1",
diff --git a/plugins/github-actions/CHANGELOG.md b/plugins/github-actions/CHANGELOG.md
index a8cf2cd18f..df186ba39d 100644
--- a/plugins/github-actions/CHANGELOG.md
+++ b/plugins/github-actions/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-github-actions
+## 0.5.17-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.5.16
### Patch Changes
diff --git a/plugins/github-actions/package.json b/plugins/github-actions/package.json
index 48f20a6722..78c0f5096a 100644
--- a/plugins/github-actions/package.json
+++ b/plugins/github-actions/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-github-actions",
"description": "A Backstage plugin that integrates towards GitHub Actions",
- "version": "0.5.16",
+ "version": "0.5.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -42,7 +42,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@octokit/rest": "^19.0.3",
"luxon": "^3.0.0",
"react-use": "^17.2.4"
diff --git a/plugins/github-deployments/CHANGELOG.md b/plugins/github-deployments/CHANGELOG.md
index a66a149b8b..47f7e4d86b 100644
--- a/plugins/github-deployments/CHANGELOG.md
+++ b/plugins/github-deployments/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-github-deployments
+## 0.1.48-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/integration-react@1.1.12-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.1.47
### Patch Changes
diff --git a/plugins/github-deployments/package.json b/plugins/github-deployments/package.json
index 0d56c6bb1b..e60e3da91a 100644
--- a/plugins/github-deployments/package.json
+++ b/plugins/github-deployments/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-github-deployments",
"description": "A Backstage plugin that integrates towards GitHub Deployments",
- "version": "0.1.47",
+ "version": "0.1.48-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,7 +33,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@octokit/graphql": "^5.0.0",
"luxon": "^3.0.0",
"react-use": "^17.2.4"
diff --git a/plugins/github-issues/CHANGELOG.md b/plugins/github-issues/CHANGELOG.md
index 46352a55d2..affbf603a6 100644
--- a/plugins/github-issues/CHANGELOG.md
+++ b/plugins/github-issues/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-github-issues
+## 0.2.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.2.5
### Patch Changes
diff --git a/plugins/github-issues/package.json b/plugins/github-issues/package.json
index 419a5babcb..cac335d9e3 100644
--- a/plugins/github-issues/package.json
+++ b/plugins/github-issues/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-github-issues",
- "version": "0.2.5",
+ "version": "0.2.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -31,7 +31,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.4",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "^4.0.0-alpha.61",
+ "@material-ui/lab": "4.0.0-alpha.61",
"luxon": "^3.0.0",
"octokit": "^2.0.4",
"react-use": "^17.4.0"
diff --git a/plugins/github-pull-requests-board/CHANGELOG.md b/plugins/github-pull-requests-board/CHANGELOG.md
index 3480ae5f6e..681552a571 100644
--- a/plugins/github-pull-requests-board/CHANGELOG.md
+++ b/plugins/github-pull-requests-board/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-github-pull-requests-board
+## 0.1.11-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/integration@1.4.3
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.1.10
### Patch Changes
diff --git a/plugins/github-pull-requests-board/package.json b/plugins/github-pull-requests-board/package.json
index 689359ba1a..1792a6e98c 100644
--- a/plugins/github-pull-requests-board/package.json
+++ b/plugins/github-pull-requests-board/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-github-pull-requests-board",
"description": "A Backstage plugin that allows you to see all open Pull Requests for all the repositories owned by your team",
- "version": "0.1.10",
+ "version": "0.1.11-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -42,7 +42,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@octokit/rest": "^19.0.3",
"luxon": "^3.0.0",
"p-limit": "^4.0.0",
diff --git a/plugins/gitops-profiles/CHANGELOG.md b/plugins/gitops-profiles/CHANGELOG.md
index 5c7f777ddc..6201080830 100644
--- a/plugins/gitops-profiles/CHANGELOG.md
+++ b/plugins/gitops-profiles/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-gitops-profiles
+## 0.3.35-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/theme@0.2.18
+
## 0.3.34
### Patch Changes
diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json
index d61efb7706..2208c6d011 100644
--- a/plugins/gitops-profiles/package.json
+++ b/plugins/gitops-profiles/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-gitops-profiles",
"description": "A Backstage plugin that helps you manage GitOps profiles",
- "version": "0.3.34",
+ "version": "0.3.35-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,12 +33,13 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
+ "@backstage/config": "workspace:^",
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx
index 5aa59a1cae..eb58e03580 100644
--- a/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx
+++ b/plugins/gitops-profiles/src/components/ProfileCatalog/ProfileCatalog.test.tsx
@@ -14,7 +14,11 @@
* limitations under the License.
*/
-import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
+import {
+ MockConfigApi,
+ renderInTestApp,
+ TestApiRegistry,
+} from '@backstage/test-utils';
import React from 'react';
import { gitOpsApiRef, GitOpsRestApi } from '../../api';
import ProfileCatalog from './ProfileCatalog';
@@ -31,6 +35,7 @@ import { githubAuthApiRef } from '@backstage/core-plugin-api';
describe('ProfileCatalog', () => {
it('should render', async () => {
const oauthRequestApi = new OAuthRequestManager();
+ const configApi = new MockConfigApi({});
const apis = TestApiRegistry.from(
[gitOpsApiRef, new GitOpsRestApi('http://localhost:3008')],
[
@@ -40,6 +45,7 @@ describe('ProfileCatalog', () => {
'http://example.com/{{pluginId}}',
),
oauthRequestApi,
+ configApi: configApi,
}),
],
);
diff --git a/plugins/gocd/CHANGELOG.md b/plugins/gocd/CHANGELOG.md
index 544bb848dc..06ca6ecca7 100644
--- a/plugins/gocd/CHANGELOG.md
+++ b/plugins/gocd/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-gocd
+## 0.1.23-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.1.22
### Patch Changes
diff --git a/plugins/gocd/package.json b/plugins/gocd/package.json
index e5727302f6..a8e2599f3b 100644
--- a/plugins/gocd/package.json
+++ b/plugins/gocd/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-gocd",
"description": "A Backstage plugin that integrates towards GoCD",
- "version": "0.1.22",
+ "version": "0.1.23-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -37,7 +37,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"lodash": "^4.17.21",
"luxon": "^3.0.0",
"qs": "^6.10.1",
diff --git a/plugins/graphiql/CHANGELOG.md b/plugins/graphiql/CHANGELOG.md
index 35d00a5080..816962b034 100644
--- a/plugins/graphiql/CHANGELOG.md
+++ b/plugins/graphiql/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-graphiql
+## 0.2.49-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/theme@0.2.18
+
## 0.2.48
### Patch Changes
diff --git a/plugins/graphiql/api-report.md b/plugins/graphiql/api-report.md
index c96966c585..ea765d4d2b 100644
--- a/plugins/graphiql/api-report.md
+++ b/plugins/graphiql/api-report.md
@@ -16,7 +16,7 @@ import { RouteRef } from '@backstage/core-plugin-api';
export type EndpointConfig = {
id: string;
title: string;
- url: string;
+ url: string | Promise;
method?: 'POST';
headers?: {
[name in string]: string;
diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json
index a91f8647ec..fcdf347ba1 100644
--- a/plugins/graphiql/package.json
+++ b/plugins/graphiql/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphiql",
"description": "Backstage plugin for browsing GraphQL APIs",
- "version": "0.2.48",
+ "version": "0.2.49-next.0",
"publishConfig": {
"access": "public",
"main": "dist/index.esm.js",
@@ -37,7 +37,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"graphiql": "^1.5.12",
"graphql": "^16.0.0",
"graphql-ws": "^5.4.1",
diff --git a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts
index 7f9c0ca4ee..52dc40fd81 100644
--- a/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts
+++ b/plugins/graphiql/src/lib/api/GraphQLEndpoints.ts
@@ -26,7 +26,7 @@ export type EndpointConfig = {
id: string;
title: string;
// Endpoint URL
- url: string;
+ url: string | Promise;
// only supports POST right now
method?: 'POST';
// Defaults to setting Content-Type to application/json
@@ -66,7 +66,7 @@ export class GraphQLEndpoints implements GraphQLBrowseApi {
...config.headers,
...options.headers,
};
- const res = await fetch(url, {
+ const res = await fetch(await url, {
method,
headers,
body,
diff --git a/plugins/graphql-backend/CHANGELOG.md b/plugins/graphql-backend/CHANGELOG.md
index 87f2fe54e0..1e2f5059eb 100644
--- a/plugins/graphql-backend/CHANGELOG.md
+++ b/plugins/graphql-backend/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-graphql-backend
+## 0.1.34-next.0
+
+### Patch Changes
+
+- e47e69eadf0: Updated dependency `@apollo/server` to `^4.0.0`.
+- Updated dependencies
+ - @backstage/plugin-catalog-graphql@0.3.20-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+
## 0.1.33
### Patch Changes
diff --git a/plugins/graphql-backend/package.json b/plugins/graphql-backend/package.json
index 10e3f6eca6..736c057a5b 100644
--- a/plugins/graphql-backend/package.json
+++ b/plugins/graphql-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphql-backend",
"description": "An experimental Backstage backend plugin for GraphQL",
- "version": "0.1.33",
+ "version": "0.1.34-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -33,13 +33,12 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
+ "@apollo/server": "^4.0.0",
"@backstage/backend-common": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-catalog-graphql": "workspace:^",
"@graphql-tools/schema": "^9.0.0",
"@types/express": "^4.17.6",
- "apollo-server": "^3.0.0",
- "apollo-server-express": "^3.0.0",
"express": "^4.17.1",
"express-promise-router": "^4.1.0",
"graphql": "^16.0.0",
diff --git a/plugins/graphql-backend/src/service/router.ts b/plugins/graphql-backend/src/service/router.ts
index aa83b51beb..fbddee435f 100644
--- a/plugins/graphql-backend/src/service/router.ts
+++ b/plugins/graphql-backend/src/service/router.ts
@@ -19,7 +19,8 @@ import express from 'express';
import Router from 'express-promise-router';
import { Logger } from 'winston';
import { createApplication } from 'graphql-modules';
-import { ApolloServer } from 'apollo-server-express';
+import { ApolloServer } from '@apollo/server';
+import { expressMiddleware } from '@apollo/server/express4';
import { createModule as createCatalogModule } from '@backstage/plugin-catalog-graphql';
import { Config } from '@backstage/config';
import helmet from 'helmet';
@@ -61,8 +62,6 @@ export async function createRouter(
response.json({ status: 'ok' });
});
- const apolloMiddleware = server.getMiddleware({ path: '/' });
-
if (process.env.NODE_ENV === 'development')
router.use(
helmet.contentSecurityPolicy({
@@ -72,7 +71,7 @@ export async function createRouter(
}),
);
- router.use(apolloMiddleware);
+ router.use(expressMiddleware(server));
router.use(errorHandler());
return router;
diff --git a/plugins/graphql-voyager/CHANGELOG.md b/plugins/graphql-voyager/CHANGELOG.md
index 43d2b66fd8..480e859caa 100644
--- a/plugins/graphql-voyager/CHANGELOG.md
+++ b/plugins/graphql-voyager/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-graphql-voyager
+## 0.1.2-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/theme@0.2.18
+
## 0.1.1
### Patch Changes
diff --git a/plugins/graphql-voyager/package.json b/plugins/graphql-voyager/package.json
index 25f947e455..deba02b6a4 100644
--- a/plugins/graphql-voyager/package.json
+++ b/plugins/graphql-voyager/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-graphql-voyager",
"description": "Backstage plugin for GraphQL Voyager",
- "version": "0.1.1",
+ "version": "0.1.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -28,7 +28,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"graphql-voyager": "^1.0.0-rc.31",
"react-use": "^17.2.4"
},
diff --git a/plugins/home/CHANGELOG.md b/plugins/home/CHANGELOG.md
index 1deba56290..0c1e662fef 100644
--- a/plugins/home/CHANGELOG.md
+++ b/plugins/home/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-home
+## 0.4.33-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.4.32
### Patch Changes
diff --git a/plugins/home/package.json b/plugins/home/package.json
index 1896a15006..767ed33557 100644
--- a/plugins/home/package.json
+++ b/plugins/home/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-home",
"description": "A Backstage plugin that helps you build a home page",
- "version": "0.4.32",
+ "version": "0.4.33-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -41,7 +41,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"lodash": "^4.17.21",
"react-use": "^17.2.4"
},
diff --git a/plugins/ilert/CHANGELOG.md b/plugins/ilert/CHANGELOG.md
index ff0683d4a8..ae6e292c85 100644
--- a/plugins/ilert/CHANGELOG.md
+++ b/plugins/ilert/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-ilert
+## 0.2.6-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.2.5
### Patch Changes
diff --git a/plugins/ilert/package.json b/plugins/ilert/package.json
index 903b69523e..67573d1e3a 100644
--- a/plugins/ilert/package.json
+++ b/plugins/ilert/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-ilert",
"description": "A Backstage plugin that integrates towards iLert",
- "version": "0.2.5",
+ "version": "0.2.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
"@date-io/luxon": "1.x",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@material-ui/pickers": "^3.3.10",
"humanize-duration": "^3.26.0",
"luxon": "^3.0.0",
diff --git a/plugins/jenkins-backend/CHANGELOG.md b/plugins/jenkins-backend/CHANGELOG.md
index a9f6bcaac4..0655d6c189 100644
--- a/plugins/jenkins-backend/CHANGELOG.md
+++ b/plugins/jenkins-backend/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-jenkins-backend
+## 0.1.34-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-jenkins-common@0.1.14
+ - @backstage/plugin-permission-common@0.7.4
+
## 0.1.33
### Patch Changes
diff --git a/plugins/jenkins-backend/package.json b/plugins/jenkins-backend/package.json
index 9f85185900..54375b6f30 100644
--- a/plugins/jenkins-backend/package.json
+++ b/plugins/jenkins-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-jenkins-backend",
"description": "A Backstage backend plugin that integrates towards Jenkins",
- "version": "0.1.33",
+ "version": "0.1.34-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/jenkins/CHANGELOG.md b/plugins/jenkins/CHANGELOG.md
index 19c41d8931..97bfce2e07 100644
--- a/plugins/jenkins/CHANGELOG.md
+++ b/plugins/jenkins/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-jenkins
+## 0.7.16-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-jenkins-common@0.1.14
+
## 0.7.15
### Patch Changes
diff --git a/plugins/jenkins/package.json b/plugins/jenkins/package.json
index fbb95c8303..23bbcfedcd 100644
--- a/plugins/jenkins/package.json
+++ b/plugins/jenkins/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-jenkins",
"description": "A Backstage plugin that integrates towards Jenkins",
- "version": "0.7.15",
+ "version": "0.7.16-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -42,7 +42,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"luxon": "^3.0.0",
"react-use": "^17.2.4"
},
diff --git a/plugins/kafka-backend/CHANGELOG.md b/plugins/kafka-backend/CHANGELOG.md
index a21c47dd96..d844d8339b 100644
--- a/plugins/kafka-backend/CHANGELOG.md
+++ b/plugins/kafka-backend/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-kafka-backend
+## 0.2.37-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+
## 0.2.36
### Patch Changes
diff --git a/plugins/kafka-backend/package.json b/plugins/kafka-backend/package.json
index 3f43fac671..d29ce0fb82 100644
--- a/plugins/kafka-backend/package.json
+++ b/plugins/kafka-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-kafka-backend",
"description": "A Backstage backend plugin that integrates towards Kafka",
- "version": "0.2.36",
+ "version": "0.2.37-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/kafka/CHANGELOG.md b/plugins/kafka/CHANGELOG.md
index 0ed581fc62..324b9ae111 100644
--- a/plugins/kafka/CHANGELOG.md
+++ b/plugins/kafka/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-kafka
+## 0.3.17-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.3.16
### Patch Changes
diff --git a/plugins/kafka/package.json b/plugins/kafka/package.json
index d3eab939e6..a39b72c5b9 100644
--- a/plugins/kafka/package.json
+++ b/plugins/kafka/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-kafka",
"description": "A Backstage plugin that integrates towards Kafka",
- "version": "0.3.16",
+ "version": "0.3.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -32,7 +32,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/kubernetes-backend/CHANGELOG.md b/plugins/kubernetes-backend/CHANGELOG.md
index 617a9d16f9..75a4567b98 100644
--- a/plugins/kubernetes-backend/CHANGELOG.md
+++ b/plugins/kubernetes-backend/CHANGELOG.md
@@ -1,5 +1,26 @@
# @backstage/plugin-kubernetes-backend
+## 0.10.0-next.0
+
+### Minor Changes
+
+- 804f6d16b0c: **BREAKING**: `KubernetesBuilder.create` now requires a `permissions` field of type `PermissionEvaluator`. The kubernetes `/proxy` endpoint now requires two tokens: the `Backstage-Kubernetes-Authorization` header should contain a bearer token for the target cluster, and the `Authorization` header should contain a backstage identity token. The kubernetes `/proxy` endpoint now requires a `Backstage-Kubernetes-Cluster` header replacing the previously required `X-Kubernetes-Cluster` header.
+
+### Patch Changes
+
+- 75d4985f5e8: Fixes bug whereby backstage crashes when bad credentials are provided to the kubernetes plugin.
+- 83d250badc6: Fix parsing error when kubernetes api is returning badly structured data.
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/plugin-kubernetes-common@0.6.2-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-permission-common@0.7.4
+ - @backstage/plugin-permission-node@0.7.7-next.0
+
## 0.9.4
### Patch Changes
diff --git a/plugins/kubernetes-backend/api-report.md b/plugins/kubernetes-backend/api-report.md
index a6a4bcfc58..449131a26e 100644
--- a/plugins/kubernetes-backend/api-report.md
+++ b/plugins/kubernetes-backend/api-report.md
@@ -17,6 +17,7 @@ import { KubernetesRequestAuth } from '@backstage/plugin-kubernetes-common';
import type { KubernetesRequestBody } from '@backstage/plugin-kubernetes-common';
import { Logger } from 'winston';
import type { ObjectsByEntityResponse } from '@backstage/plugin-kubernetes-common';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import type { RequestHandler } from 'express';
import { TokenCredential } from '@azure/identity';
@@ -141,6 +142,9 @@ export class GoogleServiceAccountAuthTranslator
): Promise;
}
+// @public
+export const HEADER_KUBERNETES_AUTH: string;
+
// @public
export const HEADER_KUBERNETES_CLUSTER: string;
@@ -200,6 +204,7 @@ export class KubernetesBuilder {
clusterSupplier: KubernetesClustersSupplier,
catalogApi: CatalogApi,
proxy: KubernetesProxy,
+ permissionApi: PermissionEvaluator,
): express.Router;
// (undocumented)
protected buildServiceLocator(
@@ -271,6 +276,8 @@ export interface KubernetesEnvironment {
config: Config;
// (undocumented)
logger: Logger;
+ // (undocumented)
+ permissions: PermissionEvaluator;
}
// @public
@@ -340,9 +347,16 @@ export type KubernetesObjectTypes =
export class KubernetesProxy {
constructor(logger: Logger, clusterSupplier: KubernetesClustersSupplier);
// (undocumented)
- createRequestHandler(): RequestHandler;
+ createRequestHandler(
+ options: KubernetesProxyCreateRequestHandlerOptions,
+ ): RequestHandler;
}
+// @public
+export type KubernetesProxyCreateRequestHandlerOptions = {
+ permissionApi: PermissionEvaluator;
+};
+
// @public
export interface KubernetesServiceLocator {
// (undocumented)
@@ -418,6 +432,8 @@ export interface RouterOptions {
discovery: PluginEndpointDiscovery;
// (undocumented)
logger: Logger;
+ // (undocumented)
+ permissions: PermissionEvaluator;
}
// @public (undocumented)
diff --git a/plugins/kubernetes-backend/examples/dice-roller/metrics-server.yaml b/plugins/kubernetes-backend/examples/dice-roller/metrics-server.yaml
index 673064aaf2..06875a5cf1 100644
--- a/plugins/kubernetes-backend/examples/dice-roller/metrics-server.yaml
+++ b/plugins/kubernetes-backend/examples/dice-roller/metrics-server.yaml
@@ -82,7 +82,7 @@ spec:
emptyDir: {}
containers:
- name: metrics-server
- image: k8s.gcr.io/metrics-server-amd64:v0.3.1
+ image: registry.k8s.io/metrics-server-amd64:v0.3.1
args:
- --kubelet-insecure-tls
- --kubelet-preferred-address-types=InternalIP
diff --git a/plugins/kubernetes-backend/package.json b/plugins/kubernetes-backend/package.json
index 850f3c67f4..8f7b071c9a 100644
--- a/plugins/kubernetes-backend/package.json
+++ b/plugins/kubernetes-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-kubernetes-backend",
"description": "A Backstage backend plugin that integrates towards Kubernetes",
- "version": "0.9.4",
+ "version": "0.10.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -42,6 +42,8 @@
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-kubernetes-common": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
+ "@backstage/plugin-permission-node": "workspace:^",
"@google-cloud/container": "^4.0.0",
"@jest-mock/express": "^2.0.1",
"@kubernetes/client-node": "0.18.1",
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts
index 2696587e69..1f8d525ea8 100644
--- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.test.ts
@@ -17,6 +17,13 @@ import AWS from 'aws-sdk';
import AWSMock from 'aws-sdk-mock';
import { AwsIamKubernetesAuthTranslator } from './AwsIamKubernetesAuthTranslator';
+const awsError: AWS.AWSError = {
+ code: '123',
+ message: 'no way',
+ name: 'nope',
+ time: new Date(),
+};
+
describe('AwsIamKubernetesAuthTranslator tests', () => {
let role: any = undefined;
const credentials: any = {
@@ -84,6 +91,25 @@ describe('AwsIamKubernetesAuthTranslator tests', () => {
expect(response.serviceAccountToken).toBeDefined();
});
+ describe('When the credentials is failing', () => {
+ beforeEach(() => {
+ jest.spyOn(AWS.config, 'getCredentials').mockImplementation(cb => {
+ cb(awsError, null);
+ });
+ });
+ it('throws the right error', async () => {
+ const authTranslator = new AwsIamKubernetesAuthTranslator();
+ await expect(
+ authTranslator.decorateClusterDetailsWithAuth({
+ assumeRole: role,
+ name: 'test-cluster',
+ url: '',
+ authProvider: 'aws',
+ }),
+ ).rejects.toEqual(awsError);
+ });
+ });
+
describe('When the role is assumed', () => {
// These credentials are not real.
// Pulled from example in docs: https://docs.aws.amazon.com/general/latest/gr/aws-sec-cred-types.html
diff --git a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts
index b89945902d..84e60c9c69 100644
--- a/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts
+++ b/plugins/kubernetes-backend/src/kubernetes-auth-translator/AwsIamKubernetesAuthTranslator.ts
@@ -55,46 +55,44 @@ export class AwsIamKubernetesAuthTranslator
assumeRole?: string,
externalId?: string,
): Promise {
- return new Promise(async (resolve, reject) => {
- const awsCreds = await this.awsGetCredentials();
+ const awsCreds = await this.awsGetCredentials();
- if (!(awsCreds instanceof Credentials))
- return reject(Error('No AWS credentials found.'));
+ if (!(awsCreds instanceof Credentials))
+ throw new Error('No AWS credentials found.');
- let creds: SigningCreds = {
- accessKeyId: awsCreds.accessKeyId,
- secretAccessKey: awsCreds.secretAccessKey,
- sessionToken: awsCreds.sessionToken,
+ let creds: SigningCreds = {
+ accessKeyId: awsCreds.accessKeyId,
+ secretAccessKey: awsCreds.secretAccessKey,
+ sessionToken: awsCreds.sessionToken,
+ };
+
+ if (!this.validCredentials(creds))
+ throw new Error('Invalid AWS credentials found.');
+ if (!assumeRole) return creds;
+
+ try {
+ const params: AWS.STS.Types.AssumeRoleRequest = {
+ RoleArn: assumeRole,
+ RoleSessionName: 'backstage-login',
};
+ if (externalId) params.ExternalId = externalId;
- if (!this.validCredentials(creds))
- return reject(Error('Invalid AWS credentials found.'));
- if (!assumeRole) return resolve(creds);
+ const assumedRole = await new AWS.STS().assumeRole(params).promise();
- try {
- const params: AWS.STS.Types.AssumeRoleRequest = {
- RoleArn: assumeRole,
- RoleSessionName: 'backstage-login',
- };
- if (externalId) params.ExternalId = externalId;
-
- const assumedRole = await new AWS.STS().assumeRole(params).promise();
-
- if (!assumedRole.Credentials) {
- throw new Error(`No credentials returned for role ${assumeRole}`);
- }
-
- creds = {
- accessKeyId: assumedRole.Credentials.AccessKeyId,
- secretAccessKey: assumedRole.Credentials.SecretAccessKey,
- sessionToken: assumedRole.Credentials.SessionToken,
- };
- } catch (e) {
- console.warn(`There was an error assuming the role: ${e}`);
- return reject(Error(`Unable to assume role: ${e}`));
+ if (!assumedRole.Credentials) {
+ throw new Error(`No credentials returned for role ${assumeRole}`);
}
- return resolve(creds);
- });
+
+ creds = {
+ accessKeyId: assumedRole.Credentials.AccessKeyId,
+ secretAccessKey: assumedRole.Credentials.SecretAccessKey,
+ sessionToken: assumedRole.Credentials.SessionToken,
+ };
+ } catch (e) {
+ console.warn(`There was an error assuming the role: ${e}`);
+ throw new Error(`Unable to assume role: ${e}`);
+ }
+ return creds;
}
async getBearerToken(
clusterName: string,
diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts
index cc2dea26c0..8eb7a066df 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.test.ts
@@ -31,16 +31,24 @@ import {
import { KubernetesBuilder } from './KubernetesBuilder';
import { KubernetesFanOutHandler } from './KubernetesFanOutHandler';
import { CatalogApi } from '@backstage/catalog-client';
-import { HEADER_KUBERNETES_CLUSTER } from './KubernetesProxy';
+import {
+ HEADER_KUBERNETES_CLUSTER,
+ HEADER_KUBERNETES_AUTH,
+} from './KubernetesProxy';
import { setupServer } from 'msw/node';
import { setupRequestMockHandlers } from '@backstage/backend-test-utils';
import { rest } from 'msw';
+import {
+ AuthorizeResult,
+ PermissionEvaluator,
+} from '@backstage/plugin-permission-common';
describe('KubernetesBuilder', () => {
let app: express.Express;
let kubernetesFanOutHandler: jest.Mocked;
let config: Config;
let catalogApi: CatalogApi;
+ let permissions: jest.Mocked;
beforeAll(async () => {
const logger = getVoidLogger();
@@ -74,10 +82,16 @@ describe('KubernetesBuilder', () => {
getKubernetesObjectsByEntity: jest.fn(),
} as any;
+ permissions = {
+ authorize: jest.fn(),
+ authorizeConditional: jest.fn(),
+ };
+
const { router } = await KubernetesBuilder.createBuilder({
config,
logger,
catalogApi,
+ permissions,
})
.setObjectsProvider(kubernetesFanOutHandler)
.setClusterSupplier(clusterSupplier)
@@ -250,6 +264,7 @@ describe('KubernetesBuilder', () => {
logger,
config,
catalogApi,
+ permissions,
})
.setClusterSupplier(clusterSupplier)
.setServiceLocator(serviceLocator)
@@ -271,27 +286,32 @@ describe('KubernetesBuilder', () => {
expect(response.status).toEqual(200);
});
});
-
describe('post /proxy', () => {
const worker = setupServer();
setupRequestMockHandlers(worker);
beforeEach(() => {
worker.use(
- rest.post('https://localhost:1234/api/v1/namespaces', (req, res, ctx) =>
- req
- .arrayBuffer()
- .then(body =>
- res(
- ctx.set('content-type', `${req.headers.get('content-type')}`),
- ctx.body(body),
- ),
- ),
+ rest.post(
+ 'https://localhost:1234/api/v1/namespaces',
+ (req, res, ctx) => {
+ if (!req.headers.get('Authorization')) {
+ return res(ctx.status(401));
+ }
+ return req
+ .arrayBuffer()
+ .then(body =>
+ res(
+ ctx.set('content-type', `${req.headers.get('content-type')}`),
+ ctx.body(body),
+ ),
+ );
+ },
),
);
});
- it('returns the given request body', async () => {
+ it('returns the given request body with permission set to allow', async () => {
const requestBody = {
kind: 'Namespace',
apiVersion: 'v1',
@@ -300,9 +320,14 @@ describe('KubernetesBuilder', () => {
},
};
+ permissions.authorize.mockReturnValue(
+ Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
+ );
+
const proxyEndpointRequest = request(app)
.post('/proxy/api/v1/namespaces')
.set(HEADER_KUBERNETES_CLUSTER, 'some-cluster')
+ .set(HEADER_KUBERNETES_AUTH, 'randomtoken')
.send(requestBody);
worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough()));
@@ -312,7 +337,7 @@ describe('KubernetesBuilder', () => {
expect(response.body).toStrictEqual(requestBody);
});
- it('supports yaml content type', async () => {
+ it('supports yaml content type with permission set to allow', async () => {
const requestBody = `---
kind: Namespace
apiVersion: v1
@@ -320,9 +345,14 @@ metadata:
name: new-ns
`;
+ permissions.authorize.mockReturnValue(
+ Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
+ );
+
const proxyEndpointRequest = request(app)
.post('/proxy/api/v1/namespaces')
.set(HEADER_KUBERNETES_CLUSTER, 'some-cluster')
+ .set(HEADER_KUBERNETES_AUTH, 'randomtoken')
.set('content-type', 'application/yaml')
.send(requestBody);
@@ -331,5 +361,50 @@ metadata:
const response = await proxyEndpointRequest;
expect(response.text).toEqual(requestBody);
});
+
+ it('returns a 403 response if Permission Policy is in place that blocks endpoint', async () => {
+ const requestBody = {
+ kind: 'Namespace',
+ apiVersion: 'v1',
+ metadata: {
+ name: 'new-ns',
+ },
+ };
+
+ permissions.authorize.mockReturnValue(
+ Promise.resolve([{ result: AuthorizeResult.DENY }]),
+ );
+
+ const proxyEndpointRequest = request(app)
+ .post('/proxy/api/v1/namespaces')
+ .set(HEADER_KUBERNETES_CLUSTER, 'some-cluster')
+ .set(HEADER_KUBERNETES_AUTH, 'randomtoken')
+ .send(requestBody);
+
+ worker.use(rest.all(proxyEndpointRequest.url, req => req.passthrough()));
+
+ const response = await proxyEndpointRequest;
+
+ expect(response.status).toEqual(403);
+ });
+ });
+ describe('get /.well-known/backstage/permissions/metadata', () => {
+ it('lists permissions supported by the kubernetes plugin', async () => {
+ const response = await request(app).get(
+ '/.well-known/backstage/permissions/metadata',
+ );
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toMatchObject({
+ permissions: [
+ {
+ type: 'basic',
+ name: 'kubernetes.proxy',
+ attributes: {},
+ },
+ ],
+ rules: [],
+ });
+ });
});
});
diff --git a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts
index 74ec9eada2..367e5edef3 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesBuilder.ts
@@ -15,6 +15,9 @@
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Config } from '@backstage/config';
+import { kubernetesPermissions } from '@backstage/plugin-kubernetes-common';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
+import { createPermissionIntegrationRouter } from '@backstage/plugin-permission-node';
import express from 'express';
import Router from 'express-promise-router';
import { Duration } from 'luxon';
@@ -49,6 +52,7 @@ export interface KubernetesEnvironment {
logger: Logger;
config: Config;
catalogApi: CatalogApi;
+ permissions: PermissionEvaluator;
}
/**
@@ -89,6 +93,7 @@ export class KubernetesBuilder {
public async build(): KubernetesBuilderReturn {
const logger = this.env.logger;
const config = this.env.config;
+ const permissions = this.env.permissions;
logger.info('Initializing Kubernetes backend');
@@ -126,6 +131,7 @@ export class KubernetesBuilder {
clusterSupplier,
this.env.catalogApi,
proxy,
+ permissions,
);
return {
@@ -262,12 +268,17 @@ export class KubernetesBuilder {
clusterSupplier: KubernetesClustersSupplier,
catalogApi: CatalogApi,
proxy: KubernetesProxy,
+ permissionApi: PermissionEvaluator,
): express.Router {
const logger = this.env.logger;
const router = Router();
- router.use('/proxy', proxy.createRequestHandler());
+ router.use('/proxy', proxy.createRequestHandler({ permissionApi }));
router.use(express.json());
-
+ router.use(
+ createPermissionIntegrationRouter({
+ permissions: kubernetesPermissions,
+ }),
+ );
// @deprecated
router.post('/services/:serviceId', async (req, res) => {
const serviceId = req.params.serviceId;
diff --git a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts
index e0a6c21c4a..f71c6506bd 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesFanOutHandler.ts
@@ -49,6 +49,14 @@ import {
PodStatus,
} from '@kubernetes/client-node';
+const isRejected = (
+ input: PromiseSettledResult,
+): input is PromiseRejectedResult => input.status === 'rejected';
+
+const isFulfilled = (
+ input: PromiseSettledResult,
+): input is PromiseFulfilledResult => input.status === 'fulfilled';
+
/**
*
* @public
@@ -298,12 +306,12 @@ export class KubernetesFanOutHandler {
auth: KubernetesRequestAuth,
requestContext: ServiceLocatorRequestContext,
) {
- const clusterDetails: ClusterDetails[] = await (
+ const clusterDetails: ClusterDetails[] = (
await this.serviceLocator.getClustersByEntity(entity, requestContext)
).clusters;
// Execute all of these async actions simultaneously/without blocking sequentially as no common object is modified by them
- return await Promise.all(
+ const promiseResults = await Promise.allSettled(
clusterDetails.map(cd => {
const kubernetesAuthTranslator: KubernetesAuthTranslator =
this.getAuthTranslator(cd.authProvider);
@@ -313,6 +321,11 @@ export class KubernetesFanOutHandler {
);
}),
);
+
+ promiseResults.filter(isRejected).map(item => {
+ this.logger.info(`Failed to decorate cluster details: ${item.reason}`);
+ });
+ return promiseResults.filter(isFulfilled).map(item => item.value);
}
toObjectsByEntityResponse(
diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts
index 4254bb33da..d2404af839 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.test.ts
@@ -30,6 +30,10 @@ import {
HEADER_KUBERNETES_CLUSTER,
KubernetesProxy,
} from './KubernetesProxy';
+import {
+ AuthorizeResult,
+ PermissionEvaluator,
+} from '@backstage/plugin-permission-common';
describe('KubernetesProxy', () => {
let proxy: KubernetesProxy;
@@ -64,6 +68,11 @@ describe('KubernetesProxy', () => {
getClusters: jest.fn(),
};
+ const permissionApi: jest.Mocked = {
+ authorize: jest.fn(),
+ authorizeConditional: jest.fn(),
+ };
+
beforeEach(() => {
jest.resetAllMocks();
proxy = new KubernetesProxy(getVoidLogger(), clusterSupplier);
@@ -71,13 +80,16 @@ describe('KubernetesProxy', () => {
it('should return a ERROR_NOT_FOUND if no clusters are found', async () => {
clusterSupplier.getClusters.mockResolvedValue([]);
+ permissionApi.authorize.mockReturnValue(
+ Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
+ );
const req = buildMockRequest('test', 'api');
const { res, next } = getMockRes();
- await expect(proxy.createRequestHandler()(req, res, next)).rejects.toThrow(
- NotFoundError,
- );
+ await expect(
+ proxy.createRequestHandler({ permissionApi })(req, res, next),
+ ).rejects.toThrow(NotFoundError);
});
it('should pass the exact response from Kubernetes', async () => {
@@ -100,7 +112,13 @@ describe('KubernetesProxy', () => {
authProvider: 'serviceAccount',
},
] as ClusterDetails[]);
- const app = express().use('/mountpath', proxy.createRequestHandler());
+ const app = express().use(
+ '/mountpath',
+ proxy.createRequestHandler({ permissionApi }),
+ );
+ permissionApi.authorize.mockReturnValue(
+ Promise.resolve([{ result: AuthorizeResult.ALLOW }]),
+ );
const requestPromise = request(app)
.get('/mountpath/api')
.set(HEADER_KUBERNETES_CLUSTER, 'cluster1');
diff --git a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts
index 178105ef79..49a381b75a 100644
--- a/plugins/kubernetes-backend/src/service/KubernetesProxy.ts
+++ b/plugins/kubernetes-backend/src/service/KubernetesProxy.ts
@@ -18,9 +18,16 @@ import {
ErrorResponseBody,
ForwardedError,
InputError,
+ NotAllowedError,
NotFoundError,
serializeError,
} from '@backstage/errors';
+import { getBearerTokenFromAuthorizationHeader } from '@backstage/plugin-auth-node';
+import { kubernetesProxyPermission } from '@backstage/plugin-kubernetes-common';
+import {
+ PermissionEvaluator,
+ AuthorizeResult,
+} from '@backstage/plugin-permission-common';
import { bufferFromFileOrString } from '@kubernetes/client-node';
import type { Request, RequestHandler } from 'express';
import { createProxyMiddleware } from 'http-proxy-middleware';
@@ -34,7 +41,24 @@ export const APPLICATION_JSON: string = 'application/json';
*
* @public
*/
-export const HEADER_KUBERNETES_CLUSTER: string = 'X-Kubernetes-Cluster';
+export const HEADER_KUBERNETES_CLUSTER: string = 'Backstage-Kubernetes-Cluster';
+
+/**
+ * The header that is used to specify the Authentication Authorities token.
+ * e.x if using the google auth provider as your authentication authority then this field would be the google provided bearer token.
+ * @public
+ */
+export const HEADER_KUBERNETES_AUTH: string =
+ 'Backstage-Kubernetes-Authorization';
+
+/**
+ * The options object expected to be passed as a parameter to KubernetesProxy.createRequestHandler().
+ *
+ * @public
+ */
+export type KubernetesProxyCreateRequestHandlerOptions = {
+ permissionApi: PermissionEvaluator;
+};
/**
* A proxy that routes requests to the Kubernetes API.
@@ -49,8 +73,29 @@ export class KubernetesProxy {
private readonly clusterSupplier: KubernetesClustersSupplier,
) {}
- public createRequestHandler(): RequestHandler {
+ public createRequestHandler(
+ options: KubernetesProxyCreateRequestHandlerOptions,
+ ): RequestHandler {
+ const { permissionApi } = options;
return async (req, res, next) => {
+ const token = getBearerTokenFromAuthorizationHeader(
+ req.header('authorization'),
+ );
+
+ const authorizeResponse = (
+ await permissionApi.authorize(
+ [{ permission: kubernetesProxyPermission }],
+ {
+ token,
+ },
+ )
+ )[0];
+
+ if (authorizeResponse.result === AuthorizeResult.DENY) {
+ res.status(403).json({ error: new NotAllowedError('Unauthorized') });
+ return;
+ }
+
const middleware = await this.getMiddleware(req);
middleware(req, res, next);
};
@@ -104,6 +149,11 @@ export class KubernetesProxy {
res.status(500).json(body);
},
+ onProxyReq: (proxyReq, req) => {
+ // the kubernetes proxy endpoint expects a header field labeled `Backstage-Kubernetes-Authorization` that will be used to authenticate with the Kubernetes Api. The token provided as a value should be an bearer token for the target cluster.
+ const token = req.header(HEADER_KUBERNETES_AUTH) ?? '';
+ proxyReq.setHeader('Authorization', token);
+ },
});
this.middlewareForClusterName.set(originalCluster.name, middleware);
diff --git a/plugins/kubernetes-backend/src/service/index.ts b/plugins/kubernetes-backend/src/service/index.ts
index 620f788ac2..2f3e907879 100644
--- a/plugins/kubernetes-backend/src/service/index.ts
+++ b/plugins/kubernetes-backend/src/service/index.ts
@@ -16,5 +16,10 @@
export * from './KubernetesBuilder';
export { DEFAULT_OBJECTS } from './KubernetesFanOutHandler';
-export { HEADER_KUBERNETES_CLUSTER, KubernetesProxy } from './KubernetesProxy';
+export {
+ HEADER_KUBERNETES_CLUSTER,
+ KubernetesProxy,
+ HEADER_KUBERNETES_AUTH,
+} from './KubernetesProxy';
+export type { KubernetesProxyCreateRequestHandlerOptions } from './KubernetesProxy';
export * from './router';
diff --git a/plugins/kubernetes-backend/src/service/router.ts b/plugins/kubernetes-backend/src/service/router.ts
index 341956553e..f7e58f535c 100644
--- a/plugins/kubernetes-backend/src/service/router.ts
+++ b/plugins/kubernetes-backend/src/service/router.ts
@@ -21,6 +21,7 @@ import express from 'express';
import { KubernetesBuilder } from './KubernetesBuilder';
import { PluginEndpointDiscovery } from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
/**
*
@@ -32,6 +33,7 @@ export interface RouterOptions {
catalogApi: CatalogApi;
clusterSupplier?: KubernetesClustersSupplier;
discovery: PluginEndpointDiscovery;
+ permissions: PermissionEvaluator;
}
/**
diff --git a/plugins/kubernetes-backend/src/service/standaloneApplication.ts b/plugins/kubernetes-backend/src/service/standaloneApplication.ts
index 8234f89d48..e3afb5a385 100644
--- a/plugins/kubernetes-backend/src/service/standaloneApplication.ts
+++ b/plugins/kubernetes-backend/src/service/standaloneApplication.ts
@@ -28,6 +28,7 @@ import { Logger } from 'winston';
import { createRouter } from './router';
import { ConfigReader } from '@backstage/config';
import { CatalogClient } from '@backstage/catalog-client';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
export interface ApplicationOptions {
enableCors: boolean;
@@ -47,6 +48,8 @@ export async function createStandaloneApplication(
discoveryApi: SingleHostDiscovery.fromConfig(config),
});
+ const permissions = {} as PermissionEvaluator;
+
app.use(helmet());
if (enableCors) {
app.use(cors());
@@ -54,7 +57,10 @@ export async function createStandaloneApplication(
app.use(compression());
app.use(express.json());
app.use(requestLoggingHandler());
- app.use('/', await createRouter({ logger, config, discovery, catalogApi }));
+ app.use(
+ '/',
+ await createRouter({ logger, config, discovery, catalogApi, permissions }),
+ );
app.use(notFoundHandler());
app.use(errorHandler());
diff --git a/plugins/kubernetes-common/CHANGELOG.md b/plugins/kubernetes-common/CHANGELOG.md
index d12719a9f2..862899600e 100644
--- a/plugins/kubernetes-common/CHANGELOG.md
+++ b/plugins/kubernetes-common/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-kubernetes-common
+## 0.6.2-next.0
+
+### Patch Changes
+
+- 804f6d16b0c: Introduced proxy permission types to be used with the kubernetes proxy endpoint's permission framework integration.
+- Updated dependencies
+ - @backstage/catalog-model@1.2.1
+ - @backstage/plugin-permission-common@0.7.4
+
## 0.6.1
### Patch Changes
diff --git a/plugins/kubernetes-common/api-report.md b/plugins/kubernetes-common/api-report.md
index fb41895f1a..6c88074e50 100644
--- a/plugins/kubernetes-common/api-report.md
+++ b/plugins/kubernetes-common/api-report.md
@@ -3,6 +3,7 @@
> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
```ts
+import { BasicPermission } from '@backstage/plugin-permission-common';
import { Entity } from '@backstage/catalog-model';
import type { JsonObject } from '@backstage/types';
import { PodStatus } from '@kubernetes/client-node';
@@ -217,6 +218,12 @@ export type KubernetesErrorTypes =
// @public (undocumented)
export type KubernetesFetchError = StatusError | RawFetchError;
+// @public
+export const kubernetesPermissions: BasicPermission[];
+
+// @public
+export const kubernetesProxyPermission: BasicPermission;
+
// @public (undocumented)
export interface KubernetesRequestAuth {
// (undocumented)
diff --git a/plugins/kubernetes-common/package.json b/plugins/kubernetes-common/package.json
index 2d83db84d4..4eb4a02bf9 100644
--- a/plugins/kubernetes-common/package.json
+++ b/plugins/kubernetes-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-kubernetes-common",
"description": "Common functionalities for kubernetes, to be shared between kubernetes and kubernetes-backend plugin",
- "version": "0.6.1",
+ "version": "0.6.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -39,6 +39,7 @@
},
"dependencies": {
"@backstage/catalog-model": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
"@kubernetes/client-node": "0.18.1"
},
"devDependencies": {
diff --git a/plugins/kubernetes-common/src/index.ts b/plugins/kubernetes-common/src/index.ts
index ade73e4f79..d5cf8dcf61 100644
--- a/plugins/kubernetes-common/src/index.ts
+++ b/plugins/kubernetes-common/src/index.ts
@@ -22,3 +22,7 @@
export * from './types';
export * from './catalog-entity-constants';
+export {
+ kubernetesProxyPermission,
+ kubernetesPermissions,
+} from './permissions';
diff --git a/plugins/kubernetes-common/src/permissions.ts b/plugins/kubernetes-common/src/permissions.ts
new file mode 100644
index 0000000000..d3ccbe8171
--- /dev/null
+++ b/plugins/kubernetes-common/src/permissions.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { createPermission } from '@backstage/plugin-permission-common';
+
+/** This permission is used to check access to the proxy endpoint
+ * @public
+ */
+export const kubernetesProxyPermission = createPermission({
+ name: 'kubernetes.proxy',
+ attributes: {},
+});
+
+/**
+ * List of all Kubernetes permissions.
+ * @public
+ */
+export const kubernetesPermissions = [kubernetesProxyPermission];
diff --git a/plugins/kubernetes/CHANGELOG.md b/plugins/kubernetes/CHANGELOG.md
index 02dbba634d..cc8131b399 100644
--- a/plugins/kubernetes/CHANGELOG.md
+++ b/plugins/kubernetes/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-kubernetes
+## 0.7.10-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/plugin-kubernetes-common@0.6.2-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.7.9
### Patch Changes
diff --git a/plugins/kubernetes/package.json b/plugins/kubernetes/package.json
index b22d50681d..f674402545 100644
--- a/plugins/kubernetes/package.json
+++ b/plugins/kubernetes/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-kubernetes",
"description": "A Backstage plugin that integrates towards Kubernetes",
- "version": "0.7.9",
+ "version": "0.7.10-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -43,7 +43,7 @@
"@kubernetes/client-node": "0.18.1",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@types/react": "^16.13.1 || ^17.0.0",
"cronstrue": "^2.2.0",
"js-yaml": "^4.0.0",
diff --git a/plugins/lighthouse-backend/CHANGELOG.md b/plugins/lighthouse-backend/CHANGELOG.md
index 860493f57b..7320fc7b77 100644
--- a/plugins/lighthouse-backend/CHANGELOG.md
+++ b/plugins/lighthouse-backend/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-lighthouse-backend
+## 0.1.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/types@1.0.2
+ - @backstage/plugin-lighthouse-common@0.1.1
+
## 0.1.1
### Patch Changes
diff --git a/plugins/lighthouse-backend/package.json b/plugins/lighthouse-backend/package.json
index d13e1487e9..523f3837f5 100644
--- a/plugins/lighthouse-backend/package.json
+++ b/plugins/lighthouse-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-lighthouse-backend",
"description": "Backend functionalities for lighthouse",
- "version": "0.1.1",
+ "version": "0.1.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/lighthouse/CHANGELOG.md b/plugins/lighthouse/CHANGELOG.md
index df404fb220..e662f3d81d 100644
--- a/plugins/lighthouse/CHANGELOG.md
+++ b/plugins/lighthouse/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-lighthouse
+## 0.4.2-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-lighthouse-common@0.1.1
+
## 0.4.1
### Patch Changes
diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json
index e9c70378e5..c46fcffe57 100644
--- a/plugins/lighthouse/package.json
+++ b/plugins/lighthouse/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-lighthouse",
"description": "A Backstage plugin that integrates towards Lighthouse",
- "version": "0.4.1",
+ "version": "0.4.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -42,7 +42,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/linguist-backend/CHANGELOG.md b/plugins/linguist-backend/CHANGELOG.md
index e94a176158..5f38af055f 100644
--- a/plugins/linguist-backend/CHANGELOG.md
+++ b/plugins/linguist-backend/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-linguist-backend
+## 0.2.1-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-linguist-common@0.1.0
+
## 0.2.0
### Minor Changes
diff --git a/plugins/linguist-backend/package.json b/plugins/linguist-backend/package.json
index 755b1b959a..d9f971d178 100644
--- a/plugins/linguist-backend/package.json
+++ b/plugins/linguist-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-linguist-backend",
- "version": "0.2.0",
+ "version": "0.2.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/linguist/CHANGELOG.md b/plugins/linguist/CHANGELOG.md
index d289a06299..1670d80f97 100644
--- a/plugins/linguist/CHANGELOG.md
+++ b/plugins/linguist/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-linguist
+## 0.1.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-linguist-common@0.1.0
+
## 0.1.1
### Patch Changes
diff --git a/plugins/linguist/package.json b/plugins/linguist/package.json
index f589b26bd3..0a36244b81 100644
--- a/plugins/linguist/package.json
+++ b/plugins/linguist/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-linguist",
- "version": "0.1.1",
+ "version": "0.1.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -37,7 +37,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "^4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"luxon": "^2.0.2",
"react-use": "^17.2.4",
"slugify": "^1.6.4"
diff --git a/plugins/microsoft-calendar/CHANGELOG.md b/plugins/microsoft-calendar/CHANGELOG.md
index 93d51a338c..1cb17d787b 100644
--- a/plugins/microsoft-calendar/CHANGELOG.md
+++ b/plugins/microsoft-calendar/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-microsoft-calendar
+## 0.1.2-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+
## 0.1.1
### Patch Changes
diff --git a/plugins/microsoft-calendar/README.md b/plugins/microsoft-calendar/README.md
index 219a7cf2dc..92421046e2 100644
--- a/plugins/microsoft-calendar/README.md
+++ b/plugins/microsoft-calendar/README.md
@@ -1,7 +1,6 @@
# Microsoft-Calendar Plugin
-Welcome to the Microsoft calendar plugin!
-_This plugin was created through the Backstage CLI_
+This plugin is contributed by [StatusNeo](https://statusneo.com/)
## Features
diff --git a/plugins/microsoft-calendar/package.json b/plugins/microsoft-calendar/package.json
index cb125480f2..8170c7847f 100644
--- a/plugins/microsoft-calendar/package.json
+++ b/plugins/microsoft-calendar/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-microsoft-calendar",
- "version": "0.1.1",
+ "version": "0.1.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -13,10 +13,24 @@
"role": "frontend-plugin"
},
"author": {
- "name": "Abhay Soni",
- "email": "abhaysoni.developer@gmail.com",
- "url": "https://github.com/Abhay-soni-developer"
+ "name": "Statusneo",
+ "email": "reach@statusneo.com",
+ "url": "https://statusneo.com/"
},
+ "contributors": [
+ {
+ "email": "abhaysoni.developer@gmail.com",
+ "url": "https://github.com/Abhay-soni-developer",
+ "name": "abhaysoni"
+ }
+ ],
+ "maintainers": [
+ {
+ "email": "abhaysoni.developer@gmail.com",
+ "url": "https://github.com/Abhay-soni-developer",
+ "name": "abhaysoni"
+ }
+ ],
"scripts": {
"start": "backstage-cli package start",
"build": "backstage-cli package build",
diff --git a/plugins/newrelic-dashboard/CHANGELOG.md b/plugins/newrelic-dashboard/CHANGELOG.md
index 44122710e5..81112322fb 100644
--- a/plugins/newrelic-dashboard/CHANGELOG.md
+++ b/plugins/newrelic-dashboard/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-newrelic-dashboard
+## 0.2.10-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+
## 0.2.9
### Patch Changes
diff --git a/plugins/newrelic-dashboard/package.json b/plugins/newrelic-dashboard/package.json
index 83aa0811b6..8718cbfe70 100644
--- a/plugins/newrelic-dashboard/package.json
+++ b/plugins/newrelic-dashboard/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-newrelic-dashboard",
- "version": "0.2.9",
+ "version": "0.2.10-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"@backstage/plugin-catalog-react": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"devDependencies": {
diff --git a/plugins/newrelic/CHANGELOG.md b/plugins/newrelic/CHANGELOG.md
index d241da2d28..60ccace53b 100644
--- a/plugins/newrelic/CHANGELOG.md
+++ b/plugins/newrelic/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-newrelic
+## 0.3.35-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/theme@0.2.18
+
## 0.3.34
### Patch Changes
diff --git a/plugins/newrelic/package.json b/plugins/newrelic/package.json
index 08d4ad0970..89823c559b 100644
--- a/plugins/newrelic/package.json
+++ b/plugins/newrelic/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-newrelic",
"description": "A Backstage plugin that integrates towards New Relic",
- "version": "0.3.34",
+ "version": "0.3.35-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -38,7 +38,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/octopus-deploy/CHANGELOG.md b/plugins/octopus-deploy/CHANGELOG.md
index 8ab8d174db..8b39119ce2 100644
--- a/plugins/octopus-deploy/CHANGELOG.md
+++ b/plugins/octopus-deploy/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-octopus-deploy
+## 0.1.1-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.1.0
### Minor Changes
diff --git a/plugins/octopus-deploy/package.json b/plugins/octopus-deploy/package.json
index 4cbe40b006..ce36c80678 100644
--- a/plugins/octopus-deploy/package.json
+++ b/plugins/octopus-deploy/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-octopus-deploy",
- "version": "0.1.0",
+ "version": "0.1.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.9.13",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/org-react/CHANGELOG.md b/plugins/org-react/CHANGELOG.md
index a8b59a4bc1..cdb4ef0b9e 100644
--- a/plugins/org-react/CHANGELOG.md
+++ b/plugins/org-react/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-org-react
+## 0.1.6-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.1.5
### Patch Changes
diff --git a/plugins/org-react/package.json b/plugins/org-react/package.json
index 027e34c391..6b48a9c381 100644
--- a/plugins/org-react/package.json
+++ b/plugins/org-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-org-react",
- "version": "0.1.5",
+ "version": "0.1.6-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -38,7 +38,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "^4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-use": "^17.2.4"
},
"peerDependencies": {
diff --git a/plugins/org/CHANGELOG.md b/plugins/org/CHANGELOG.md
index 4251195733..d4db2bcc61 100644
--- a/plugins/org/CHANGELOG.md
+++ b/plugins/org/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-org
+## 0.6.7-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.6.6
### Patch Changes
diff --git a/plugins/org/package.json b/plugins/org/package.json
index 1e948703c5..43a102e1dd 100644
--- a/plugins/org/package.json
+++ b/plugins/org/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-org",
"description": "A Backstage plugin that helps you create entity pages for your organization",
- "version": "0.6.6",
+ "version": "0.6.7-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -35,7 +35,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"p-limit": "^3.1.0",
"pluralize": "^8.0.0",
"qs": "^6.10.1",
diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx
index f183ab4d8e..526d7c9c4c 100644
--- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx
+++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.stories.tsx
@@ -128,3 +128,33 @@ export const ExtraDetails = () => (
);
+
+const groupWithTitle: GroupEntity = {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Group',
+ metadata: {
+ name: 'team-a',
+ description: 'Team A description',
+ title: 'Team A title',
+ },
+ spec: {
+ profile: {
+ email: 'team-a@example.com',
+ picture:
+ 'https://avatars.dicebear.com/api/identicon/team-a@example.com.svg?background=%23fff&margin=25',
+ },
+ type: 'group',
+ children: [],
+ },
+ relations: [dummyDepartment],
+};
+
+export const UseGroupTitle = () => (
+
+
+
+
+
+
+
+);
diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx
index 5e795b2710..e8f06e49b3 100644
--- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx
+++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx
@@ -81,7 +81,7 @@ export const GroupProfileCard = (props: {
}
const {
- metadata: { name, description, annotations, links },
+ metadata: { name, description, title, annotations, links },
spec: { profile },
} = group;
@@ -99,7 +99,7 @@ export const GroupProfileCard = (props: {
const entityMetadataEditUrl =
group.metadata.annotations?.[ANNOTATION_EDIT_URL];
- const displayName = profile?.displayName ?? name;
+ const displayName = profile?.displayName ?? title ?? name;
const emailHref = profile?.email ? `mailto:${profile.email}` : '#';
const infoCardAction = entityMetadataEditUrl ? (
{
describe('applyConditions', () => {
- let server: SetupServerApi;
+ let server: SetupServer;
const mockConditions: PermissionCriteria = {
not: {
diff --git a/plugins/permission-node/CHANGELOG.md b/plugins/permission-node/CHANGELOG.md
index 472fa004a1..3bd2cf06c4 100644
--- a/plugins/permission-node/CHANGELOG.md
+++ b/plugins/permission-node/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-permission-node
+## 0.7.7-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-permission-common@0.7.4
+
## 0.7.6
### Patch Changes
diff --git a/plugins/permission-node/api-report.md b/plugins/permission-node/api-report.md
index f89ff0a8d5..b3d06efdc7 100644
--- a/plugins/permission-node/api-report.md
+++ b/plugins/permission-node/api-report.md
@@ -73,6 +73,11 @@ export type ConditionTransformer = (
conditions: PermissionCriteria,
) => PermissionCriteria;
+// @public
+export const createConditionAuthorizer: (
+ rules: PermissionRule[],
+) => (decision: PolicyDecision, resource: TResource | undefined) => boolean;
+
// @public
export const createConditionExports: <
TResourceType extends string,
diff --git a/plugins/permission-node/package.json b/plugins/permission-node/package.json
index 1473de80b2..e4b2229bbc 100644
--- a/plugins/permission-node/package.json
+++ b/plugins/permission-node/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-permission-node",
"description": "Common permission and authorization utilities for backend plugins",
- "version": "0.7.6",
+ "version": "0.7.7-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts
index 1a7462665e..6e28c5ddc4 100644
--- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts
+++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.test.ts
@@ -25,6 +25,7 @@ import { z } from 'zod';
import {
createPermissionIntegrationRouter,
CreatePermissionIntegrationRouterResourceOptions,
+ createConditionAuthorizer,
} from './createPermissionIntegrationRouter';
import { createPermissionRule } from './createPermissionRule';
@@ -33,6 +34,9 @@ const testPermission: Permission = createPermission({
attributes: {},
});
+const mockTestRule1Apply = jest
+ .fn()
+ .mockImplementation((_resource: any, _params) => true);
const testRule1 = createPermissionRule({
name: 'test-rule-1',
description: 'Test rule 1',
@@ -41,15 +45,18 @@ const testRule1 = createPermissionRule({
foo: z.string(),
bar: z.number().describe('bar'),
}),
- apply: (_resource: any, _params) => true,
+ apply: mockTestRule1Apply,
toQuery: _params => ({}),
});
+const mockTestRule2Apply = jest
+ .fn()
+ .mockImplementation((_resource: any) => false);
const testRule2 = createPermissionRule({
name: 'test-rule-2',
description: 'Test rule 2',
resourceType: 'test-resource',
- apply: (_resource: any) => false,
+ apply: mockTestRule2Apply,
toQuery: () => ({}),
});
@@ -625,3 +632,72 @@ describe('createPermissionIntegrationRouter', () => {
});
});
});
+
+describe('createConditionAuthorizer', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should return true in case of allowed decision', () => {
+ const isAuthorized = createConditionAuthorizer([testRule1, testRule2]);
+ expect(
+ isAuthorized(
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ {},
+ ),
+ ).toBe(true);
+
+ expect(mockTestRule1Apply).not.toHaveBeenCalled();
+ expect(mockTestRule2Apply).not.toHaveBeenCalled();
+ });
+ it('should return false in case of denied decision', () => {
+ const isAuthorized = createConditionAuthorizer([testRule1, testRule2]);
+ expect(
+ isAuthorized(
+ {
+ result: AuthorizeResult.DENY,
+ },
+ {},
+ ),
+ ).toBe(false);
+
+ expect(mockTestRule1Apply).not.toHaveBeenCalled();
+ expect(mockTestRule2Apply).not.toHaveBeenCalled();
+ });
+
+ it('should apply conditions to a resource in case of conditional decision', () => {
+ const isAuthorized = createConditionAuthorizer([testRule1, testRule2]);
+ expect(
+ isAuthorized(
+ {
+ pluginId: 'plugin',
+ resourceType: 'test-resource',
+ result: AuthorizeResult.CONDITIONAL,
+ conditions: {
+ allOf: [
+ {
+ rule: 'test-rule-1',
+ resourceType: 'test-resource',
+ params: {
+ foo: 'a',
+ bar: 1,
+ },
+ },
+ {
+ rule: 'test-rule-2',
+ resourceType: 'test-resource',
+ params: {},
+ },
+ ],
+ },
+ },
+ {},
+ ),
+ ).toBe(false);
+
+ expect(mockTestRule1Apply).toHaveBeenCalledTimes(1);
+ expect(mockTestRule2Apply).toHaveBeenCalledTimes(1);
+ });
+});
diff --git a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts
index 087de23198..850123d55e 100644
--- a/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts
+++ b/plugins/permission-node/src/integration/createPermissionIntegrationRouter.ts
@@ -27,6 +27,7 @@ import {
Permission,
PermissionCondition,
PermissionCriteria,
+ PolicyDecision,
} from '@backstage/plugin-permission-common';
import { PermissionRule } from '../types';
import {
@@ -160,6 +161,30 @@ const applyConditions = (
return rule.apply(resource, criteria.params ?? {});
};
+/**
+
+ * Takes some permission conditions and returns a definitive authorization result
+ * on the resource to which they apply.
+ *
+ * @public
+ */
+export const createConditionAuthorizer = (
+ rules: PermissionRule[],
+) => {
+ const getRule = createGetRule(rules);
+
+ return (
+ decision: PolicyDecision,
+ resource: TResource | undefined,
+ ): boolean => {
+ if (decision.result === AuthorizeResult.CONDITIONAL) {
+ return applyConditions(decision.conditions, resource, getRule);
+ }
+
+ return decision.result === AuthorizeResult.ALLOW;
+ };
+};
+
/**
* Options for creating a permission integration router specific
* for a particular resource type.
diff --git a/plugins/playlist-backend/CHANGELOG.md b/plugins/playlist-backend/CHANGELOG.md
index 195cdb1f50..51e419f0fb 100644
--- a/plugins/playlist-backend/CHANGELOG.md
+++ b/plugins/playlist-backend/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-playlist-backend
+## 0.2.7-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-permission-common@0.7.4
+ - @backstage/plugin-permission-node@0.7.7-next.0
+ - @backstage/plugin-playlist-common@0.1.5
+
## 0.2.6
### Patch Changes
diff --git a/plugins/playlist-backend/package.json b/plugins/playlist-backend/package.json
index 90e5813247..870284f364 100644
--- a/plugins/playlist-backend/package.json
+++ b/plugins/playlist-backend/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-playlist-backend",
- "version": "0.2.6",
+ "version": "0.2.7-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/playlist/CHANGELOG.md b/plugins/playlist/CHANGELOG.md
index 43a7ff4e4d..6e09f280bd 100644
--- a/plugins/playlist/CHANGELOG.md
+++ b/plugins/playlist/CHANGELOG.md
@@ -1,5 +1,22 @@
# @backstage/plugin-playlist
+## 0.1.8-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-search-react@1.5.2-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/plugin-permission-react@0.4.11
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-permission-common@0.7.4
+ - @backstage/plugin-playlist-common@0.1.5
+
## 0.1.7
### Patch Changes
diff --git a/plugins/playlist/package.json b/plugins/playlist/package.json
index 59cde3f246..4bf202bdba 100644
--- a/plugins/playlist/package.json
+++ b/plugins/playlist/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-playlist",
- "version": "0.1.7",
+ "version": "0.1.8-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -40,7 +40,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "^4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"lodash": "^4.17.21",
"qs": "^6.9.4",
"react-hook-form": "^7.13.0",
diff --git a/plugins/proxy-backend/CHANGELOG.md b/plugins/proxy-backend/CHANGELOG.md
index 8c4ef92ea5..58d68eceab 100644
--- a/plugins/proxy-backend/CHANGELOG.md
+++ b/plugins/proxy-backend/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-proxy-backend
+## 0.2.38-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-plugin-api@0.5.1-next.0
+
## 0.2.37
### Patch Changes
diff --git a/plugins/proxy-backend/package.json b/plugins/proxy-backend/package.json
index 6b9c115dbb..11caccbff3 100644
--- a/plugins/proxy-backend/package.json
+++ b/plugins/proxy-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-proxy-backend",
"description": "A Backstage backend plugin that helps you set up proxy endpoints in the backend",
- "version": "0.2.37",
+ "version": "0.2.38-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/rollbar-backend/CHANGELOG.md b/plugins/rollbar-backend/CHANGELOG.md
index f69c395c83..f7f91fea98 100644
--- a/plugins/rollbar-backend/CHANGELOG.md
+++ b/plugins/rollbar-backend/CHANGELOG.md
@@ -1,5 +1,13 @@
# @backstage/plugin-rollbar-backend
+## 0.1.41-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+
## 0.1.40
### Patch Changes
diff --git a/plugins/rollbar-backend/package.json b/plugins/rollbar-backend/package.json
index 0bba2d3888..bec3819121 100644
--- a/plugins/rollbar-backend/package.json
+++ b/plugins/rollbar-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-rollbar-backend",
"description": "A Backstage backend plugin that integrates towards Rollbar",
- "version": "0.1.40",
+ "version": "0.1.41-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/rollbar/CHANGELOG.md b/plugins/rollbar/CHANGELOG.md
index 2693bf4b5b..24e85c8e77 100644
--- a/plugins/rollbar/CHANGELOG.md
+++ b/plugins/rollbar/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-rollbar
+## 0.4.17-next.0
+
+### Patch Changes
+
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/theme@0.2.18
+
## 0.4.16
### Patch Changes
diff --git a/plugins/rollbar/package.json b/plugins/rollbar/package.json
index 9b46d91db6..4abd1b90f2 100644
--- a/plugins/rollbar/package.json
+++ b/plugins/rollbar/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-rollbar",
"description": "A Backstage plugin that integrates towards Rollbar",
- "version": "0.4.16",
+ "version": "0.4.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -40,7 +40,7 @@
"@backstage/theme": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"lodash": "^4.17.21",
"react-sparklines": "^1.7.0",
"react-use": "^17.2.4"
diff --git a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md
index 284e381553..b03b35dc72 100644
--- a/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md
+++ b/plugins/scaffolder-backend-module-cookiecutter/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-scaffolder-backend-module-cookiecutter
+## 0.2.19-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.12.1-next.0
+ - @backstage/plugin-scaffolder-node@0.1.2-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.2.18
### Patch Changes
diff --git a/plugins/scaffolder-backend-module-cookiecutter/package.json b/plugins/scaffolder-backend-module-cookiecutter/package.json
index da8f92da5c..c78cdae972 100644
--- a/plugins/scaffolder-backend-module-cookiecutter/package.json
+++ b/plugins/scaffolder-backend-module-cookiecutter/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-cookiecutter",
"description": "A module for the scaffolder backend that lets you template projects using cookiecutter",
- "version": "0.2.18",
+ "version": "0.2.19-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/scaffolder-backend-module-gitlab/.eslintrc.js b/plugins/scaffolder-backend-module-gitlab/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md
new file mode 100644
index 0000000000..9bd33f6aae
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/CHANGELOG.md
@@ -0,0 +1,15 @@
+# @backstage/plugin-scaffolder-backend-module-gitlab
+
+## 0.1.0-next.0
+
+### Minor Changes
+
+- 1ad400bb2de: Add Gitlab Scaffolder Plugin
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-node@0.1.2-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/errors@1.1.5
diff --git a/plugins/scaffolder-backend-module-gitlab/README.md b/plugins/scaffolder-backend-module-gitlab/README.md
new file mode 100644
index 0000000000..35353cf9c1
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/README.md
@@ -0,0 +1,162 @@
+# scaffolder-backend-module-gitlab
+
+Welcome to the Gitlab Module for Scaffolder.
+
+Here you can find all Gitlab related features to improve your scaffolder:
+
+## Getting started
+
+## From your Backstage root directory
+
+```bash
+# From your Backstage root directory
+yarn add --cwd packages/backend @backstage/plugin-scaffolder-backend-module-gitlab
+```
+
+Configure the action:
+(you can check the [docs](https://backstage.io/docs/features/software-templates/writing-custom-actions#registering-custom-actions) to see all options):
+
+```typescript
+// packages/backend/src/plugins/scaffolder.ts
+
+import {
+ createGitlabProjectAccessTokenAction,
+ createGitlabProjectAccessTokenAction,
+ createGitlabProjectDeployTokenAction,
+} from '@backstage/plugin-scaffolder-backend-module-gitlab';
+
+// Create BuiltIn Actions
+const builtInActions = createBuiltinActions({
+ integrations,
+ catalogClient,
+ config: env.config,
+ reader: env.reader,
+});
+
+// Add Gitlab Actions
+const actions = [
+ ...builtInActions,
+ createGitlabProjectAccessTokenAction({
+ integrations: integrations,
+ }),
+ createGitlabProjectAccessTokenAction({
+ integrations: integrations,
+ }),
+ createGitlabProjectDeployTokenAction({
+ integrations: integrations,
+ }),
+];
+
+// Create Scaffolder Router
+return await createRouter({
+ containerRunner,
+ catalogClient,
+ actions,
+ logger: env.logger,
+ config: env.config,
+ database: env.database,
+ reader: env.reader,
+});
+```
+
+After that you can use the action in your template:
+
+```yaml
+apiVersion: scaffolder.backstage.io/v1beta3
+kind: Template
+metadata:
+ name: gitlab-demo
+ title: Gitlab DEMO
+ description: Scaffolder Gitlab Demo
+spec:
+ owner: backstage/techdocs-core
+ type: service
+
+ parameters:
+ - title: Fill in some steps
+ required:
+ - name
+ properties:
+ name:
+ title: Name
+ type: string
+ description: Unique name of the component
+ ui:autofocus: true
+ ui:options:
+ rows: 5
+ - title: Choose a location
+ required:
+ - repoUrl
+ properties:
+ repoUrl:
+ title: Repository Location
+ type: string
+ ui:field: RepoUrlPicker
+ ui:options:
+ allowedHosts:
+ - gitlab.com
+
+ steps:
+ - id: fetch
+ name: Fetch
+ action: fetch:template
+ input:
+ url: https://github.com/TEMPLATE
+ values:
+ name: ${{ parameters.name }}
+
+ - id: publish
+ name: Publish
+ action: publish:gitlab
+ input:
+ description: This is ${{ parameters.name }}
+ repoUrl: ${{ parameters.repoUrl }}
+ sourcePath: pimcore
+ defaultBranch: main
+
+ - id: gitlab-deploy-token
+ name: Create Deploy Token
+ action: gitlab:projectDeployToken:create
+ input:
+ repoUrl: ${{ parameters.repoUrl }}
+ projectId: "${{ steps['publish'].output.projectId }}"
+ name: ${{ parameters.name }}-secret
+ username: ${{ parameters.name }}-secret
+ scopes: ['read_registry']
+
+ - id: gitlab-access-token
+ name: Gitlab Project Access Token
+ action: gitlab:projectAccessToken:create
+ input:
+ repoUrl: ${{ parameters.repoUrl }}
+ projectId: "${{ steps['publish-manifest'].output.projectId }}"
+ name: ${{ parameters.name }}-access-token
+ accessLevel: 40
+ scopes: ['read_repository', 'write_repository']
+
+ - id: gitlab-project-variable
+ name: Gitlab Project Variable
+ action: gitlab:projectVariable:create
+ input:
+ repoUrl: ${{ parameters.repoUrl }}
+ projectId: "${{ steps['publish'].output.projectId }}"
+ key: 'VARIABLE_NAME'
+ value: "${{ steps['gitlab-access-token'].output.access_token }}"
+ variableType: 'env_var'
+ masked: true
+ variableProtected: false
+ raw: false
+ environmentScope: '*'
+
+ - id: register
+ name: Register
+ action: catalog:register
+ input:
+ repoContentsUrl: ${{ steps['publish'].output.repoContentsUrl }}
+ catalogInfoPath: '/catalog-info.yaml'
+
+ output:
+ links:
+ - title: Repository
+ url: ${{ steps['publish'].output.remoteUrl }}
+```
diff --git a/plugins/scaffolder-backend-module-gitlab/api-report.md b/plugins/scaffolder-backend-module-gitlab/api-report.md
new file mode 100644
index 0000000000..6db732fdcc
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/api-report.md
@@ -0,0 +1,48 @@
+## API Report File for "@backstage/plugin-scaffolder-backend-module-gitlab"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { ScmIntegrationRegistry } from '@backstage/integration';
+import { TemplateAction } from '@backstage/plugin-scaffolder-node';
+
+// @public
+export const createGitlabProjectAccessTokenAction: (options: {
+ integrations: ScmIntegrationRegistry;
+}) => TemplateAction<{
+ repoUrl: string;
+ projectId: string | number;
+ name: string;
+ accessLevel: number;
+ scopes: string[];
+ token?: string | undefined;
+}>;
+
+// @public
+export const createGitlabProjectDeployTokenAction: (options: {
+ integrations: ScmIntegrationRegistry;
+}) => TemplateAction<{
+ repoUrl: string;
+ projectId: string | number;
+ name: string;
+ username: string;
+ scopes: string[];
+ token?: string | undefined;
+}>;
+
+// @public
+export const createGitlabProjectVariableAction: (options: {
+ integrations: ScmIntegrationRegistry;
+}) => TemplateAction<{
+ repoUrl: string;
+ projectId: string | number;
+ key: string;
+ value: string;
+ variableType: string;
+ variableProtected: boolean;
+ masked: boolean;
+ raw: boolean;
+ environmentScope: string;
+ token?: string | undefined;
+}>;
+```
diff --git a/plugins/scaffolder-backend-module-gitlab/package.json b/plugins/scaffolder-backend-module-gitlab/package.json
new file mode 100644
index 0000000000..e870ba57a0
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/package.json
@@ -0,0 +1,48 @@
+{
+ "name": "@backstage/plugin-scaffolder-backend-module-gitlab",
+ "version": "0.1.0-next.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public",
+ "main": "dist/index.esm.js",
+ "types": "dist/index.d.ts"
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "homepage": "https://backstage.io",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/backstage/backstage",
+ "directory": "plugins/scaffolder-backend-module-gitlab"
+ },
+ "keywords": [
+ "backstage"
+ ],
+ "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/config": "workspace:^",
+ "@backstage/errors": "workspace:^",
+ "@backstage/integration": "workspace:^",
+ "@backstage/plugin-scaffolder-node": "workspace:^",
+ "@gitbeaker/node": "^35.8.0"
+ },
+ "devDependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "@backstage/core-app-api": "workspace:^"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts
new file mode 100644
index 0000000000..0f39e3bc33
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectAccessTokenAction.ts
@@ -0,0 +1,112 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
+import { ScmIntegrationRegistry } from '@backstage/integration';
+import { getToken } from '../util';
+
+/**
+ * Creates a `gitlab:create-project-access-token` Scaffolder action.
+ *
+ * @param options - Templating configuration.
+ * @public
+ */
+export const createGitlabProjectAccessTokenAction = (options: {
+ integrations: ScmIntegrationRegistry;
+}) => {
+ const { integrations } = options;
+ return createTemplateAction<{
+ repoUrl: string;
+ projectId: string | number;
+ name: string;
+ accessLevel: number;
+ scopes: string[];
+ token?: string;
+ }>({
+ id: 'gitlab:projectAccessToken:create',
+ schema: {
+ input: {
+ required: ['projectId', 'repoUrl'],
+ type: 'object',
+ properties: {
+ repoUrl: {
+ title: 'Repository Location',
+ type: 'string',
+ },
+ projectId: {
+ title: 'Project ID',
+ type: ['string', 'number'],
+ },
+ name: {
+ title: 'Deploy Token Name',
+ type: 'string',
+ },
+ accessLevel: {
+ title: 'Access Level of the Token',
+ type: 'number',
+ },
+ scopes: {
+ title: 'Scopes',
+ type: 'array',
+ },
+ token: {
+ title: 'Authentication Token',
+ type: 'string',
+ description: 'The token to use for authorization to GitLab',
+ },
+ },
+ },
+ output: {
+ type: 'object',
+ properties: {
+ access_token: {
+ title: 'Access Token',
+ type: 'string',
+ },
+ },
+ },
+ },
+ async handler(ctx) {
+ ctx.logger.info(`Creating Token for Project "${ctx.input.projectId}"`);
+ const { repoUrl, projectId, name, accessLevel, scopes } = ctx.input;
+ const { token, integrationConfig } = getToken(
+ repoUrl,
+ ctx.input.token,
+ integrations,
+ );
+
+ const response = await fetch(
+ `${integrationConfig.config.baseUrl}/api/v4/projects/${projectId}/access_tokens`,
+ {
+ method: 'POST', // *GET, POST, PUT, DELETE, etc.
+ headers: {
+ 'PRIVATE-TOKEN': token,
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ name: name,
+ scopes: scopes,
+ access_level: accessLevel,
+ }),
+ },
+ );
+
+ const result = await response.json();
+
+ ctx.output('access_token', result.token);
+ },
+ });
+};
diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts
new file mode 100644
index 0000000000..1d3ae804c1
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.test.ts
@@ -0,0 +1,101 @@
+/*
+ * 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 { createGitlabProjectDeployTokenAction } from './createGitlabProjectDeployTokenAction';
+import { ScmIntegrations } from '@backstage/integration';
+import { ConfigReader } from '@backstage/config';
+import { getVoidLogger } from '@backstage/backend-common';
+import { PassThrough } from 'stream';
+
+const mockGitlabClient = {
+ ProjectDeployTokens: {
+ add: jest.fn(),
+ },
+};
+jest.mock('@gitbeaker/node', () => ({
+ Gitlab: class {
+ constructor() {
+ return mockGitlabClient;
+ }
+ },
+}));
+
+describe('gitlab:create-deploy-token', () => {
+ const config = new ConfigReader({
+ integrations: {
+ gitlab: [
+ {
+ host: 'gitlab.com',
+ token: 'tokenlols',
+ apiBaseUrl: 'https://api.gitlab.com',
+ },
+ {
+ host: 'hosted.gitlab.com',
+ apiBaseUrl: 'https://api.hosted.gitlab.com',
+ },
+ ],
+ },
+ });
+
+ const integrations = ScmIntegrations.fromConfig(config);
+ const action = createGitlabProjectDeployTokenAction({ integrations });
+ const mockContext = {
+ input: {
+ repoUrl: 'gitlab.com?repo=repo&owner=owner',
+ projectId: '123',
+ name: 'tokenname',
+ username: 'tokenuser',
+ scopes: ['read_repository'],
+ },
+ workspacePath: 'lol',
+ logger: getVoidLogger(),
+ logStream: new PassThrough(),
+ output: jest.fn(),
+ createTemporaryDirectory: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should work when there is a token provided through ctx.input', async () => {
+ mockGitlabClient.ProjectDeployTokens.add.mockResolvedValue({
+ token: 'TOKEN',
+ username: 'User',
+ });
+
+ await action.handler({
+ ...mockContext,
+ input: {
+ repoUrl: 'hosted.gitlab.com?repo=bob&owner=owner',
+ projectId: '123',
+ name: 'tokenname',
+ username: 'tokenuser',
+ scopes: ['read_repository'],
+ },
+ });
+
+ expect(mockGitlabClient.ProjectDeployTokens.add).toHaveBeenCalledWith(
+ '123',
+ 'tokenname',
+ ['read_repository'],
+ { username: 'tokenuser' },
+ );
+
+ expect(mockContext.output).toHaveBeenCalledWith('deploy_token', 'TOKEN');
+ expect(mockContext.output).toHaveBeenCalledWith('user', 'User');
+ });
+});
diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts
new file mode 100644
index 0000000000..35a53c87e2
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectDeployTokenAction.ts
@@ -0,0 +1,120 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
+import { Gitlab } from '@gitbeaker/node';
+import { ScmIntegrationRegistry } from '@backstage/integration';
+import { DeployTokenScope } from '@gitbeaker/core/dist/types/templates/ResourceDeployTokens';
+import { getToken } from '../util';
+import { InputError } from '@backstage/errors';
+
+/**
+ * Creates a `gitlab:create-project-deploy-token` Scaffolder action.
+ *
+ * @param options - Templating configuration.
+ * @public
+ */
+export const createGitlabProjectDeployTokenAction = (options: {
+ integrations: ScmIntegrationRegistry;
+}) => {
+ const { integrations } = options;
+ return createTemplateAction<{
+ repoUrl: string;
+ projectId: string | number;
+ name: string;
+ username: string;
+ scopes: string[];
+ token?: string;
+ }>({
+ id: 'gitlab:projectDeployToken:create',
+ schema: {
+ input: {
+ required: ['projectId', 'repoUrl'],
+ type: 'object',
+ properties: {
+ repoUrl: {
+ title: 'Repository Location',
+ type: 'string',
+ },
+ projectId: {
+ title: 'Project ID',
+ type: ['string', 'number'],
+ },
+ name: {
+ title: 'Deploy Token Name',
+ type: 'string',
+ },
+ username: {
+ title: 'Deploy Token Username',
+ type: 'string',
+ },
+ scopes: {
+ title: 'Scopes',
+ type: 'array',
+ },
+ token: {
+ title: 'Authentication Token',
+ type: 'string',
+ description: 'The token to use for authorization to GitLab',
+ },
+ },
+ },
+ output: {
+ type: 'object',
+ properties: {
+ deploy_token: {
+ title: 'Deploy Token',
+ type: 'string',
+ },
+ user: {
+ title: 'User',
+ type: 'string',
+ },
+ },
+ },
+ },
+ async handler(ctx) {
+ ctx.logger.info(`Creating Token for Project "${ctx.input.projectId}"`);
+ const { repoUrl, projectId, name, username, scopes } = ctx.input;
+ const { token, integrationConfig } = getToken(
+ repoUrl,
+ ctx.input.token,
+ integrations,
+ );
+
+ const api = new Gitlab({
+ host: integrationConfig.config.baseUrl,
+ token: token,
+ });
+
+ const deployToken = await api.ProjectDeployTokens.add(
+ projectId,
+ name,
+ scopes as DeployTokenScope[],
+ {
+ username: username,
+ },
+ );
+
+ if (!deployToken.hasOwnProperty('token')) {
+ throw new InputError(`No deploy_token given from gitlab instance`);
+ }
+
+ ctx.output('deploy_token', deployToken.token as string);
+ ctx.output('user', deployToken.username);
+ },
+ });
+};
diff --git a/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts
new file mode 100644
index 0000000000..847bbc2057
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/src/actions/createGitlabProjectVariableAction.ts
@@ -0,0 +1,139 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
+import { ScmIntegrationRegistry } from '@backstage/integration';
+import { getToken } from '../util';
+import { Gitlab } from '@gitbeaker/node';
+
+/**
+ * Creates a `gitlab:create-project-variable` Scaffolder action.
+ *
+ * @param options - Templating configuration.
+ * @public
+ */
+export const createGitlabProjectVariableAction = (options: {
+ integrations: ScmIntegrationRegistry;
+}) => {
+ const { integrations } = options;
+ return createTemplateAction<{
+ repoUrl: string;
+ projectId: string | number;
+ key: string;
+ value: string;
+ variableType: string;
+ variableProtected: boolean;
+ masked: boolean;
+ raw: boolean;
+ environmentScope: string;
+ token?: string;
+ }>({
+ id: 'gitlab:projectVariable:create',
+ schema: {
+ input: {
+ required: [
+ 'repoUrl',
+ 'projectId',
+ 'key',
+ 'value',
+ 'variableType',
+ 'variableProtected',
+ 'masked',
+ 'raw',
+ 'environmentScope',
+ ],
+ type: 'object',
+ properties: {
+ repoUrl: {
+ title: 'Repository Location',
+ type: 'string',
+ },
+ projectId: {
+ title: 'Project ID',
+ type: ['string', 'number'],
+ },
+ key: {
+ title:
+ 'The key of a variable; must have no more than 255 characters; only A-Z, a-z, 0-9, and _ are allowed',
+ type: 'string',
+ },
+ value: {
+ title: 'The value of a variable',
+ type: 'string',
+ },
+ variableType: {
+ title: 'Variable Type (env_var or file)',
+ type: 'string',
+ },
+ variableProtected: {
+ title: 'Whether the variable is protected. Default: false',
+ type: 'boolean',
+ },
+ masked: {
+ title: 'Whether the variable is masked. Default: false',
+ type: 'boolean',
+ },
+ raw: {
+ title: 'Whether the variable is expandable. Default: false',
+ type: 'boolean',
+ },
+ environmentScope: {
+ title: 'The environment_scope of the variable. Default: *',
+ type: 'string',
+ },
+ token: {
+ title: 'Authentication Token',
+ type: 'string',
+ description: 'The token to use for authorization to GitLab',
+ },
+ },
+ },
+ },
+ async handler(ctx) {
+ const {
+ repoUrl,
+ projectId,
+ key,
+ value,
+ variableType,
+ variableProtected,
+ masked,
+ raw,
+ environmentScope,
+ } = ctx.input;
+ const { token, integrationConfig } = getToken(
+ repoUrl,
+ ctx.input.token,
+ integrations,
+ );
+
+ const api = new Gitlab({
+ host: integrationConfig.config.baseUrl,
+ token: token,
+ });
+
+ await api.ProjectVariables.create(projectId, {
+ key: key,
+ value: value,
+ variable_type: variableType,
+ protected: variableProtected,
+ masked: masked,
+ raw: raw,
+ environment_scope: environmentScope,
+ });
+ },
+ });
+};
diff --git a/plugins/scaffolder-backend-module-gitlab/src/index.ts b/plugins/scaffolder-backend-module-gitlab/src/index.ts
new file mode 100644
index 0000000000..11209e19bd
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/src/index.ts
@@ -0,0 +1,24 @@
+/*
+ * 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.
+ */
+/**
+ * A module for the scaffolder backend that lets you create gitlab project access tokens or deploy tokens
+ *
+ * @packageDocumentation
+ */
+
+export * from './actions/createGitlabProjectDeployTokenAction';
+export * from './actions/createGitlabProjectAccessTokenAction';
+export * from './actions/createGitlabProjectVariableAction';
diff --git a/plugins/scaffolder-backend-module-gitlab/src/util.ts b/plugins/scaffolder-backend-module-gitlab/src/util.ts
new file mode 100644
index 0000000000..fc6667d6d3
--- /dev/null
+++ b/plugins/scaffolder-backend-module-gitlab/src/util.ts
@@ -0,0 +1,57 @@
+/*
+ * 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 { InputError } from '@backstage/errors';
+import {
+ GitLabIntegration,
+ ScmIntegrationRegistry,
+} from '@backstage/integration';
+
+export const parseRepoHost = (repoUrl: string): string => {
+ let parsed;
+ try {
+ parsed = new URL(`https://${repoUrl}`);
+ } catch (error) {
+ throw new InputError(
+ `Invalid repo URL passed to publisher, got ${repoUrl}, ${error}`,
+ );
+ }
+ return parsed.host;
+};
+
+export const getToken = (
+ repoUrl: string,
+ inputToken: string | null | undefined,
+ integrations: ScmIntegrationRegistry,
+): { token: string; integrationConfig: GitLabIntegration } => {
+ const host = parseRepoHost(repoUrl);
+ const integrationConfig = integrations.gitlab.byHost(host);
+
+ if (!integrationConfig) {
+ throw new InputError(
+ `No matching integration configuration for host ${host}, please check your integrations config`,
+ );
+ }
+
+ const token = inputToken || integrationConfig.config.token!;
+ const tokenType = inputToken ? 'oauthToken' : 'token';
+
+ if (tokenType === 'oauthToken') {
+ throw new InputError(`OAuth Token is currently not supported`);
+ }
+
+ return { token: token, integrationConfig: integrationConfig };
+};
diff --git a/plugins/scaffolder-backend-module-rails/CHANGELOG.md b/plugins/scaffolder-backend-module-rails/CHANGELOG.md
index 457cef36bb..bd3509dbd0 100644
--- a/plugins/scaffolder-backend-module-rails/CHANGELOG.md
+++ b/plugins/scaffolder-backend-module-rails/CHANGELOG.md
@@ -1,5 +1,18 @@
# @backstage/plugin-scaffolder-backend-module-rails
+## 0.4.12-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-backend@1.12.1-next.0
+ - @backstage/plugin-scaffolder-node@0.1.2-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+
## 0.4.11
### Patch Changes
diff --git a/plugins/scaffolder-backend-module-rails/package.json b/plugins/scaffolder-backend-module-rails/package.json
index 96da1120e8..aa1a178d9d 100644
--- a/plugins/scaffolder-backend-module-rails/package.json
+++ b/plugins/scaffolder-backend-module-rails/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-rails",
"description": "A module for the scaffolder backend that lets you template projects using Rails",
- "version": "0.4.11",
+ "version": "0.4.12-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md
index 5207cdec38..67772961ab 100644
--- a/plugins/scaffolder-backend-module-sentry/CHANGELOG.md
+++ b/plugins/scaffolder-backend-module-sentry/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-scaffolder-backend-module-sentry
+## 0.1.4-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-node@0.1.2-next.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+
## 0.1.3
### Patch Changes
diff --git a/plugins/scaffolder-backend-module-sentry/package.json b/plugins/scaffolder-backend-module-sentry/package.json
index bff7043171..6f04036110 100644
--- a/plugins/scaffolder-backend-module-sentry/package.json
+++ b/plugins/scaffolder-backend-module-sentry/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-sentry",
- "version": "0.1.3",
+ "version": "0.1.4-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md
index 36e965218d..2948bbc79b 100644
--- a/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md
+++ b/plugins/scaffolder-backend-module-yeoman/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-scaffolder-backend-module-yeoman
+## 0.2.17-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/plugin-scaffolder-node@0.1.2-next.0
+ - @backstage/config@1.0.7
+ - @backstage/types@1.0.2
+
## 0.2.16
### Patch Changes
diff --git a/plugins/scaffolder-backend-module-yeoman/package.json b/plugins/scaffolder-backend-module-yeoman/package.json
index 9aeee3ec51..50db3c38d0 100644
--- a/plugins/scaffolder-backend-module-yeoman/package.json
+++ b/plugins/scaffolder-backend-module-yeoman/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-scaffolder-backend-module-yeoman",
- "version": "0.2.16",
+ "version": "0.2.17-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/scaffolder-backend/CHANGELOG.md b/plugins/scaffolder-backend/CHANGELOG.md
index 0cd0e43f9d..89bfc23549 100644
--- a/plugins/scaffolder-backend/CHANGELOG.md
+++ b/plugins/scaffolder-backend/CHANGELOG.md
@@ -1,5 +1,29 @@
# @backstage/plugin-scaffolder-backend
+## 1.12.1-next.0
+
+### Patch Changes
+
+- e23abb37ec1: Rename output parameter `mergeRequestURL` of `publish:gitlab:merge-request` action to `mergeRequestUrl`.
+- e27ddc36dad: Added a possibility to cancel the running task (executing of a scaffolder template)
+- c9a0fdcd2c8: Fix deprecated types.
+- Updated dependencies
+ - @backstage/plugin-scaffolder-node@0.1.2-next.0
+ - @backstage/plugin-scaffolder-common@1.2.7-next.0
+ - @backstage/plugin-catalog-backend@1.8.1-next.0
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-catalog-common@1.0.12
+ - @backstage/plugin-catalog-node@1.3.5-next.0
+
## 1.12.0
### Minor Changes
diff --git a/plugins/scaffolder-backend/alpha-api-report.md b/plugins/scaffolder-backend/alpha-api-report.md
index 3199444c6b..2ecd2cc800 100644
--- a/plugins/scaffolder-backend/alpha-api-report.md
+++ b/plugins/scaffolder-backend/alpha-api-report.md
@@ -4,14 +4,43 @@
```ts
import { BackendFeature } from '@backstage/backend-plugin-api';
+import { ConditionalPolicyDecision } from '@backstage/plugin-permission-common';
+import { Conditions } from '@backstage/plugin-permission-node';
+import { PermissionCondition } from '@backstage/plugin-permission-common';
+import { PermissionCriteria } from '@backstage/plugin-permission-common';
+import { PermissionRule } from '@backstage/plugin-permission-node';
+import { PermissionRuleParams } from '@backstage/plugin-permission-common';
+import { ResourcePermission } from '@backstage/plugin-permission-common';
import { TaskBroker } from '@backstage/plugin-scaffolder-backend';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
+import { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateFilter } from '@backstage/plugin-scaffolder-backend';
import { TemplateGlobal } from '@backstage/plugin-scaffolder-backend';
+import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common';
// @alpha
export const catalogModuleTemplateKind: () => BackendFeature;
+// @alpha
+export const createScaffolderConditionalDecision: (
+ permission: ResourcePermission<'scaffolder-template'>,
+ conditions: PermissionCriteria<
+ PermissionCondition<'scaffolder-template', PermissionRuleParams>
+ >,
+) => ConditionalPolicyDecision;
+
+// @alpha
+export const scaffolderConditions: Conditions<{
+ hasTag: PermissionRule<
+ TemplateParametersV1beta3 | TemplateEntityStepV1beta3,
+ {},
+ 'scaffolder-template',
+ {
+ tag: string;
+ }
+ >;
+}>;
+
// @alpha
export const scaffolderPlugin: (
options: ScaffolderPluginOptions,
diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md
index 452689c2ea..be24be46cc 100644
--- a/plugins/scaffolder-backend/api-report.md
+++ b/plugins/scaffolder-backend/api-report.md
@@ -7,23 +7,29 @@
import { ActionContext as ActionContext_2 } from '@backstage/plugin-scaffolder-node';
import { CatalogApi } from '@backstage/catalog-client';
-import { CatalogProcessor } from '@backstage/plugin-catalog-backend';
-import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend';
+import { CatalogProcessor } from '@backstage/plugin-catalog-node';
+import { CatalogProcessorEmit } from '@backstage/plugin-catalog-node';
import { Config } from '@backstage/config';
import { createPullRequest } from 'octokit-plugin-create-pull-request';
+import { Duration } from 'luxon';
import { Entity } from '@backstage/catalog-model';
import express from 'express';
import { GithubCredentialsProvider } from '@backstage/integration';
+import { HumanDuration } from '@backstage/types';
import { IdentityApi } from '@backstage/plugin-auth-node';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { Knex } from 'knex';
-import { LocationSpec } from '@backstage/plugin-catalog-backend';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
import { Logger } from 'winston';
import { Observable } from '@backstage/types';
import { Octokit } from 'octokit';
+import { PermissionEvaluator } from '@backstage/plugin-permission-common';
+import { PermissionRule } from '@backstage/plugin-permission-node';
+import { PermissionRuleParams } from '@backstage/plugin-permission-common';
import { PluginDatabaseManager } from '@backstage/backend-common';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
+import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder-common/alpha';
import { Schema } from 'jsonschema';
import { ScmIntegrationRegistry } from '@backstage/integration';
import { ScmIntegrations } from '@backstage/integration';
@@ -33,6 +39,8 @@ import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { TaskSpecV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateAction as TemplateAction_2 } from '@backstage/plugin-scaffolder-node';
import { TemplateActionOptions } from '@backstage/plugin-scaffolder-node';
+import { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common';
+import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common';
import { UrlReader } from '@backstage/backend-common';
import { Writable } from 'stream';
import { ZodType } from 'zod';
@@ -103,6 +111,15 @@ export function createFetchPlainAction(options: {
targetPath?: string | undefined;
}>;
+// @public
+export function createFetchPlainFileAction(options: {
+ reader: UrlReader;
+ integrations: ScmIntegrations;
+}): TemplateAction_2<{
+ url: string;
+ targetPath: string;
+}>;
+
// @public
export function createFetchTemplateAction(options: {
reader: UrlReader;
@@ -528,6 +545,11 @@ export const createTemplateAction: <
action: TemplateActionOptions,
) => TemplateAction_2;
+// @public
+export function createWaitAction(options?: {
+ maxWaitTime?: Duration | HumanDuration;
+}): TemplateAction_2;
+
// @public
export type CreateWorkerOptions = {
taskBroker: TaskBroker;
@@ -550,6 +572,14 @@ export interface CurrentClaimedTask {
// @public
export class DatabaseTaskStore implements TaskStore {
+ // (undocumented)
+ cancelTask(
+ options: TaskStoreEmitOptions<
+ {
+ message: string;
+ } & JsonObject
+ >,
+ ): Promise;
// (undocumented)
claimTask(): Promise;
// (undocumented)
@@ -641,6 +671,10 @@ export interface RouterOptions {
// (undocumented)
logger: Logger;
// (undocumented)
+ permissionApi?: PermissionEvaluator;
+ // (undocumented)
+ permissionRules?: TemplatePermissionRuleInput[];
+ // (undocumented)
reader: UrlReader;
// (undocumented)
scheduler?: PluginTaskScheduler;
@@ -694,6 +728,8 @@ export type SerializedTaskEvent = {
// @public
export interface TaskBroker {
+ // (undocumented)
+ cancel?(taskId: string): Promise;
// (undocumented)
claim(): Promise;
// (undocumented)
@@ -731,6 +767,8 @@ export type TaskCompletionState = 'failed' | 'completed';
// @public
export interface TaskContext {
+ // (undocumented)
+ cancelSignal: AbortSignal;
// (undocumented)
complete(result: TaskCompletionState, metadata?: JsonObject): Promise;
// (undocumented)
@@ -750,16 +788,19 @@ export interface TaskContext {
}
// @public
-export type TaskEventType = 'completion' | 'log';
+export type TaskEventType = 'completion' | 'log' | 'cancelled';
// @public
export class TaskManager implements TaskContext {
+ // (undocumented)
+ get cancelSignal(): AbortSignal;
// (undocumented)
complete(result: TaskCompletionState, metadata?: JsonObject): Promise;
// (undocumented)
static create(
task: CurrentClaimedTask,
storage: TaskStore,
+ abortSignal: AbortSignal,
logger: Logger,
): TaskManager;
// (undocumented)
@@ -781,14 +822,16 @@ export type TaskSecrets = TaskSecrets_2;
// @public
export type TaskStatus =
- | 'open'
- | 'processing'
- | 'failed'
| 'cancelled'
- | 'completed';
+ | 'completed'
+ | 'failed'
+ | 'open'
+ | 'processing';
// @public
export interface TaskStore {
+ // (undocumented)
+ cancelTask?(options: TaskStoreEmitOptions): Promise;
// (undocumented)
claimTask(): Promise;
// (undocumented)
@@ -887,4 +930,14 @@ export type TemplateFilter = (...args: JsonValue[]) => JsonValue | undefined;
export type TemplateGlobal =
| ((...args: JsonValue[]) => JsonValue | undefined)
| JsonValue;
+
+// @public (undocumented)
+export type TemplatePermissionRuleInput<
+ TParams extends PermissionRuleParams = PermissionRuleParams,
+> = PermissionRule<
+ TemplateEntityStepV1beta3 | TemplateParametersV1beta3,
+ {},
+ typeof RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
+ TParams
+>;
```
diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json
index d7eae0580a..93506eb01c 100644
--- a/plugins/scaffolder-backend/package.json
+++ b/plugins/scaffolder-backend/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder-backend",
"description": "The Backstage backend plugin that helps you create new things",
- "version": "1.12.0",
+ "version": "1.12.1-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -56,7 +56,10 @@
"@backstage/integration": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
"@backstage/plugin-catalog-backend": "workspace:^",
+ "@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-node": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
+ "@backstage/plugin-permission-node": "workspace:^",
"@backstage/plugin-scaffolder-common": "workspace:^",
"@backstage/plugin-scaffolder-node": "workspace:^",
"@backstage/types": "workspace:^",
@@ -64,6 +67,7 @@
"@gitbeaker/node": "^35.1.0",
"@octokit/webhooks": "^10.0.0",
"@types/express": "^4.17.6",
+ "@types/luxon": "^3.0.0",
"azure-devops-node-api": "^11.0.1",
"command-exists": "^1.2.9",
"compression": "^1.7.4",
@@ -109,6 +113,7 @@
"mock-fs": "^5.1.0",
"msw": "^1.0.0",
"supertest": "^6.1.3",
+ "wait-for-expect": "^3.0.2",
"yaml": "^2.0.0"
},
"files": [
diff --git a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts
index 182bb60223..97abf41828 100644
--- a/plugins/scaffolder-backend/src/ScaffolderPlugin.ts
+++ b/plugins/scaffolder-backend/src/ScaffolderPlugin.ts
@@ -94,6 +94,7 @@ export const scaffolderPlugin = createBackendPlugin(
database,
httpRouter,
catalogClient,
+ permissions,
}) {
const {
additionalTemplateFilters,
@@ -131,6 +132,7 @@ export const scaffolderPlugin = createBackendPlugin(
taskWorkers,
additionalTemplateFilters,
additionalTemplateGlobals,
+ permissionApi: permissions,
});
httpRouter.use(router);
},
diff --git a/plugins/scaffolder-backend/src/alpha.ts b/plugins/scaffolder-backend/src/alpha.ts
index ac96f081ad..713f2569aa 100644
--- a/plugins/scaffolder-backend/src/alpha.ts
+++ b/plugins/scaffolder-backend/src/alpha.ts
@@ -15,5 +15,6 @@
*/
export * from './modules';
+export * from './service';
export { scaffolderPlugin } from './ScaffolderPlugin';
export type { ScaffolderPluginOptions } from './ScaffolderPlugin';
diff --git a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts
index d095b77b4c..1de381a8ff 100644
--- a/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts
+++ b/plugins/scaffolder-backend/src/processor/ScaffolderEntitiesProcessor.ts
@@ -24,9 +24,9 @@ import {
import {
CatalogProcessor,
CatalogProcessorEmit,
- LocationSpec,
processingResult,
-} from '@backstage/plugin-catalog-backend';
+} from '@backstage/plugin-catalog-node';
+import { LocationSpec } from '@backstage/plugin-catalog-common';
import {
TemplateEntityV1beta3,
templateEntityV1beta3Validator,
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts
index 9ba4a27b1a..747b5cdaed 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts
@@ -30,8 +30,12 @@ import {
} from './catalog';
import { TemplateFilter, TemplateGlobal } from '../../../lib';
-import { createDebugLogAction } from './debug';
-import { createFetchPlainAction, createFetchTemplateAction } from './fetch';
+import { createDebugLogAction, createWaitAction } from './debug';
+import {
+ createFetchPlainAction,
+ createFetchPlainFileAction,
+ createFetchTemplateAction,
+} from './fetch';
import {
createFilesystemDeleteAction,
createFilesystemRenameAction,
@@ -112,6 +116,10 @@ export const createBuiltinActions = (
reader,
integrations,
}),
+ createFetchPlainFileAction({
+ reader,
+ integrations,
+ }),
createFetchTemplateAction({
integrations,
reader,
@@ -159,6 +167,7 @@ export const createBuiltinActions = (
config,
}),
createDebugLogAction(),
+ createWaitAction(),
createCatalogRegisterAction({ catalogClient, integrations }),
createFetchCatalogEntityAction({ catalogClient }),
createCatalogWriteAction(),
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/index.ts
index 8cc50a7eba..3776ce7f03 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/index.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/index.ts
@@ -15,3 +15,4 @@
*/
export { createDebugLogAction } from './log';
+export { createWaitAction } from './wait';
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts
new file mode 100644
index 0000000000..0d4cdaba4b
--- /dev/null
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.test.ts
@@ -0,0 +1,76 @@
+/*
+ * 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 { getVoidLogger } from '@backstage/backend-common';
+import mockFs from 'mock-fs';
+import { createWaitAction } from './wait';
+import { Writable } from 'stream';
+import os from 'os';
+
+describe('debug:wait', () => {
+ const action = createWaitAction();
+
+ const logStream = {
+ write: jest.fn(),
+ } as jest.Mocked> as jest.Mocked;
+
+ const mockTmpDir = os.tmpdir();
+ const mockContext = {
+ input: {},
+ baseUrl: 'somebase',
+ workspacePath: mockTmpDir,
+ logger: getVoidLogger(),
+ logStream,
+ output: jest.fn(),
+ createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir),
+ };
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ afterEach(() => {
+ mockFs.restore();
+ });
+
+ it('should wait for specified period of time', async () => {
+ const context = {
+ ...mockContext,
+ input: {
+ milliseconds: 50,
+ },
+ };
+ const start = new Date().getTime();
+ await action.handler(context);
+ const end = new Date().getTime();
+ expect(end - start).toBeGreaterThanOrEqual(50);
+ });
+
+ it('should not allow to set waiting time longer than the max waiting time', async () => {
+ const context = {
+ ...mockContext,
+ input: {
+ minutes: 11,
+ },
+ };
+
+ await expect(async () => {
+ await action.handler(context);
+ }).rejects.toThrow(
+ 'Waiting duration is longer than the maximum threshold of 0 hours, 0 minutes, 30 seconds',
+ );
+ });
+});
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts
new file mode 100644
index 0000000000..02cab239cb
--- /dev/null
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts
@@ -0,0 +1,131 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
+import { HumanDuration } from '@backstage/types';
+import yaml from 'yaml';
+import { Duration } from 'luxon';
+
+const id = 'debug:wait';
+
+const MAX_WAIT_TIME_IN_ISO = 'T00:00:30';
+
+const examples = [
+ {
+ description: 'Waiting for 5 seconds',
+ example: yaml.stringify({
+ steps: [
+ {
+ action: id,
+ id: 'wait-5sec',
+ name: 'Waiting for 5 seconds',
+ input: {
+ seconds: 5,
+ },
+ },
+ ],
+ }),
+ },
+ {
+ description: 'Waiting for 5 minutes',
+ example: yaml.stringify({
+ steps: [
+ {
+ action: id,
+ id: 'wait-5min',
+ name: 'Waiting for 5 minutes',
+ input: {
+ minutes: 5,
+ },
+ },
+ ],
+ }),
+ },
+];
+
+/**
+ * Waits for a certain period of time.
+ *
+ * @remarks
+ *
+ * This task is useful to give some waiting time for manual intervention.
+ * Has to be used in a combination with other actions.
+ *
+ * @public
+ */
+export function createWaitAction(options?: {
+ maxWaitTime?: Duration | HumanDuration;
+}) {
+ const toDuration = (
+ maxWaitTime: Duration | HumanDuration | undefined,
+ ): Duration => {
+ if (maxWaitTime) {
+ if (maxWaitTime instanceof Duration) {
+ return maxWaitTime;
+ }
+ return Duration.fromObject(maxWaitTime);
+ }
+ return Duration.fromISOTime(MAX_WAIT_TIME_IN_ISO);
+ };
+
+ return createTemplateAction({
+ id,
+ description: 'Waits for a certain period of time.',
+ examples,
+ schema: {
+ input: {
+ type: 'object',
+ properties: {
+ minutes: {
+ title: 'Waiting period in minutes.',
+ type: 'number',
+ },
+ seconds: {
+ title: 'Waiting period in seconds.',
+ type: 'number',
+ },
+ milliseconds: {
+ title: 'Waiting period in milliseconds.',
+ type: 'number',
+ },
+ },
+ },
+ },
+ async handler(ctx) {
+ const delayTime = Duration.fromObject(ctx.input);
+ const maxWait = toDuration(options?.maxWaitTime);
+
+ if (delayTime.minus(maxWait).toMillis() > 0) {
+ throw new Error(
+ `Waiting duration is longer than the maximum threshold of ${maxWait.toHuman()}`,
+ );
+ }
+
+ await new Promise(resolve => {
+ const controller = new AbortController();
+ const timeoutHandle = setTimeout(abort, delayTime.toMillis());
+ ctx.signal?.addEventListener('abort', abort);
+
+ function abort() {
+ ctx.signal?.removeEventListener('abort', abort);
+ clearTimeout(timeoutHandle!);
+ controller.abort();
+ resolve('finished');
+ }
+ });
+ },
+ });
+}
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts
index 86d5f17803..3c4a677d4d 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.test.ts
@@ -21,7 +21,7 @@ import { resolve as resolvePath } from 'path';
import { UrlReader } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { ScmIntegrations } from '@backstage/integration';
-import { fetchContents } from './helpers';
+import { fetchContents, fetchFile } from './helpers';
import os from 'os';
describe('fetchContent helper', () => {
@@ -37,9 +37,10 @@ describe('fetchContent helper', () => {
}),
);
+ const readUrl = jest.fn();
const readTree = jest.fn();
const reader: UrlReader = {
- readUrl: jest.fn(),
+ readUrl,
readTree,
search: jest.fn(),
};
@@ -50,73 +51,165 @@ describe('fetchContent helper', () => {
outputPath: os.tmpdir(),
};
- it('should reject absolute file locations', async () => {
- await expect(
- fetchContents({
- ...options,
- baseUrl: 'file:///some/path',
- fetchUrl: '/etc/passwd',
- }),
- ).rejects.toThrow(
- 'Relative path is not allowed to refer to a directory outside its parent',
- );
- });
-
- it('should reject relative file locations that exit the baseUrl', async () => {
- await expect(
- fetchContents({
- ...options,
- baseUrl: 'file:///some/path',
- fetchUrl: '../test',
- }),
- ).rejects.toThrow(
- 'Relative path is not allowed to refer to a directory outside its parent',
- );
- });
-
- it('should copy file to outputpath', async () => {
- await fetchContents({
- ...options,
- baseUrl: 'file:///some/path',
- fetchUrl: 'foo',
- outputPath: 'somepath',
+ describe('fetch contents', () => {
+ it('should reject absolute file locations', async () => {
+ await expect(
+ fetchContents({
+ ...options,
+ baseUrl: 'file:///some/path',
+ fetchUrl: '/etc/passwd',
+ }),
+ ).rejects.toThrow(
+ 'Relative path is not allowed to refer to a directory outside its parent',
+ );
});
- expect(fs.copy).toHaveBeenCalledWith(resolvePath('/some/foo'), 'somepath');
- });
- it('should reject if no integration matches location', async () => {
- await expect(
- fetchContents({
- ...options,
- baseUrl: 'http://example.com/some/folder',
- }),
- ).rejects.toThrow(
- 'No integration found for location http://example.com/some/folder',
- );
- });
+ it('should reject relative file locations that exit the baseUrl', async () => {
+ await expect(
+ fetchContents({
+ ...options,
+ baseUrl: 'file:///some/path',
+ fetchUrl: '../test',
+ }),
+ ).rejects.toThrow(
+ 'Relative path is not allowed to refer to a directory outside its parent',
+ );
+ });
- it('should reject if fetch url is relative and no base url is specified', async () => {
- await expect(
- fetchContents({
+ it('should copy file to outputpath', async () => {
+ await fetchContents({
...options,
+ baseUrl: 'file:///some/path',
fetchUrl: 'foo',
- }),
- ).rejects.toThrow(
- 'Failed to fetch, template location could not be determined and the fetch URL is relative, foo',
- );
+ outputPath: 'somepath',
+ });
+ expect(fs.copy).toHaveBeenCalledWith(
+ resolvePath('/some/foo'),
+ 'somepath',
+ );
+ });
+
+ it('should reject if no integration matches location', async () => {
+ await expect(
+ fetchContents({
+ ...options,
+ baseUrl: 'http://example.com/some/folder',
+ }),
+ ).rejects.toThrow(
+ 'No integration found for location http://example.com/some/folder',
+ );
+ });
+
+ it('should reject if fetch url is relative and no base url is specified', async () => {
+ await expect(
+ fetchContents({
+ ...options,
+ fetchUrl: 'foo',
+ }),
+ ).rejects.toThrow(
+ 'Failed to fetch, template location could not be determined and the fetch URL is relative, foo',
+ );
+ });
+
+ it('should fetch url contents', async () => {
+ const dirFunction = jest.fn();
+ readTree.mockResolvedValue({
+ dir: dirFunction,
+ });
+ await fetchContents({
+ ...options,
+ outputPath: 'foo',
+ fetchUrl: 'https://github.com/backstage/foo',
+ });
+ expect(fs.ensureDir).toHaveBeenCalledWith('foo');
+ expect(dirFunction).toHaveBeenCalledWith({ targetDir: 'foo' });
+ });
});
- it('should fetch url contents', async () => {
- const dirFunction = jest.fn();
- readTree.mockResolvedValue({
- dir: dirFunction,
+ describe('fetch file', () => {
+ it('should reject absolute file locations', async () => {
+ await expect(
+ fetchFile({
+ ...options,
+ baseUrl: 'file:///some/path',
+ fetchUrl: '/etc/passwd',
+ }),
+ ).rejects.toThrow(
+ 'Relative path is not allowed to refer to a directory outside its parent',
+ );
});
- await fetchContents({
- ...options,
- outputPath: 'foo',
- fetchUrl: 'https://github.com/backstage/foo',
+
+ it('should reject relative file locations that exit the baseUrl', async () => {
+ await expect(
+ fetchFile({
+ ...options,
+ baseUrl: 'file:///some/path',
+ fetchUrl: '../test',
+ }),
+ ).rejects.toThrow(
+ 'Relative path is not allowed to refer to a directory outside its parent',
+ );
+ });
+
+ it('should copy file to outputpath', async () => {
+ await fetchFile({
+ ...options,
+ baseUrl: 'file:///some/path',
+ fetchUrl: 'foo',
+ outputPath: 'somepath',
+ });
+ expect(fs.copyFile).toHaveBeenCalledWith(
+ resolvePath('/some/foo'),
+ 'somepath',
+ );
+ });
+
+ it('should reject if no integration matches location', async () => {
+ await expect(
+ fetchFile({
+ ...options,
+ baseUrl: 'http://example.com/some/folder',
+ }),
+ ).rejects.toThrow(
+ 'No integration found for location http://example.com/some/folder',
+ );
+ });
+
+ it('should reject if fetch url is relative and no base url is specified', async () => {
+ await expect(
+ fetchFile({
+ ...options,
+ fetchUrl: 'foo',
+ }),
+ ).rejects.toThrow(
+ 'Failed to fetch, template location could not be determined and the fetch URL is relative, foo',
+ );
+ });
+
+ it('should fetch content from url', async () => {
+ readUrl.mockResolvedValue({
+ buffer: () => Buffer.from('test', 'utf8'),
+ });
+ await fetchFile({
+ ...options,
+ outputPath: 'foo',
+ fetchUrl: 'https://github.com/backstage/foo',
+ });
+ expect(fs.ensureDir).toHaveBeenCalledWith('.');
+ expect(fs.outputFile).toHaveBeenCalledWith('foo', 'test');
+ });
+
+ it('should fetch content from url into directory', async () => {
+ readUrl.mockResolvedValue({
+ buffer: () => Buffer.from('test', 'utf8'),
+ });
+ await fetchFile({
+ ...options,
+ outputPath: 'mydir/foo',
+ fetchUrl: 'https://github.com/backstage/foo',
+ });
+ expect(fs.ensureDir).toHaveBeenCalledWith('mydir');
+ expect(fs.outputFile).toHaveBeenCalledWith('mydir/foo', 'test');
});
- expect(fs.ensureDir).toHaveBeenCalled();
- expect(dirFunction).toHaveBeenCalledWith({ targetDir: 'foo' });
});
});
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts
index 4b82595cfd..301c9991c4 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/helpers.ts
@@ -35,6 +35,53 @@ export async function fetchContents(options: {
}) {
const { reader, integrations, baseUrl, fetchUrl = '.', outputPath } = options;
+ const fetchUrlIsAbsolute = isFetchUrlAbsolute(fetchUrl);
+
+ // We handle both file locations and url ones
+ if (!fetchUrlIsAbsolute && baseUrl?.startsWith('file://')) {
+ const basePath = baseUrl.slice('file://'.length);
+ const srcDir = resolveSafeChildPath(path.dirname(basePath), fetchUrl);
+ await fs.copy(srcDir, outputPath);
+ } else {
+ const readUrl = getReadUrl(fetchUrl, baseUrl, integrations);
+
+ const res = await reader.readTree(readUrl);
+ await fs.ensureDir(outputPath);
+ await res.dir({ targetDir: outputPath });
+ }
+}
+
+/**
+ * A helper function that reads the content of a single file from the given URL.
+ * Can be used in your own actions, and also used behind `fetch:plain:file`
+ */
+export async function fetchFile(options: {
+ reader: UrlReader;
+ integrations: ScmIntegrations;
+ baseUrl?: string;
+ fetchUrl?: string;
+ outputPath: string;
+}) {
+ const { reader, integrations, baseUrl, fetchUrl = '.', outputPath } = options;
+
+ const fetchUrlIsAbsolute = isFetchUrlAbsolute(fetchUrl);
+
+ // We handle both file locations and url ones
+ if (!fetchUrlIsAbsolute && baseUrl?.startsWith('file://')) {
+ const basePath = baseUrl.slice('file://'.length);
+ const src = resolveSafeChildPath(path.dirname(basePath), fetchUrl);
+ await fs.copyFile(src, outputPath);
+ } else {
+ const readUrl = getReadUrl(fetchUrl, baseUrl, integrations);
+
+ const res = await reader.readUrl(readUrl);
+ await fs.ensureDir(path.dirname(outputPath));
+ const buffer = await res.buffer();
+ await fs.outputFile(outputPath, buffer.toString());
+ }
+}
+
+function isFetchUrlAbsolute(fetchUrl: string) {
let fetchUrlIsAbsolute = false;
try {
// eslint-disable-next-line no-new
@@ -43,35 +90,28 @@ export async function fetchContents(options: {
} catch {
/* ignored */
}
+ return fetchUrlIsAbsolute;
+}
- // We handle both file locations and url ones
- if (!fetchUrlIsAbsolute && baseUrl?.startsWith('file://')) {
- const basePath = baseUrl.slice('file://'.length);
- const srcDir = resolveSafeChildPath(path.dirname(basePath), fetchUrl);
- await fs.copy(srcDir, outputPath);
- } else {
- let readUrl;
-
- if (fetchUrlIsAbsolute) {
- readUrl = fetchUrl;
- } else if (baseUrl) {
- const integration = integrations.byUrl(baseUrl);
- if (!integration) {
- throw new InputError(`No integration found for location ${baseUrl}`);
- }
-
- readUrl = integration.resolveUrl({
- url: fetchUrl,
- base: baseUrl,
- });
- } else {
- throw new InputError(
- `Failed to fetch, template location could not be determined and the fetch URL is relative, ${fetchUrl}`,
- );
+function getReadUrl(
+ fetchUrl: string,
+ baseUrl: string | undefined,
+ integrations: ScmIntegrations,
+) {
+ if (isFetchUrlAbsolute(fetchUrl)) {
+ return fetchUrl;
+ } else if (baseUrl) {
+ const integration = integrations.byUrl(baseUrl);
+ if (!integration) {
+ throw new InputError(`No integration found for location ${baseUrl}`);
}
- const res = await reader.readTree(readUrl);
- await fs.ensureDir(outputPath);
- await res.dir({ targetDir: outputPath });
+ return integration.resolveUrl({
+ url: fetchUrl,
+ base: baseUrl,
+ });
}
+ throw new InputError(
+ `Failed to fetch, template location could not be determined and the fetch URL is relative, ${fetchUrl}`,
+ );
}
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts
index c9a16c274e..14a40f53c3 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/index.ts
@@ -15,5 +15,6 @@
*/
export { createFetchPlainAction } from './plain';
+export { createFetchPlainFileAction } from './plainFile';
export { createFetchTemplateAction } from './template';
export { fetchContents } from './helpers';
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts
new file mode 100644
index 0000000000..5d9e99fc7d
--- /dev/null
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.test.ts
@@ -0,0 +1,85 @@
+/*
+ * 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.
+ */
+
+jest.mock('./helpers');
+
+import os from 'os';
+import { resolve as resolvePath } from 'path';
+import { getVoidLogger, UrlReader } from '@backstage/backend-common';
+import { ConfigReader } from '@backstage/config';
+import { ScmIntegrations } from '@backstage/integration';
+import { createFetchPlainFileAction } from './plainFile';
+import { PassThrough } from 'stream';
+import { fetchFile } from './helpers';
+
+describe('fetch:plain:file', () => {
+ const integrations = ScmIntegrations.fromConfig(
+ new ConfigReader({
+ integrations: {
+ github: [{ host: 'github.com', token: 'token' }],
+ },
+ }),
+ );
+ const reader: UrlReader = {
+ readUrl: jest.fn(),
+ readTree: jest.fn(),
+ search: jest.fn(),
+ };
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ const action = createFetchPlainFileAction({ integrations, reader });
+ const mockContext = {
+ workspacePath: os.tmpdir(),
+ logger: getVoidLogger(),
+ logStream: new PassThrough(),
+ output: jest.fn(),
+ createTemporaryDirectory: jest.fn(),
+ };
+
+ it('should disallow a target path outside working directory', async () => {
+ await expect(
+ action.handler({
+ ...mockContext,
+ input: {
+ url: 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets/Backstage%20Community%20Sessions.png',
+ targetPath: '/foobar',
+ },
+ }),
+ ).rejects.toThrow(
+ /Relative path is not allowed to refer to a directory outside its parent/,
+ );
+ });
+
+ it('should fetch plain', async () => {
+ await action.handler({
+ ...mockContext,
+ input: {
+ url: 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets/Backstage%20Community%20Sessions.png',
+ targetPath: 'lol',
+ },
+ });
+ expect(fetchFile).toHaveBeenCalledWith(
+ expect.objectContaining({
+ outputPath: resolvePath(mockContext.workspacePath, 'lol'),
+ fetchUrl:
+ 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets/Backstage%20Community%20Sessions.png',
+ }),
+ );
+ });
+});
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts
new file mode 100644
index 0000000000..7b33ad93b4
--- /dev/null
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts
@@ -0,0 +1,75 @@
+/*
+ * 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 { UrlReader, resolveSafeChildPath } from '@backstage/backend-common';
+import { ScmIntegrations } from '@backstage/integration';
+import { fetchFile } from './helpers';
+import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
+
+/**
+ * Downloads content and places it in the workspace, or optionally
+ * in a subdirectory specified by the 'targetPath' input option.
+ * @public
+ */
+export function createFetchPlainFileAction(options: {
+ reader: UrlReader;
+ integrations: ScmIntegrations;
+}) {
+ const { reader, integrations } = options;
+
+ return createTemplateAction<{ url: string; targetPath: string }>({
+ id: 'fetch:plain:file',
+ description: 'Downloads single file and places it in the workspace.',
+ schema: {
+ input: {
+ type: 'object',
+ required: ['url', 'targetPath'],
+ properties: {
+ url: {
+ title: 'Fetch URL',
+ description:
+ 'Relative path or absolute URL pointing to the single file to fetch.',
+ type: 'string',
+ },
+ targetPath: {
+ title: 'Target Path',
+ description:
+ 'Target path within the working directory to download the file as.',
+ type: 'string',
+ },
+ },
+ },
+ },
+ supportsDryRun: true,
+ async handler(ctx) {
+ ctx.logger.info('Fetching plain content from remote URL');
+
+ // Finally move the template result into the task workspace
+ const outputPath = resolveSafeChildPath(
+ ctx.workspacePath,
+ ctx.input.targetPath,
+ );
+
+ await fetchFile({
+ reader,
+ integrations,
+ baseUrl: ctx.templateInfo?.baseUrl,
+ fetchUrl: ctx.input.url,
+ outputPath,
+ });
+ },
+ });
+}
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts
index bb27410402..06e056cda3 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/gitlabMergeRequest.ts
@@ -127,7 +127,7 @@ export const createPublishGitlabMergeRequestAction = (options: {
title: 'Gitlab Project path',
type: 'string',
},
- mergeRequestURL: {
+ mergeRequestUrl: {
title: 'MergeRequest(MR) URL',
type: 'string',
description: 'Link to the merge request in GitLab',
diff --git a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts
index 1929e3b8ac..d9302f4d74 100644
--- a/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/dryrun/createDryRunner.ts
@@ -25,7 +25,7 @@ import {
SerializedFile,
serializeDirectoryContents,
} from '../../lib/files';
-import { TemplateFilter, TemplateGlobal } from '../../lib/templating';
+import { TemplateFilter, TemplateGlobal } from '../../lib';
import { TemplateActionRegistry } from '../actions';
import { NunjucksWorkflowRunner } from '../tasks/NunjucksWorkflowRunner';
import { DecoratedActionsRegistry } from './DecoratedActionsRegistry';
@@ -94,6 +94,8 @@ export function createDryRunner(options: TemplateTesterCreateOptions) {
try {
await deserializeDirectoryContents(contentsPath, input.directoryContents);
+ const abortSignal = new AbortController().signal;
+
const result = await workflowRunner.execute({
spec: {
...input.spec,
@@ -117,6 +119,7 @@ export function createDryRunner(options: TemplateTesterCreateOptions) {
done: false,
isDryRun: true,
getWorkspaceName: async () => `dry-run-${dryRunId}`,
+ cancelSignal: abortSignal,
async emitLog(message: string, logMetadata?: JsonObject) {
if (logMetadata?.stepId === dryRunId) {
return;
@@ -128,7 +131,7 @@ export function createDryRunner(options: TemplateTesterCreateOptions) {
},
});
},
- async complete() {
+ complete: async () => {
throw new Error('Not implemented');
},
});
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts
new file mode 100644
index 0000000000..4c0eb94c82
--- /dev/null
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.test.ts
@@ -0,0 +1,191 @@
+/*
+ * 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 { DatabaseManager } from '@backstage/backend-common';
+import { ConfigReader } from '@backstage/config';
+import { DatabaseTaskStore } from './DatabaseTaskStore';
+import { TaskSpec } from '@backstage/plugin-scaffolder-common';
+import { ConflictError } from '@backstage/errors';
+
+const createStore = async () => {
+ const manager = DatabaseManager.fromConfig(
+ new ConfigReader({
+ backend: {
+ database: {
+ client: 'better-sqlite3',
+ connection: ':memory:',
+ },
+ },
+ }),
+ ).forPlugin('scaffolder');
+ const store = await DatabaseTaskStore.create({
+ database: manager,
+ });
+ return { store, manager };
+};
+
+describe('DatabaseTaskStore', () => {
+ it('should create the database store and run migration', async () => {
+ const { store, manager } = await createStore();
+ expect(store).toBeDefined();
+
+ const client = await manager.getClient();
+ expect(client.schema.hasTable('tasks')).toBeTruthy();
+ expect(client.schema.hasTable('task_events')).toBeTruthy();
+ });
+
+ it('should list all created tasks', async () => {
+ const { store } = await createStore();
+ await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'me',
+ });
+
+ const { tasks } = await store.list({});
+ expect(tasks.length).toBe(1);
+ expect(tasks[0].createdBy).toBe('me');
+ expect(tasks[0].status).toBe('open');
+ expect(tasks[0].id).toBeDefined();
+ });
+
+ it('should list filtered created tasks by createdBy', async () => {
+ const { store } = await createStore();
+
+ await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'me',
+ });
+
+ await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'him',
+ });
+
+ const { tasks } = await store.list({ createdBy: 'him' });
+ expect(tasks.length).toBe(1);
+ expect(tasks[0].createdBy).toBe('him');
+ expect(tasks[0].status).toBe('open');
+ expect(tasks[0].id).toBeDefined();
+ });
+
+ it('should sent an event to start cancelling the task', async () => {
+ const { store } = await createStore();
+
+ const { taskId } = await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'me',
+ });
+ const task = await store.getTask(taskId);
+ expect(task.status).toBe('open');
+
+ await store.cancelTask({
+ taskId,
+ body: {
+ message: `Step 2 has been cancelled.`,
+ stepId: 2,
+ status: 'cancelled',
+ },
+ });
+
+ const { events } = await store.listEvents({ taskId });
+ const event = events[0];
+ expect(event.taskId).toBe(taskId);
+ expect(event.body.status).toBe('cancelled');
+ });
+
+ it('should emit a log event', async () => {
+ const { store } = await createStore();
+ const { taskId } = await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'me',
+ });
+ await store.emitLogEvent({
+ taskId,
+ body: {
+ message: 'Step #2 failed',
+ stepId: 2,
+ status: 'failed',
+ },
+ });
+ const { events } = await store.listEvents({ taskId });
+ const event = events[0];
+ expect(event.taskId).toBe(taskId);
+ expect(event.body.status).toBe('failed');
+ expect(event.type).toBe('log');
+ });
+
+ it('should complete the task', async () => {
+ const { store } = await createStore();
+ const { taskId } = await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'me',
+ });
+ const task = await store.getTask(taskId);
+ expect(task.status).toBe('open');
+
+ const message = `This task was marked as stale as it exceeded its timeout`;
+ await store.completeTask({
+ taskId,
+ status: 'cancelled',
+ eventBody: { message },
+ });
+
+ const taskAfterCompletion = await store.getTask(taskId);
+ expect(taskAfterCompletion.status).toBe('cancelled');
+ });
+
+ it('should claim a new task', async () => {
+ const { store } = await createStore();
+ const { taskId } = await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'me',
+ });
+ const task = await store.getTask(taskId);
+ expect(task.status).toBe('open');
+ await store.claimTask();
+
+ const claimedTask = await store.getTask(taskId);
+ expect(claimedTask.status).toBe('processing');
+ });
+
+ it('should shutdown the running task', async () => {
+ const { store } = await createStore();
+ const { taskId } = await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'me',
+ });
+ const task = await store.getTask(taskId);
+ expect(task.status).toBe('open');
+ await store.claimTask();
+ await store.shutdownTask({ taskId });
+
+ const claimedTask = await store.getTask(taskId);
+ expect(claimedTask.status).toBe('failed');
+ });
+
+ it('should be not possible to shutdown not running task', async () => {
+ const { store } = await createStore();
+ const { taskId } = await store.createTask({
+ spec: {} as TaskSpec,
+ createdBy: 'me',
+ });
+ const task = await store.getTask(taskId);
+ expect(task.status).toBe('open');
+ await expect(async () => {
+ await store.shutdownTask({ taskId });
+ }).rejects.toThrow(ConflictError);
+ });
+});
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts
index 879b457544..f3cb9b645e 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/DatabaseTaskStore.ts
@@ -220,7 +220,7 @@ export class DatabaseTaskStore implements TaskStore {
.update({
status: 'processing',
last_heartbeat_at: this.db.fn.now(),
- // remove the secrets when moving moving to processing state.
+ // remove the secrets when moving to processing state.
secrets: null,
});
@@ -287,13 +287,14 @@ export class DatabaseTaskStore implements TaskStore {
const { taskId, status, eventBody } = options;
let oldStatus: string;
- if (status === 'failed' || status === 'completed') {
+ if (['failed', 'completed', 'cancelled'].includes(status)) {
oldStatus = 'processing';
} else {
throw new Error(
`Invalid status update of run '${taskId}' to status '${status}'`,
);
}
+
await this.db.transaction(async tx => {
const [task] = await tx('tasks')
.where({
@@ -302,6 +303,40 @@ export class DatabaseTaskStore implements TaskStore {
.limit(1)
.select();
+ const updateTask = async (criteria: {
+ id: string;
+ status?: TaskStatus;
+ }) => {
+ const updateCount = await tx('tasks')
+ .where(criteria)
+ .update({
+ status,
+ });
+
+ if (updateCount !== 1) {
+ throw new ConflictError(
+ `Failed to update status to '${status}' for taskId ${taskId}`,
+ );
+ }
+
+ await tx('task_events').insert({
+ task_id: taskId,
+ event_type: 'completion',
+ body: JSON.stringify(eventBody),
+ });
+ };
+
+ if (status === 'cancelled') {
+ await updateTask({
+ id: taskId,
+ });
+ return;
+ }
+
+ if (task.status === 'cancelled') {
+ return;
+ }
+
if (!task) {
throw new Error(`No task with taskId ${taskId} found`);
}
@@ -311,25 +346,10 @@ export class DatabaseTaskStore implements TaskStore {
`as it is currently '${task.status}', expected '${oldStatus}'`,
);
}
- const updateCount = await tx('tasks')
- .where({
- id: taskId,
- status: oldStatus,
- })
- .update({
- status,
- });
- if (updateCount !== 1) {
- throw new ConflictError(
- `Failed to update status to '${status}' for taskId ${taskId}`,
- );
- }
-
- await tx('task_events').insert({
- task_id: taskId,
- event_type: 'completion',
- body: JSON.stringify(eventBody),
+ await updateTask({
+ id: taskId,
+ status: oldStatus,
});
});
}
@@ -419,4 +439,16 @@ export class DatabaseTaskStore implements TaskStore {
},
});
}
+
+ async cancelTask(
+ options: TaskStoreEmitOptions<{ message: string } & JsonObject>,
+ ): Promise {
+ const { taskId, body } = options;
+ const serializedBody = JSON.stringify(body);
+ await this.db('task_events').insert({
+ task_id: taskId,
+ event_type: 'cancelled',
+ body: serializedBody,
+ });
+ }
}
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts
index 0d2246ed36..3b6ec54249 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.test.ts
@@ -70,6 +70,7 @@ describe('DefaultWorkflowRunner', () => {
complete: async () => {},
done: false,
emitLog: async () => {},
+ cancelSignal: new AbortController().signal,
getWorkspaceName: () => Promise.resolve('test-workspace'),
});
@@ -166,7 +167,7 @@ describe('DefaultWorkflowRunner', () => {
});
await expect(runner.execute(task)).rejects.toThrow(
- /Invalid input passed to action jest-validated-action, instance requires property \"foo\"/,
+ /Invalid input passed to action jest-validated-action, instance requires property "foo"/,
);
});
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
index 88abe05cad..629c730d48 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
@@ -15,7 +15,12 @@
*/
import { ScmIntegrations } from '@backstage/integration';
-import { TaskContext, WorkflowResponse, WorkflowRunner } from './types';
+import {
+ TaskContext,
+ TaskTrackType,
+ WorkflowResponse,
+ WorkflowRunner,
+} from './types';
import * as winston from 'winston';
import fs from 'fs-extra';
import path from 'path';
@@ -99,6 +104,7 @@ const createStepLogger = ({
export class NunjucksWorkflowRunner implements WorkflowRunner {
constructor(private readonly options: NunjucksWorkflowRunnerOptions) {}
+
private readonly tracker = scaffoldingTracker();
private isSingleTemplateString(input: string) {
@@ -180,6 +186,138 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
});
}
+ async executeStep(
+ task: TaskContext,
+ step: TaskStep,
+ context: TemplateContext,
+ renderTemplate: (template: string, values: unknown) => string,
+ taskTrack: TaskTrackType,
+ workspacePath: string,
+ ) {
+ const stepTrack = await this.tracker.stepStart(task, step);
+
+ if (task.cancelSignal.aborted) {
+ throw new Error(`Step ${step.name} has been cancelled.`);
+ }
+
+ try {
+ if (step.if) {
+ const ifResult = await this.render(step.if, context, renderTemplate);
+ if (!isTruthy(ifResult)) {
+ await stepTrack.skipFalsy();
+ return;
+ }
+ }
+
+ const action: TemplateAction =
+ this.options.actionRegistry.get(step.action);
+ const { taskLogger, streamLogger } = createStepLogger({ task, step });
+
+ if (task.isDryRun) {
+ const redactedSecrets = Object.fromEntries(
+ Object.entries(task.secrets ?? {}).map(secret => [
+ secret[0],
+ '[REDACTED]',
+ ]),
+ );
+ const debugInput =
+ (step.input &&
+ this.render(
+ step.input,
+ {
+ ...context,
+ secrets: redactedSecrets,
+ },
+ renderTemplate,
+ )) ??
+ {};
+ taskLogger.info(
+ `Running ${
+ action.id
+ } in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
+ debugInput,
+ undefined,
+ 2,
+ )}`,
+ );
+ if (!action.supportsDryRun) {
+ await taskTrack.skipDryRun(step, action);
+ const outputSchema = action.schema?.output;
+ if (outputSchema) {
+ context.steps[step.id] = {
+ output: generateExampleOutput(outputSchema) as {
+ [name in string]: JsonValue;
+ },
+ };
+ } else {
+ context.steps[step.id] = { output: {} };
+ }
+ return;
+ }
+ }
+
+ // Secrets are only passed when templating the input to actions for security reasons
+ const input =
+ (step.input &&
+ this.render(
+ step.input,
+ { ...context, secrets: task.secrets ?? {} },
+ renderTemplate,
+ )) ??
+ {};
+
+ if (action.schema?.input) {
+ const validateResult = validateJsonSchema(input, action.schema.input);
+ if (!validateResult.valid) {
+ const errors = validateResult.errors.join(', ');
+ throw new InputError(
+ `Invalid input passed to action ${action.id}, ${errors}`,
+ );
+ }
+ }
+
+ const tmpDirs = new Array();
+ const stepOutput: { [outputName: string]: JsonValue } = {};
+
+ await action.handler({
+ input,
+ secrets: task.secrets ?? {},
+ logger: taskLogger,
+ logStream: streamLogger,
+ workspacePath,
+ createTemporaryDirectory: async () => {
+ const tmpDir = await fs.mkdtemp(`${workspacePath}_step-${step.id}-`);
+ tmpDirs.push(tmpDir);
+ return tmpDir;
+ },
+ output(name: string, value: JsonValue) {
+ stepOutput[name] = value;
+ },
+ templateInfo: task.spec.templateInfo,
+ user: task.spec.user,
+ isDryRun: task.isDryRun,
+ signal: task.cancelSignal,
+ });
+
+ // Remove all temporary directories that were created when executing the action
+ for (const tmpDir of tmpDirs) {
+ await fs.remove(tmpDir);
+ }
+
+ context.steps[step.id] = { output: stepOutput };
+
+ if (task.cancelSignal.aborted) {
+ throw new Error(`Step ${step.name} has been cancelled.`);
+ }
+
+ await stepTrack.markSuccessful();
+ } catch (err) {
+ await taskTrack.markFailed(step, err);
+ await stepTrack.markFailed();
+ throw err;
+ }
+ }
+
async execute(task: TaskContext): Promise {
if (!isValidTaskSpec(task.spec)) {
throw new InputError(
@@ -191,7 +329,12 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
await task.getWorkspaceName(),
);
- const { integrations } = this.options;
+ const {
+ additionalTemplateFilters,
+ additionalTemplateGlobals,
+ integrations,
+ } = this.options;
+
const renderTemplate = await SecureTemplater.loadRenderer({
// TODO(blam): let's work out how we can deprecate this.
// We shouldn't really need to be exposing these now we can deal with
@@ -200,8 +343,8 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
parseRepoUrl(url: string) {
return parseRepoUrl(url, integrations);
},
- additionalTemplateFilters: this.options.additionalTemplateFilters,
- additionalTemplateGlobals: this.options.additionalTemplateGlobals,
+ additionalTemplateFilters,
+ additionalTemplateGlobals,
});
try {
@@ -215,126 +358,14 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
};
for (const step of task.spec.steps) {
- const stepTrack = await this.tracker.stepStart(task, step);
- try {
- if (step.if) {
- const ifResult = await this.render(
- step.if,
- context,
- renderTemplate,
- );
- if (!isTruthy(ifResult)) {
- await stepTrack.skipFalsy();
- continue;
- }
- }
-
- const action = this.options.actionRegistry.get(step.action);
- const { taskLogger, streamLogger } = createStepLogger({ task, step });
-
- if (task.isDryRun) {
- const redactedSecrets = Object.fromEntries(
- Object.entries(task.secrets ?? {}).map(secret => [
- secret[0],
- '[REDACTED]',
- ]),
- );
- const debugInput =
- (step.input &&
- this.render(
- step.input,
- {
- ...context,
- secrets: redactedSecrets,
- },
- renderTemplate,
- )) ??
- {};
- taskLogger.info(
- `Running ${
- action.id
- } in dry-run mode with inputs (secrets redacted): ${JSON.stringify(
- debugInput,
- undefined,
- 2,
- )}`,
- );
- if (!action.supportsDryRun) {
- await taskTrack.skipDryRun(step, action);
- const outputSchema = action.schema?.output;
- if (outputSchema) {
- context.steps[step.id] = {
- output: generateExampleOutput(outputSchema) as {
- [name in string]: JsonValue;
- },
- };
- } else {
- context.steps[step.id] = { output: {} };
- }
- continue;
- }
- }
-
- // Secrets are only passed when templating the input to actions for security reasons
- const input =
- (step.input &&
- this.render(
- step.input,
- { ...context, secrets: task.secrets ?? {} },
- renderTemplate,
- )) ??
- {};
-
- if (action.schema?.input) {
- const validateResult = validateJsonSchema(
- input,
- action.schema.input,
- );
- if (!validateResult.valid) {
- const errors = validateResult.errors.join(', ');
- throw new InputError(
- `Invalid input passed to action ${action.id}, ${errors}`,
- );
- }
- }
-
- const tmpDirs = new Array();
- const stepOutput: { [outputName: string]: JsonValue } = {};
-
- await action.handler({
- input,
- secrets: task.secrets ?? {},
- logger: taskLogger,
- logStream: streamLogger,
- workspacePath,
- createTemporaryDirectory: async () => {
- const tmpDir = await fs.mkdtemp(
- `${workspacePath}_step-${step.id}-`,
- );
- tmpDirs.push(tmpDir);
- return tmpDir;
- },
- output(name: string, value: JsonValue) {
- stepOutput[name] = value;
- },
- templateInfo: task.spec.templateInfo,
- user: task.spec.user,
- isDryRun: task.isDryRun,
- });
-
- // Remove all temporary directories that were created when executing the action
- for (const tmpDir of tmpDirs) {
- await fs.remove(tmpDir);
- }
-
- context.steps[step.id] = { output: stepOutput };
-
- await stepTrack.markSuccessful();
- } catch (err) {
- await taskTrack.markFailed(step, err);
- await stepTrack.markFailed();
- throw err;
- }
+ await this.executeStep(
+ task,
+ step,
+ context,
+ renderTemplate,
+ taskTrack,
+ workspacePath,
+ );
}
const output = this.render(task.spec.output, context, renderTemplate);
@@ -380,7 +411,10 @@ function scaffoldingTracker() {
template,
});
- async function skipDryRun(step: TaskStep, action: TemplateAction) {
+ async function skipDryRun(
+ step: TaskStep,
+ action: TemplateAction,
+ ) {
task.emitLog(`Skipping because ${action.id} does not support dry-run`, {
stepId: step.id,
status: 'skipped',
@@ -409,8 +443,22 @@ function scaffoldingTracker() {
taskTimer({ result: 'failed' });
}
+ async function markCancelled(step: TaskStep) {
+ await task.emitLog(`Step ${step.id} has been cancelled.`, {
+ stepId: step.id,
+ status: 'cancelled',
+ });
+ taskCount.inc({
+ template,
+ user,
+ result: 'cancelled',
+ });
+ taskTimer({ result: 'cancelled' });
+ }
+
return {
skipDryRun,
+ markCancelled,
markSuccessful,
markFailed,
};
@@ -441,6 +489,15 @@ function scaffoldingTracker() {
stepTimer({ result: 'ok' });
}
+ async function markCancelled() {
+ stepCount.inc({
+ template,
+ step: step.name,
+ result: 'cancelled',
+ });
+ stepTimer({ result: 'cancelled' });
+ }
+
async function markFailed() {
stepCount.inc({
template,
@@ -459,8 +516,9 @@ function scaffoldingTracker() {
}
return {
- markSuccessful,
+ markCancelled,
markFailed,
+ markSuccessful,
skipFalsy,
};
}
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts
index d01351d8ae..fc5a724e47 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts
@@ -39,8 +39,13 @@ export class TaskManager implements TaskContext {
private heartbeatTimeoutId?: ReturnType;
- static create(task: CurrentClaimedTask, storage: TaskStore, logger: Logger) {
- const agent = new TaskManager(task, storage, logger);
+ static create(
+ task: CurrentClaimedTask,
+ storage: TaskStore,
+ abortSignal: AbortSignal,
+ logger: Logger,
+ ) {
+ const agent = new TaskManager(task, storage, abortSignal, logger);
agent.startTimeout();
return agent;
}
@@ -49,6 +54,7 @@ export class TaskManager implements TaskContext {
private constructor(
private readonly task: CurrentClaimedTask,
private readonly storage: TaskStore,
+ private readonly signal: AbortSignal,
private readonly logger: Logger,
) {}
@@ -56,6 +62,10 @@ export class TaskManager implements TaskContext {
return this.task.spec;
}
+ get cancelSignal() {
+ return this.signal;
+ }
+
get secrets() {
return this.task.secrets;
}
@@ -165,6 +175,33 @@ export class StorageTaskBroker implements TaskBroker {
private deferredDispatch = defer();
+ private async registerCancellable(
+ taskId: string,
+ abortController: AbortController,
+ ) {
+ let shouldUnsubscribe = false;
+ const subscription = this.event$({ taskId, after: undefined }).subscribe({
+ error: _ => {
+ subscription.unsubscribe();
+ },
+ next: ({ events }) => {
+ for (const event of events) {
+ if (event.type === 'cancelled') {
+ abortController.abort();
+ shouldUnsubscribe = true;
+ }
+
+ if (event.type === 'completion') {
+ shouldUnsubscribe = true;
+ }
+ }
+ if (shouldUnsubscribe) {
+ subscription.unsubscribe();
+ }
+ },
+ });
+ }
+
/**
* {@inheritdoc TaskBroker.claim}
*/
@@ -172,6 +209,8 @@ export class StorageTaskBroker implements TaskBroker {
for (;;) {
const pendingTask = await this.storage.claimTask();
if (pendingTask) {
+ const abortController = new AbortController();
+ await this.registerCancellable(pendingTask.id, abortController);
return TaskManager.create(
{
taskId: pendingTask.id,
@@ -180,6 +219,7 @@ export class StorageTaskBroker implements TaskBroker {
createdBy: pendingTask.createdBy,
},
this.storage,
+ abortController.signal,
this.logger,
);
}
@@ -271,4 +311,24 @@ export class StorageTaskBroker implements TaskBroker {
this.deferredDispatch.resolve();
this.deferredDispatch = defer();
}
+
+ async cancel(taskId: string) {
+ const { events } = await this.storage.listEvents({ taskId });
+ const currentStepId =
+ events.length > 0
+ ? events
+ .filter(({ body }) => body?.stepId)
+ .reduce((prev, curr) => (prev.id > curr.id ? prev : curr)).body
+ .stepId
+ : 0;
+
+ await this.storage.cancelTask?.({
+ taskId,
+ body: {
+ message: `Step ${currentStepId} has been cancelled.`,
+ stepId: currentStepId,
+ status: 'cancelled',
+ },
+ });
+ }
}
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts
index d39671cb67..e886b31369 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts
@@ -15,7 +15,7 @@
*/
import os from 'os';
-import { getVoidLogger, DatabaseManager } from '@backstage/backend-common';
+import { DatabaseManager, getVoidLogger } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { DatabaseTaskStore } from './DatabaseTaskStore';
import { StorageTaskBroker } from './StorageTaskBroker';
@@ -23,7 +23,14 @@ import { TaskWorker, TaskWorkerOptions } from './TaskWorker';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateActionRegistry } from '../actions';
import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner';
-import { TaskBroker, TaskContext, WorkflowRunner } from './types';
+import {
+ SerializedTaskEvent,
+ TaskBroker,
+ TaskContext,
+ WorkflowRunner,
+} from './types';
+import ObservableImpl from 'zen-observable';
+import waitForExpect from 'wait-for-expect';
jest.mock('./NunjucksWorkflowRunner');
const MockedNunjucksWorkflowRunner =
@@ -198,6 +205,67 @@ describe('Concurrent TaskWorker', () => {
});
});
+describe('Cancellable TaskWorker', () => {
+ let storage: DatabaseTaskStore;
+ const integrations: ScmIntegrations = {} as ScmIntegrations;
+ const actionRegistry: TemplateActionRegistry = {} as TemplateActionRegistry;
+ const workingDirectory = os.tmpdir();
+
+ let myTask: TaskContext | undefined = undefined;
+
+ const workflowRunner: NunjucksWorkflowRunner = {
+ execute: (task: TaskContext) => {
+ myTask = task;
+ },
+ } as unknown as NunjucksWorkflowRunner;
+
+ beforeAll(async () => {
+ storage = await createStore();
+ });
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ MockedNunjucksWorkflowRunner.mockImplementation(() => workflowRunner);
+ });
+
+ const logger = getVoidLogger();
+
+ it('should be able to cancel the running task', async () => {
+ const taskBroker = new StorageTaskBroker(storage, logger);
+ const taskWorker = await TaskWorker.create({
+ logger,
+ workingDirectory,
+ integrations,
+ taskBroker,
+ actionRegistry,
+ });
+
+ const steps = [...Array(10)].map(n => ({
+ id: `test${n}`,
+ name: `test${n}`,
+ action: 'not-found-action',
+ }));
+
+ const { taskId } = await taskBroker.dispatch({
+ spec: {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
+ steps,
+ output: {
+ result: '{{ steps.test.output.testOutput }}',
+ },
+ parameters: {},
+ },
+ });
+
+ await taskWorker.start();
+ await taskBroker.cancel(taskId);
+
+ await waitForExpect(() => {
+ expect(myTask?.cancelSignal.aborted).toBeTruthy();
+ });
+ });
+});
+
describe('TaskWorker internals', () => {
const TaskWorkerConstructor = TaskWorker as unknown as {
new (options: TaskWorkerOptions): TaskWorker;
@@ -219,10 +287,24 @@ describe('TaskWorker internals', () => {
},
};
+ const subscribers = new Set<
+ ZenObservable.SubscriptionObserver<{ events: SerializedTaskEvent[] }>
+ >();
+
let claimedTaskCount = 0;
const taskWorker = new TaskWorkerConstructor({
runners: { workflowRunner },
taskBroker: {
+ event$() {
+ return new ObservableImpl<{ events: SerializedTaskEvent[] }>(
+ subscriber => {
+ subscribers.add(subscriber);
+ return () => {
+ subscribers.delete(subscriber);
+ };
+ },
+ );
+ },
async claim() {
claimedTaskCount++;
return {
@@ -233,7 +315,7 @@ describe('TaskWorker internals', () => {
async complete(_result, _metadata) {},
} as TaskContext;
},
- } as TaskBroker,
+ } as unknown as TaskBroker,
concurrentTasksLimit: 2,
});
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
index 82829665a3..5723e7136f 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
@@ -21,10 +21,7 @@ import { Logger } from 'winston';
import { TemplateActionRegistry } from '../actions';
import { ScmIntegrations } from '@backstage/integration';
import { assertError } from '@backstage/errors';
-import {
- TemplateFilter,
- TemplateGlobal,
-} from '../../lib/templating/SecureTemplater';
+import { TemplateFilter, TemplateGlobal } from '../../lib';
/**
* TaskWorkerOptions
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts
index 5a91802f3e..112276b9dd 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts
@@ -15,8 +15,9 @@
*/
import { JsonValue, JsonObject, Observable } from '@backstage/types';
-import { TaskSpec } from '@backstage/plugin-scaffolder-common';
+import { TaskSpec, TaskStep } from '@backstage/plugin-scaffolder-common';
import { TaskSecrets } from '@backstage/plugin-scaffolder-node';
+import { TemplateAction } from '@backstage/plugin-scaffolder-node';
/**
* The status of each step of the Task
@@ -24,11 +25,11 @@ import { TaskSecrets } from '@backstage/plugin-scaffolder-node';
* @public
*/
export type TaskStatus =
- | 'open'
- | 'processing'
- | 'failed'
| 'cancelled'
- | 'completed';
+ | 'completed'
+ | 'failed'
+ | 'open'
+ | 'processing';
/**
* The state of a completed task.
@@ -57,7 +58,7 @@ export type SerializedTask = {
*
* @public
*/
-export type TaskEventType = 'completion' | 'log';
+export type TaskEventType = 'completion' | 'log' | 'cancelled';
/**
* SerializedTaskEvent
@@ -99,13 +100,17 @@ export type TaskBrokerDispatchOptions = {
* @public
*/
export interface TaskContext {
+ cancelSignal: AbortSignal;
spec: TaskSpec;
secrets?: TaskSecrets;
createdBy?: string;
done: boolean;
isDryRun?: boolean;
- emitLog(message: string, logMetadata?: JsonObject): Promise;
+
complete(result: TaskCompletionState, metadata?: JsonObject): Promise;
+
+ emitLog(message: string, logMetadata?: JsonObject): Promise;
+
getWorkspaceName(): Promise;
}
@@ -115,16 +120,23 @@ export interface TaskContext {
* @public
*/
export interface TaskBroker {
+ cancel?(taskId: string): Promise;
+
claim(): Promise;
+
dispatch(
options: TaskBrokerDispatchOptions,
): Promise;
+
vacuumTasks(options: { timeoutS: number }): Promise;
+
event$(options: {
taskId: string;
after: number | undefined;
}): Observable<{ events: SerializedTaskEvent[] }>;
+
get(taskId: string): Promise;
+
list?(options?: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>;
}
@@ -181,30 +193,51 @@ export type TaskStoreCreateTaskResult = {
* @public
*/
export interface TaskStore {
+ cancelTask?(options: TaskStoreEmitOptions): Promise;
+
createTask(
options: TaskStoreCreateTaskOptions,
): Promise;
+
getTask(taskId: string): Promise;
+
claimTask(): Promise;
+
completeTask(options: {
taskId: string;
status: TaskStatus;
eventBody: JsonObject;
}): Promise;
+
heartbeatTask(taskId: string): Promise;
+
listStaleTasks(options: { timeoutS: number }): Promise<{
tasks: { taskId: string }[];
}>;
+
list?(options: { createdBy?: string }): Promise<{ tasks: SerializedTask[] }>;
emitLogEvent(options: TaskStoreEmitOptions): Promise;
+
listEvents(
options: TaskStoreListEventsOptions,
): Promise<{ events: SerializedTaskEvent[] }>;
+
shutdownTask?(options: TaskStoreShutDownTaskOptions): Promise;
}
export type WorkflowResponse = { output: { [key: string]: JsonValue } };
+
export interface WorkflowRunner {
execute(task: TaskContext): Promise;
}
+
+export type TaskTrackType = {
+ markCancelled: (step: TaskStep) => Promise;
+ markFailed: (step: TaskStep, err: Error) => Promise;
+ markSuccessful: () => Promise;
+ skipDryRun: (
+ step: TaskStep,
+ action: TemplateAction,
+ ) => Promise;
+};
diff --git a/plugins/scaffolder-backend/src/service/conditionExports.ts b/plugins/scaffolder-backend/src/service/conditionExports.ts
new file mode 100644
index 0000000000..0c502c6075
--- /dev/null
+++ b/plugins/scaffolder-backend/src/service/conditionExports.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 { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder-common/alpha';
+import { createConditionExports } from '@backstage/plugin-permission-node';
+import { scaffolderTemplateRules } from './rules';
+
+const { conditions, createConditionalDecision } = createConditionExports({
+ pluginId: 'scaffolder',
+ resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
+ rules: scaffolderTemplateRules,
+});
+
+/**
+ * These conditions are used when creating conditional decisions for scaffolder
+ * templates that are returned by authorization policies.
+ *
+ * @alpha
+ */
+export const scaffolderConditions = conditions;
+
+/**
+ * `createScaffolderConditionalDecision` can be used when authoring policies to
+ * create conditional decisions. It requires a permission of type
+ * `ResourcePermission<'scaffolder-template'>` to be passed as the first parameter.
+ * It's recommended that you use the provided `isResourcePermission` and
+ * `isPermission` helper methods to narrow the type of the permission passed to
+ * the handle method as shown below.
+ *
+ * ```
+ * // MyAuthorizationPolicy.ts
+ * ...
+ * import { createScaffolderPolicyDecision } from '@backstage/plugin-scaffolder-backend';
+ * import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder-common';
+ *
+ * class MyAuthorizationPolicy implements PermissionPolicy {
+ * async handle(request, user) {
+ * ...
+ *
+ * if (isResourcePermission(request.permission, RESOURCE_TYPE_SCAFFOLDER_TEMPLATE)) {
+ * return createScaffolderConditionalDecision(
+ * request.permission,
+ * { anyOf: [...insert conditions here...] }
+ * );
+ * }
+ *
+ * ...
+ * }
+ *
+ * ```
+ *
+ * @alpha
+ */
+export const createScaffolderConditionalDecision = createConditionalDecision;
diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts
index 3412fec2d3..97d4ab93aa 100644
--- a/plugins/scaffolder-backend/src/service/helpers.ts
+++ b/plugins/scaffolder-backend/src/service/helpers.ts
@@ -107,3 +107,7 @@ export async function findTemplate(options: {
return template as TemplateEntityV1beta3;
}
+
+export type TemplateTransform = (
+ template: TemplateEntityV1beta3,
+) => TemplateEntityV1beta3;
diff --git a/plugins/scaffolder-backend/src/service/index.ts b/plugins/scaffolder-backend/src/service/index.ts
new file mode 100644
index 0000000000..dc5508d47f
--- /dev/null
+++ b/plugins/scaffolder-backend/src/service/index.ts
@@ -0,0 +1,17 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export * from './conditionExports';
diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts
index 5e3c1c7278..4f5ed0fe22 100644
--- a/plugins/scaffolder-backend/src/service/router.test.ts
+++ b/plugins/scaffolder-backend/src/service/router.test.ts
@@ -44,6 +44,10 @@ import {
IdentityApiGetIdentityRequest,
BackstageIdentityResponse,
} from '@backstage/plugin-auth-node';
+import {
+ AuthorizeResult,
+ PermissionEvaluator,
+} from '@backstage/plugin-permission-common';
const mockAccess = jest.fn();
@@ -85,8 +89,12 @@ describe('createRouter', () => {
let loggerSpy: jest.SpyInstance;
let taskBroker: TaskBroker;
const catalogClient = { getEntityByRef: jest.fn() } as unknown as CatalogApi;
+ const permissionApi = {
+ authorize: jest.fn(),
+ authorizeConditional: jest.fn(),
+ } as unknown as PermissionEvaluator;
- const mockTemplate: TemplateEntityV1beta3 = {
+ const getMockTemplate = (): TemplateEntityV1beta3 => ({
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
@@ -101,19 +109,54 @@ describe('createRouter', () => {
spec: {
owner: 'web@example.com',
type: 'website',
- steps: [],
- parameters: {
- type: 'object',
- required: ['required'],
- properties: {
- required: {
- type: 'string',
- description: 'Required parameter',
+ steps: [
+ {
+ id: 'step-one',
+ name: 'First log',
+ action: 'debug:log',
+ input: {
+ message: 'hello',
},
},
- },
+ {
+ id: 'step-two',
+ name: 'Second log',
+ action: 'debug:log',
+ input: {
+ message: 'world',
+ },
+ 'backstage:permissions': {
+ tags: ['steps-tag'],
+ },
+ },
+ ],
+ parameters: [
+ {
+ type: 'object',
+ required: ['requiredParameter1'],
+ properties: {
+ requiredParameter1: {
+ type: 'string',
+ description: 'Required parameter 1',
+ },
+ },
+ },
+ {
+ type: 'object',
+ required: ['requiredParameter2'],
+ 'backstage:permissions': {
+ tags: ['parameters-tag'],
+ },
+ properties: {
+ requiredParameter2: {
+ type: 'string',
+ description: 'Required parameter 2',
+ },
+ },
+ },
+ ],
},
- };
+ });
const mockUser: UserEntity = {
apiVersion: 'backstage.io/v1alpha1',
@@ -152,6 +195,7 @@ describe('createRouter', () => {
catalogClient,
reader: mockUrlReader,
taskBroker,
+ permissionApi,
});
app = express().use(router);
@@ -160,15 +204,27 @@ describe('createRouter', () => {
.mockImplementation(async ref => {
const { kind } = parseEntityRef(ref);
- if (kind === 'template') {
- return mockTemplate;
+ if (kind.toLocaleLowerCase() === 'template') {
+ return getMockTemplate();
}
- if (kind === 'user') {
+ if (kind.toLocaleLowerCase() === 'user') {
return mockUser;
}
+
throw new Error(`no mock found for kind: ${kind}`);
});
+
+ jest
+ .spyOn(permissionApi, 'authorizeConditional')
+ .mockImplementation(async () => [
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ ]);
});
afterEach(() => {
@@ -216,12 +272,13 @@ describe('createRouter', () => {
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
- expect(response.body.id).toBe('a-random-id');
expect(response.status).toEqual(201);
+ expect(response.body.id).toBe('a-random-id');
});
it('should call the broker with a correct spec', async () => {
@@ -229,6 +286,7 @@ describe('createRouter', () => {
taskBroker.dispatch as jest.Mocked['dispatch'];
const mockToken =
'blob.eyJzdWIiOiJ1c2VyOmRlZmF1bHQvZ3Vlc3QiLCJuYW1lIjoiSm9obiBEb2UifQ.blob';
+ const mockTemplate = getMockTemplate();
await request(app)
.post('/v2/tasks')
@@ -239,7 +297,8 @@ describe('createRouter', () => {
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
expect(broker).toHaveBeenCalledWith(
@@ -258,7 +317,8 @@ describe('createRouter', () => {
})),
output: mockTemplate.spec.output ?? {},
parameters: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
user: {
entity: mockUser,
@@ -284,6 +344,7 @@ describe('createRouter', () => {
const broker =
taskBroker.dispatch as jest.Mocked['dispatch'];
const mockToken = 'blob.eyJzdWIiOiIiLCJuYW1lIjoiSm9obiBEb2UifQ.blob';
+ const mockTemplate = getMockTemplate();
await request(app)
.post('/v2/tasks')
@@ -294,7 +355,8 @@ describe('createRouter', () => {
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
expect(broker).toHaveBeenCalledWith(
@@ -313,7 +375,8 @@ describe('createRouter', () => {
})),
output: mockTemplate.spec.output ?? {},
parameters: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
user: {
entity: undefined,
@@ -347,7 +410,8 @@ describe('createRouter', () => {
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
@@ -370,7 +434,8 @@ describe('createRouter', () => {
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
@@ -393,7 +458,8 @@ describe('createRouter', () => {
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
@@ -713,6 +779,7 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
});
});
});
+
describe('providing an identity api', () => {
beforeEach(async () => {
const logger = getVoidLogger();
@@ -752,6 +819,7 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
reader: mockUrlReader,
taskBroker,
identity: { getIdentity },
+ permissionApi,
});
app = express().use(router);
@@ -760,15 +828,26 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
.mockImplementation(async ref => {
const { kind } = parseEntityRef(ref);
- if (kind === 'template') {
- return mockTemplate;
+ if (kind.toLocaleLowerCase() === 'template') {
+ return getMockTemplate();
}
- if (kind === 'user') {
+ if (kind.toLocaleLowerCase() === 'user') {
return mockUser;
}
throw new Error(`no mock found for kind: ${kind}`);
});
+
+ jest
+ .spyOn(permissionApi, 'authorizeConditional')
+ .mockImplementation(async () => [
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ ]);
});
afterEach(() => {
@@ -784,6 +863,125 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
});
});
+ describe('GET /v2/templates/:namespace/:kind/:name/parameter-schema', () => {
+ it('returns the parameter schema', async () => {
+ const response = await request(app)
+ .get(
+ '/v2/templates/default/Template/create-react-app-template/parameter-schema',
+ )
+ .send();
+
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({
+ title: 'Create React App Template',
+ description: 'Create a new CRA website project',
+ steps: [
+ {
+ title: 'Please enter the following information',
+ schema: {
+ required: ['requiredParameter1'],
+ type: 'object',
+ properties: {
+ requiredParameter1: {
+ description: 'Required parameter 1',
+ type: 'string',
+ },
+ },
+ },
+ },
+ {
+ title: 'Please enter the following information',
+ schema: {
+ type: 'object',
+ required: ['requiredParameter2'],
+ 'backstage:permissions': {
+ tags: ['parameters-tag'],
+ },
+ properties: {
+ requiredParameter2: {
+ type: 'string',
+ description: 'Required parameter 2',
+ },
+ },
+ },
+ },
+ ],
+ });
+ });
+
+ it('filters parameters that the user is not authorized to see', async () => {
+ jest
+ .spyOn(permissionApi, 'authorizeConditional')
+ .mockImplementationOnce(async () => [
+ {
+ result: AuthorizeResult.DENY,
+ },
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ ]);
+ const response = await request(app)
+ .get(
+ '/v2/templates/default/Template/create-react-app-template/parameter-schema',
+ )
+ .send();
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({
+ title: 'Create React App Template',
+ description: 'Create a new CRA website project',
+ steps: [],
+ });
+ });
+
+ it('filters parameters that the user is not authorized to see in case of conditional decision', async () => {
+ jest
+ .spyOn(permissionApi, 'authorizeConditional')
+ .mockImplementationOnce(async () => [
+ {
+ conditions: {
+ resourceType: 'scaffolder-template',
+ rule: 'HAS_TAG',
+ params: { tag: 'parameters-tag' },
+ },
+ pluginId: 'scaffolder',
+ resourceType: 'scaffolder-template',
+ result: AuthorizeResult.CONDITIONAL,
+ },
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ ]);
+ const response = await request(app)
+ .get(
+ '/v2/templates/default/Template/create-react-app-template/parameter-schema',
+ )
+ .send();
+ expect(response.status).toEqual(200);
+ expect(response.body).toEqual({
+ title: 'Create React App Template',
+ description: 'Create a new CRA website project',
+ steps: [
+ {
+ title: 'Please enter the following information',
+ schema: {
+ type: 'object',
+ required: ['requiredParameter2'],
+ 'backstage:permissions': {
+ tags: ['parameters-tag'],
+ },
+ properties: {
+ requiredParameter2: {
+ type: 'string',
+ description: 'Required parameter 2',
+ },
+ },
+ },
+ },
+ ],
+ });
+ });
+ });
+
describe('POST /v2/tasks', () => {
it('rejects template values which do not match the template schema definition', async () => {
const response = await request(app)
@@ -801,6 +999,150 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
expect(response.status).toEqual(400);
});
+ it('filters steps that the user is not authorized to see', async () => {
+ jest
+ .spyOn(permissionApi, 'authorizeConditional')
+ .mockImplementation(async () => [
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ {
+ result: AuthorizeResult.DENY,
+ },
+ ]);
+
+ const broker =
+ taskBroker.dispatch as jest.Mocked['dispatch'];
+ const mockTemplate = getMockTemplate();
+
+ await request(app)
+ .post('/v2/tasks')
+ .send({
+ templateRef: stringifyEntityRef({
+ kind: 'template',
+ name: 'create-react-app-template',
+ }),
+ values: {
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
+ },
+ });
+ expect(broker).toHaveBeenCalledWith(
+ expect.objectContaining({
+ createdBy: 'user:default/guest',
+ secrets: {
+ backstageToken: 'token',
+ },
+
+ spec: {
+ apiVersion: mockTemplate.apiVersion,
+ steps: [],
+ output: mockTemplate.spec.output ?? {},
+ parameters: {
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
+ },
+ user: {
+ entity: mockUser,
+ ref: 'user:default/guest',
+ },
+ templateInfo: {
+ entityRef: stringifyEntityRef({
+ kind: 'Template',
+ namespace: 'Default',
+ name: mockTemplate.metadata?.name,
+ }),
+ baseUrl: 'https://dev.azure.com',
+ entity: {
+ metadata: mockTemplate.metadata,
+ },
+ },
+ },
+ }),
+ );
+ });
+
+ it('filters steps that the user is not authorized to see in case of conditional decision', async () => {
+ jest
+ .spyOn(permissionApi, 'authorizeConditional')
+ .mockImplementation(async () => [
+ {
+ result: AuthorizeResult.ALLOW,
+ },
+ {
+ conditions: {
+ resourceType: 'scaffolder-template',
+ rule: 'HAS_TAG',
+ params: { tag: 'steps-tag' },
+ },
+ pluginId: 'scaffolder',
+ resourceType: 'scaffolder-template',
+ result: AuthorizeResult.CONDITIONAL,
+ },
+ ]);
+
+ const broker =
+ taskBroker.dispatch as jest.Mocked['dispatch'];
+ const mockTemplate = getMockTemplate();
+ await request(app)
+ .post('/v2/tasks')
+ .send({
+ templateRef: stringifyEntityRef({
+ kind: 'template',
+ name: 'create-react-app-template',
+ }),
+ values: {
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
+ },
+ });
+ expect(broker).toHaveBeenCalledWith(
+ expect.objectContaining({
+ createdBy: 'user:default/guest',
+ secrets: {
+ backstageToken: 'token',
+ },
+
+ spec: {
+ apiVersion: mockTemplate.apiVersion,
+ steps: [
+ {
+ id: 'step-two',
+ name: 'Second log',
+ action: 'debug:log',
+ input: {
+ message: 'world',
+ },
+ 'backstage:permissions': {
+ tags: ['steps-tag'],
+ },
+ },
+ ],
+ output: mockTemplate.spec.output ?? {},
+ parameters: {
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
+ },
+ user: {
+ entity: mockUser,
+ ref: 'user:default/guest',
+ },
+ templateInfo: {
+ entityRef: stringifyEntityRef({
+ kind: 'Template',
+ namespace: 'Default',
+ name: mockTemplate.metadata?.name,
+ }),
+ baseUrl: 'https://dev.azure.com',
+ entity: {
+ metadata: mockTemplate.metadata,
+ },
+ },
+ },
+ }),
+ );
+ });
+
it('return the template id', async () => {
const broker =
taskBroker.dispatch as jest.Mocked['dispatch'];
@@ -816,7 +1158,8 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
@@ -827,6 +1170,7 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
it('should call the broker with a correct spec', async () => {
const broker =
taskBroker.dispatch as jest.Mocked['dispatch'];
+ const mockTemplate = getMockTemplate();
await request(app)
.post('/v2/tasks')
@@ -836,7 +1180,8 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
expect(broker).toHaveBeenCalledWith(
@@ -855,7 +1200,8 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
})),
output: mockTemplate.spec.output ?? {},
parameters: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
user: {
entity: mockUser,
@@ -898,7 +1244,8 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
expect(response.status).not.toEqual(201);
@@ -930,7 +1277,8 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
@@ -953,7 +1301,8 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
@@ -973,7 +1322,8 @@ data: {"id":1,"taskId":"a-random-id","type":"completion","createdAt":"","body":{
name: 'create-react-app-template',
}),
values: {
- required: 'required-value',
+ requiredParameter1: 'required-value-1',
+ requiredParameter2: 'required-value-2',
},
});
diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts
index dfccc4dc43..1f2d390f9f 100644
--- a/plugins/scaffolder-backend/src/service/router.ts
+++ b/plugins/scaffolder-backend/src/service/router.ts
@@ -18,6 +18,7 @@ import { PluginDatabaseManager, UrlReader } from '@backstage/backend-common';
import { PluginTaskScheduler } from '@backstage/backend-tasks';
import { CatalogApi } from '@backstage/catalog-client';
import {
+ CompoundEntityRef,
Entity,
parseEntityRef,
stringifyEntityRef,
@@ -31,7 +32,15 @@ import {
TaskSpec,
TemplateEntityV1beta3,
templateEntityV1beta3Validator,
+ TemplateParametersV1beta3,
+ TemplateEntityStepV1beta3,
} from '@backstage/plugin-scaffolder-common';
+import {
+ RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
+ scaffolderPermissions,
+ templateParameterReadPermission,
+ templateStepReadPermission,
+} from '@backstage/plugin-scaffolder-common/alpha';
import express from 'express';
import Router from 'express-promise-router';
import { validate } from 'jsonschema';
@@ -53,6 +62,29 @@ import {
IdentityApiGetIdentityRequest,
} from '@backstage/plugin-auth-node';
import { TemplateAction } from '@backstage/plugin-scaffolder-node';
+import {
+ PermissionEvaluator,
+ PermissionRuleParams,
+} from '@backstage/plugin-permission-common';
+import {
+ createConditionAuthorizer,
+ createPermissionIntegrationRouter,
+ PermissionRule,
+} from '@backstage/plugin-permission-node';
+import { scaffolderTemplateRules } from './rules';
+
+/**
+ *
+ * @public
+ */
+export type TemplatePermissionRuleInput<
+ TParams extends PermissionRuleParams = PermissionRuleParams,
+> = PermissionRule<
+ TemplateEntityStepV1beta3 | TemplateParametersV1beta3,
+ {},
+ typeof RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
+ TParams
+>;
/**
* RouterOptions
@@ -80,6 +112,8 @@ export interface RouterOptions {
taskBroker?: TaskBroker;
additionalTemplateFilters?: Record;
additionalTemplateGlobals?: Record;
+ permissionApi?: PermissionEvaluator;
+ permissionRules?: TemplatePermissionRuleInput[];
identity?: IdentityApi;
}
@@ -174,6 +208,8 @@ export async function createRouter(
scheduler,
additionalTemplateFilters,
additionalTemplateGlobals,
+ permissionApi,
+ permissionRules,
} = options;
const logger = parentLogger.child({ plugin: 'scaffolder' });
@@ -249,41 +285,46 @@ export async function createRouter(
additionalTemplateGlobals,
});
+ const templateRules: TemplatePermissionRuleInput[] = Object.values(
+ scaffolderTemplateRules,
+ );
+
+ if (permissionRules) {
+ templateRules.push(...permissionRules);
+ }
+
+ const isAuthorized = createConditionAuthorizer(Object.values(templateRules));
+
+ const permissionIntegrationRouter = createPermissionIntegrationRouter({
+ resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
+ permissions: scaffolderPermissions,
+ rules: templateRules,
+ });
+
+ router.use(permissionIntegrationRouter);
+
router
.get(
'/v2/templates/:namespace/:kind/:name/parameter-schema',
async (req, res) => {
- const { namespace, kind, name } = req.params;
-
const userIdentity = await identity.getIdentity({
request: req,
});
const token = userIdentity?.token;
- const template = await findTemplate({
- catalogApi: catalogClient,
- entityRef: { kind, namespace, name },
- token,
+ const template = await authorizeTemplate(req.params, token);
+
+ const parameters = [template.spec.parameters ?? []].flat();
+ res.json({
+ title: template.metadata.title ?? template.metadata.name,
+ description: template.metadata.description,
+ 'ui:options': template.metadata['ui:options'],
+ steps: parameters.map(schema => ({
+ title: schema.title ?? 'Please enter the following information',
+ description: schema.description,
+ schema,
+ })),
});
- if (isSupportedTemplate(template)) {
- const parameters = [template.spec.parameters ?? []].flat();
- res.json({
- title: template.metadata.title ?? template.metadata.name,
- description: template.metadata.description,
- 'ui:options': template.metadata['ui:options'],
- steps: parameters.map(schema => ({
- title: schema.title ?? 'Please enter the following information',
- description: schema.description,
- schema,
- })),
- });
- } else {
- throw new InputError(
- `Unsupported apiVersion field in schema entity, ${
- (template as Entity).apiVersion
- }`,
- );
- }
},
)
.get('/v2/actions', async (_req, res) => {
@@ -321,22 +362,14 @@ export async function createRouter(
const values = req.body.values;
- const template = await findTemplate({
- catalogApi: catalogClient,
- entityRef: { kind, namespace, name },
+ const template = await authorizeTemplate(
+ { kind, namespace, name },
token,
- });
-
- if (!isSupportedTemplate(template)) {
- throw new InputError(
- `Unsupported apiVersion field in schema entity, ${
- (template as Entity).apiVersion
- }`,
- );
- }
+ );
for (const parameters of [template.spec.parameters ?? []].flat()) {
const result = validate(values, parameters);
+
if (!result.valid) {
res.status(400).json({ errors: result.errors });
return;
@@ -414,6 +447,11 @@ export async function createRouter(
delete task.secrets;
res.status(200).json(task);
})
+ .post('/v2/tasks/:taskId/cancel', async (req, res) => {
+ const { taskId } = req.params;
+ await taskBroker.cancel?.(taskId);
+ res.status(200).json({ status: 'cancelled' });
+ })
.get('/v2/tasks/:taskId/eventstream', async (req, res) => {
const { taskId } = req.params;
const after =
@@ -562,5 +600,56 @@ export async function createRouter(
app.set('logger', logger);
app.use('/', router);
+ async function authorizeTemplate(
+ entityRef: CompoundEntityRef,
+ token: string | undefined,
+ ) {
+ const template = await findTemplate({
+ catalogApi: catalogClient,
+ entityRef,
+ token,
+ });
+
+ if (!isSupportedTemplate(template)) {
+ throw new InputError(
+ `Unsupported apiVersion field in schema entity, ${
+ (template as Entity).apiVersion
+ }`,
+ );
+ }
+
+ if (!permissionApi) {
+ return template;
+ }
+
+ const [parameterDecision, stepDecision] =
+ await permissionApi.authorizeConditional(
+ [
+ { permission: templateParameterReadPermission },
+ { permission: templateStepReadPermission },
+ ],
+ { token },
+ );
+
+ // Authorize parameters
+ if (Array.isArray(template.spec.parameters)) {
+ template.spec.parameters = template.spec.parameters.filter(step =>
+ isAuthorized(parameterDecision, step),
+ );
+ } else if (
+ template.spec.parameters &&
+ !isAuthorized(parameterDecision, template.spec.parameters)
+ ) {
+ template.spec.parameters = undefined;
+ }
+
+ // Authorize steps
+ template.spec.steps = template.spec.steps.filter(step =>
+ isAuthorized(stepDecision, step),
+ );
+
+ return template;
+ }
+
return app;
}
diff --git a/plugins/scaffolder-backend/src/service/rules.test.ts b/plugins/scaffolder-backend/src/service/rules.test.ts
new file mode 100644
index 0000000000..7d8e0eb445
--- /dev/null
+++ b/plugins/scaffolder-backend/src/service/rules.test.ts
@@ -0,0 +1,81 @@
+/*
+ * 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 { hasTag } from './rules';
+
+describe('hasTag', () => {
+ describe('apply', () => {
+ it('returns false when the tag is not present', () => {
+ expect(
+ hasTag.apply(
+ {
+ 'backstage:permissions': {
+ tags: ['foo', 'bar'],
+ },
+ },
+ {
+ tag: 'baz',
+ },
+ ),
+ ).toEqual(false);
+ });
+
+ it('returns false when backstage:permissions is missing', () => {
+ expect(
+ hasTag.apply(
+ {},
+ {
+ tag: 'baz',
+ },
+ ),
+ ).toEqual(false);
+ });
+
+ it('returns false when tags is an empty array', () => {
+ expect(
+ hasTag.apply(
+ {
+ apiVersion: 'backstage.io/v1alpha1',
+ kind: 'Component',
+ metadata: {
+ 'backstage:permissions': {
+ tags: [],
+ },
+ },
+ },
+ {
+ tag: 'baz',
+ },
+ ),
+ ).toEqual(false);
+ });
+
+ it('returns true when the tag is present', () => {
+ expect(
+ hasTag.apply(
+ {
+ 'backstage:permissions': {
+ tags: ['foo', 'bar'],
+ },
+ },
+ {
+ tag: 'bar',
+ },
+ ),
+ ).toEqual(true);
+ });
+ });
+});
diff --git a/plugins/scaffolder-backend/src/service/rules.ts b/plugins/scaffolder-backend/src/service/rules.ts
new file mode 100644
index 0000000000..d94f7dd4f4
--- /dev/null
+++ b/plugins/scaffolder-backend/src/service/rules.ts
@@ -0,0 +1,45 @@
+/*
+ * 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 { makeCreatePermissionRule } from '@backstage/plugin-permission-node';
+import {
+ TemplateEntityStepV1beta3,
+ TemplateParametersV1beta3,
+} from '@backstage/plugin-scaffolder-common';
+import { RESOURCE_TYPE_SCAFFOLDER_TEMPLATE } from '@backstage/plugin-scaffolder-common/alpha';
+
+import { z } from 'zod';
+
+export const createScaffolderPermissionRule = makeCreatePermissionRule<
+ TemplateEntityStepV1beta3 | TemplateParametersV1beta3,
+ {},
+ typeof RESOURCE_TYPE_SCAFFOLDER_TEMPLATE
+>();
+
+export const hasTag = createScaffolderPermissionRule({
+ name: 'HAS_TAG',
+ resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
+ description: `Match parameters or steps with the given tag`,
+ paramsSchema: z.object({
+ tag: z.string().describe('Name of the tag to match on'),
+ }),
+ apply: (resource, { tag }) => {
+ return resource['backstage:permissions']?.tags?.includes(tag) ?? false;
+ },
+ toQuery: () => ({}),
+});
+
+export const scaffolderTemplateRules = { hasTag };
diff --git a/plugins/scaffolder-common/CHANGELOG.md b/plugins/scaffolder-common/CHANGELOG.md
index 4f76c33e36..83e32d4d30 100644
--- a/plugins/scaffolder-common/CHANGELOG.md
+++ b/plugins/scaffolder-common/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-scaffolder-common
+## 1.2.7-next.0
+
+### Patch Changes
+
+- 48da4c46e45: Export `typeguard` for `isTemplateEntityV1beta3`
+- Updated dependencies
+ - @backstage/catalog-model@1.2.1
+ - @backstage/types@1.0.2
+
## 1.2.6
### Patch Changes
diff --git a/plugins/scaffolder-common/alpha-api-report.md b/plugins/scaffolder-common/alpha-api-report.md
new file mode 100644
index 0000000000..e89999ad85
--- /dev/null
+++ b/plugins/scaffolder-common/alpha-api-report.md
@@ -0,0 +1,21 @@
+## API Report File for "@backstage/plugin-scaffolder-common"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { ResourcePermission } from '@backstage/plugin-permission-common';
+
+// @alpha
+export const RESOURCE_TYPE_SCAFFOLDER_TEMPLATE = 'scaffolder-template';
+
+// @alpha
+export const scaffolderPermissions: ResourcePermission<'scaffolder-template'>[];
+
+// @alpha
+export const templateParameterReadPermission: ResourcePermission<'scaffolder-template'>;
+
+// @alpha
+export const templateStepReadPermission: ResourcePermission<'scaffolder-template'>;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/scaffolder-common/api-report.md b/plugins/scaffolder-common/api-report.md
index 8864c0d76d..7c40780b57 100644
--- a/plugins/scaffolder-common/api-report.md
+++ b/plugins/scaffolder-common/api-report.md
@@ -10,6 +10,11 @@ import type { JsonValue } from '@backstage/types';
import { KindValidator } from '@backstage/catalog-model';
import type { UserEntity } from '@backstage/catalog-model';
+// @public
+export const isTemplateEntityV1beta3: (
+ entity: Entity,
+) => entity is TemplateEntityV1beta3;
+
// @public
export type TaskSpec = TaskSpecV1beta3;
@@ -37,20 +42,30 @@ export interface TaskStep {
name: string;
}
+// @public
+export interface TemplateEntityStepV1beta3 extends JsonObject {
+ // (undocumented)
+ 'backstage:permissions'?: TemplatePermissionsV1beta3;
+ // (undocumented)
+ action: string;
+ // (undocumented)
+ id?: string;
+ // (undocumented)
+ if?: string | boolean;
+ // (undocumented)
+ input?: JsonObject;
+ // (undocumented)
+ name?: string;
+}
+
// @public
export interface TemplateEntityV1beta3 extends Entity {
apiVersion: 'scaffolder.backstage.io/v1beta3';
kind: 'Template';
spec: {
type: string;
- parameters?: JsonObject | JsonObject[];
- steps: Array<{
- id?: string;
- name?: string;
- action: string;
- input?: JsonObject;
- if?: string | boolean;
- }>;
+ parameters?: TemplateParametersV1beta3 | TemplateParametersV1beta3[];
+ steps: Array;
output?: {
[name: string]: string;
};
@@ -69,4 +84,16 @@ export type TemplateInfo = {
metadata: EntityMeta;
};
};
+
+// @public
+export interface TemplateParametersV1beta3 extends JsonObject {
+ // (undocumented)
+ 'backstage:permissions'?: TemplatePermissionsV1beta3;
+}
+
+// @public
+export interface TemplatePermissionsV1beta3 extends JsonObject {
+ // (undocumented)
+ tags?: string[];
+}
```
diff --git a/plugins/scaffolder-common/package.json b/plugins/scaffolder-common/package.json
index c08763d98b..41c1b28fc1 100644
--- a/plugins/scaffolder-common/package.json
+++ b/plugins/scaffolder-common/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder-common",
"description": "Common functionalities for the scaffolder, to be shared between scaffolder and scaffolder-backend plugin",
- "version": "1.2.6",
+ "version": "1.2.7-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -11,6 +11,21 @@
"module": "dist/index.esm.js",
"types": "dist/index.d.ts"
},
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
"backstage": {
"role": "common-library"
},
@@ -39,6 +54,7 @@
},
"dependencies": {
"@backstage/catalog-model": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
"@backstage/types": "workspace:^"
},
"devDependencies": {
diff --git a/plugins/scaffolder-common/src/Template.v1beta3.schema.json b/plugins/scaffolder-common/src/Template.v1beta3.schema.json
index e45bfb788d..beb0ca5e31 100644
--- a/plugins/scaffolder-common/src/Template.v1beta3.schema.json
+++ b/plugins/scaffolder-common/src/Template.v1beta3.schema.json
@@ -16,6 +16,9 @@
"owner": "artist-relations-team",
"parameters": {
"required": ["name", "description", "repoUrl"],
+ "backstage:permissions": {
+ "tags": ["one", "two"]
+ },
"properties": {
"name": {
"title": "Name",
@@ -41,6 +44,9 @@
"action": "fetch:plain",
"parameters": {
"url": "./template"
+ },
+ "backstage:permissions": {
+ "tags": ["one", "two"]
}
},
{
@@ -92,14 +98,42 @@
"oneOf": [
{
"type": "object",
- "description": "The JSONSchema describing the inputs for the template."
+ "description": "The JSONSchema describing the inputs for the template.",
+ "properties": {
+ "backstage:permissions": {
+ "type": "object",
+ "description": "Object used for authorizing the parameter",
+ "properties": {
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
},
{
"type": "array",
"description": "A list of separate forms to collect parameters.",
"items": {
"type": "object",
- "description": "The JSONSchema describing the inputs for the template."
+ "description": "The JSONSchema describing the inputs for the template.",
+ "properties": {
+ "backstage:permissions": {
+ "type": "object",
+ "description": "Object used for authorizing the parameter",
+ "properties": {
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ }
+ }
}
}
]
@@ -131,6 +165,18 @@
"if": {
"type": ["string", "boolean"],
"description": "A templated condition that skips the step when evaluated to false. If the condition is true or not defined, the step is executed. The condition is true, if the input is not `false`, `undefined`, `null`, `\"\"`, `0`, or `[]`."
+ },
+ "backstage:permissions": {
+ "type": "object",
+ "description": "Object used for authorizing the step",
+ "properties": {
+ "tags": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
}
}
}
diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts
index ab5900ab32..432292548e 100644
--- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts
+++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.test.ts
@@ -15,7 +15,10 @@
*/
import { entityKindSchemaValidator } from '@backstage/catalog-model';
-import type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3';
+import type {
+ TemplateEntityV1beta3,
+ TemplateParametersV1beta3,
+} from './TemplateEntityV1beta3';
import schema from './Template.v1beta3.schema.json';
const validator = entityKindSchemaValidator(schema);
@@ -35,6 +38,7 @@ describe('templateEntityV1beta3Validator', () => {
owner: 'team-b',
parameters: {
required: ['owner'],
+ 'backstage:permissions': { tags: ['one', 'two'] },
properties: {
owner: {
type: 'string',
@@ -52,6 +56,9 @@ describe('templateEntityV1beta3Validator', () => {
url: './template',
},
if: '${{ parameters.owner }}',
+ 'backstage:permissions': {
+ tags: ['one', 'two'],
+ },
},
],
output: {
@@ -154,4 +161,24 @@ describe('templateEntityV1beta3Validator', () => {
(entity as any).spec.steps[0].if = 5;
expect(() => validator(entity)).toThrow(/if/);
});
+
+ it('rejects parameters with wrong backstage:permissions', async () => {
+ (entity.spec.parameters as TemplateParametersV1beta3)[
+ 'backstage:permissions'
+ ]!.tags = true as unknown as [];
+ expect(() => validator(entity)).toThrow(/must be array/);
+
+ (entity.spec.parameters as TemplateParametersV1beta3)[
+ 'backstage:permissions'
+ ] = true as {};
+ expect(() => validator(entity)).toThrow(/must be object/);
+ });
+
+ it('rejects steps with wrong backstage:permissions', async () => {
+ entity.spec.steps[0]['backstage:permissions']!.tags = true as unknown as [];
+ expect(() => validator(entity)).toThrow(/must be array/);
+
+ entity.spec.steps[0]['backstage:permissions'] = true as {};
+ expect(() => validator(entity)).toThrow(/must be object/);
+ });
});
diff --git a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts
index 8ded484ee2..ebabb5cad4 100644
--- a/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts
+++ b/plugins/scaffolder-common/src/TemplateEntityV1beta3.ts
@@ -50,18 +50,12 @@ export interface TemplateEntityV1beta3 extends Entity {
* to collect user input and validate it against that schema. This can then be used in the `steps` part below to template
* variables passed from the user into each action in the template.
*/
- parameters?: JsonObject | JsonObject[];
+ parameters?: TemplateParametersV1beta3 | TemplateParametersV1beta3[];
/**
* A list of steps to be executed in sequence which are defined by the template. These steps are a list of the underlying
* javascript action and some optional input parameters that may or may not have been collected from the end user.
*/
- steps: Array<{
- id?: string;
- name?: string;
- action: string;
- input?: JsonObject;
- if?: string | boolean;
- }>;
+ steps: Array;
/**
* The output is an object where template authors can pull out information from template actions and return them in a known standard way.
*/
@@ -73,6 +67,38 @@ export interface TemplateEntityV1beta3 extends Entity {
};
}
+/**
+ * Step that is part of a Template Entity.
+ *
+ * @public
+ */
+export interface TemplateEntityStepV1beta3 extends JsonObject {
+ id?: string;
+ name?: string;
+ action: string;
+ input?: JsonObject;
+ if?: string | boolean;
+ 'backstage:permissions'?: TemplatePermissionsV1beta3;
+}
+
+/**
+ * Parameter that is part of a Template Entity.
+ *
+ * @public
+ */
+export interface TemplateParametersV1beta3 extends JsonObject {
+ 'backstage:permissions'?: TemplatePermissionsV1beta3;
+}
+
+/**
+ * Access control properties for parts of a template.
+ *
+ * @public
+ */
+export interface TemplatePermissionsV1beta3 extends JsonObject {
+ tags?: string[];
+}
+
const validator = entityKindSchemaValidator(schema);
/**
@@ -86,3 +112,13 @@ export const templateEntityV1beta3Validator: KindValidator = {
return validator(data) === data;
},
};
+
+/**
+ * Typeguard for filtering entities and ensuring v1beta3 entities
+ * @public
+ */
+export const isTemplateEntityV1beta3 = (
+ entity: Entity,
+): entity is TemplateEntityV1beta3 =>
+ entity.apiVersion === 'scaffolder.backstage.io/v1beta3' &&
+ entity.kind === 'Template';
diff --git a/plugins/scaffolder-common/src/alpha.ts b/plugins/scaffolder-common/src/alpha.ts
new file mode 100644
index 0000000000..9a8c021d15
--- /dev/null
+++ b/plugins/scaffolder-common/src/alpha.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './permissions';
diff --git a/plugins/scaffolder-common/src/index.ts b/plugins/scaffolder-common/src/index.ts
index 521e678ab6..fd34fe6f41 100644
--- a/plugins/scaffolder-common/src/index.ts
+++ b/plugins/scaffolder-common/src/index.ts
@@ -21,5 +21,14 @@
*/
export * from './TaskSpec';
-export { templateEntityV1beta3Validator } from './TemplateEntityV1beta3';
-export type { TemplateEntityV1beta3 } from './TemplateEntityV1beta3';
+
+export {
+ templateEntityV1beta3Validator,
+ isTemplateEntityV1beta3,
+} from './TemplateEntityV1beta3';
+export type {
+ TemplateEntityV1beta3,
+ TemplateEntityStepV1beta3,
+ TemplateParametersV1beta3,
+ TemplatePermissionsV1beta3,
+} from './TemplateEntityV1beta3';
diff --git a/plugins/scaffolder-common/src/permissions.ts b/plugins/scaffolder-common/src/permissions.ts
new file mode 100644
index 0000000000..858430eaf3
--- /dev/null
+++ b/plugins/scaffolder-common/src/permissions.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 { createPermission } from '@backstage/plugin-permission-common';
+
+/**
+ * Permission resource type which corresponds to a scaffolder templates.
+ *
+ * @alpha
+ */
+export const RESOURCE_TYPE_SCAFFOLDER_TEMPLATE = 'scaffolder-template';
+
+/**
+ * This permission is used to authorize actions that involve reading
+ * one or more parameters from a template.
+ *
+ * If this permission is not authorized, it will appear that the
+ * parameter does not exist in the template — both in the frontend
+ * and in API responses.
+ *
+ * @alpha
+ */
+export const templateParameterReadPermission = createPermission({
+ name: 'scaffolder.template.parameter.read',
+ attributes: {
+ action: 'read',
+ },
+ resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
+});
+
+/**
+ * This permission is used to authorize actions that involve reading
+ * one or more steps from a template.
+ *
+ * If this permission is not authorized, it will appear that the
+ * step does not exist in the template — both in the frontend
+ * and in API responses. Steps will also not be executed.
+ *
+ * @alpha
+ */
+export const templateStepReadPermission = createPermission({
+ name: 'scaffolder.template.step.read',
+ attributes: {
+ action: 'read',
+ },
+ resourceType: RESOURCE_TYPE_SCAFFOLDER_TEMPLATE,
+});
+
+/**
+ * List of all the scaffolder permissions
+ * @alpha
+ */
+export const scaffolderPermissions = [
+ templateParameterReadPermission,
+ templateStepReadPermission,
+];
diff --git a/plugins/scaffolder-node/CHANGELOG.md b/plugins/scaffolder-node/CHANGELOG.md
index 663d7b1c39..aa73af9a1f 100644
--- a/plugins/scaffolder-node/CHANGELOG.md
+++ b/plugins/scaffolder-node/CHANGELOG.md
@@ -1,5 +1,16 @@
# @backstage/plugin-scaffolder-node
+## 0.1.2-next.0
+
+### Patch Changes
+
+- e27ddc36dad: Added a possibility to cancel the running task (executing of a scaffolder template)
+- Updated dependencies
+ - @backstage/plugin-scaffolder-common@1.2.7-next.0
+ - @backstage/backend-plugin-api@0.5.1-next.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/types@1.0.2
+
## 0.1.1
### Patch Changes
diff --git a/plugins/scaffolder-node/api-report.md b/plugins/scaffolder-node/api-report.md
index 89b7171ef9..d5b9b73767 100644
--- a/plugins/scaffolder-node/api-report.md
+++ b/plugins/scaffolder-node/api-report.md
@@ -30,6 +30,7 @@ export type ActionContext = {
entity?: UserEntity;
ref?: string;
};
+ signal?: AbortSignal;
};
// @public
diff --git a/plugins/scaffolder-node/package.json b/plugins/scaffolder-node/package.json
index d349e41ec4..624fb4bdd2 100644
--- a/plugins/scaffolder-node/package.json
+++ b/plugins/scaffolder-node/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder-node",
"description": "The plugin-scaffolder-node module for @backstage/plugin-scaffolder-backend",
- "version": "0.1.1",
+ "version": "0.1.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts
index cce149adbe..8827aaf98c 100644
--- a/plugins/scaffolder-node/src/actions/types.ts
+++ b/plugins/scaffolder-node/src/actions/types.ts
@@ -60,6 +60,11 @@ export type ActionContext = {
*/
ref?: string;
};
+
+ /**
+ * Implement the signal to make your custom step abortable https://developer.mozilla.org/en-US/docs/Web/API/AbortController/signal
+ */
+ signal?: AbortSignal;
};
/** @public */
diff --git a/plugins/scaffolder-react/CHANGELOG.md b/plugins/scaffolder-react/CHANGELOG.md
index 744d687349..5993000724 100644
--- a/plugins/scaffolder-react/CHANGELOG.md
+++ b/plugins/scaffolder-react/CHANGELOG.md
@@ -1,5 +1,32 @@
# @backstage/plugin-scaffolder-react
+## 1.3.0-next.0
+
+### Minor Changes
+
+- 259d3407b9b: Move `CategoryPicker` from `scaffolder` into `scaffolder-react`
+ Move `ContextMenu` into `scaffolder-react` and rename it to `ScaffolderPageContextMenu`
+- 2cfd03d7376: To offer better customization options, `ScaffolderPageContextMenu` takes callbacks as props instead of booleans
+- 48da4c46e45: `scaffolder/next`: Export the `TemplateGroupFilter` and `TemplateGroups` and make an extensible component
+
+### Patch Changes
+
+- e27ddc36dad: Added a possibility to cancel the running task (executing of a scaffolder template)
+- 7a6b16cc506: `scaffolder/next`: Bump `@rjsf/*` deps to 5.3.1
+- f84fc7fd040: Updated dependency `@rjsf/validator-ajv8` to `5.3.0`.
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/plugin-scaffolder-common@1.2.7-next.0
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/types@1.0.2
+ - @backstage/version-bridge@1.0.3
+
## 1.2.0
### Minor Changes
diff --git a/plugins/scaffolder-react/alpha-api-report.md b/plugins/scaffolder-react/alpha-api-report.md
index 35029cb796..319e3963f0 100644
--- a/plugins/scaffolder-react/alpha-api-report.md
+++ b/plugins/scaffolder-react/alpha-api-report.md
@@ -132,6 +132,18 @@ export type ReviewStateProps = {
formState: JsonObject;
};
+// @alpha (undocumented)
+export function ScaffolderPageContextMenu(
+ props: ScaffolderPageContextMenuProps,
+): JSX.Element | null;
+
+// @alpha (undocumented)
+export type ScaffolderPageContextMenuProps = {
+ onEditorClicked?: () => void;
+ onActionsClicked?: () => void;
+ onTasksClicked?: () => void;
+};
+
// @alpha
export const Stepper: (stepperProps: StepperProps) => JSX.Element;
@@ -190,9 +202,18 @@ export interface TemplateCardProps {
template: TemplateEntityV1beta3;
}
+// @alpha
+export const TemplateCategoryPicker: () => JSX.Element | null;
+
// @alpha
export const TemplateGroup: (props: TemplateGroupProps) => JSX.Element;
+// @alpha (undocumented)
+export type TemplateGroupFilter = {
+ title?: React_2.ReactNode;
+ filter: (entity: TemplateEntityV1beta3) => boolean;
+};
+
// @alpha
export interface TemplateGroupProps {
// (undocumented)
@@ -214,6 +235,29 @@ export interface TemplateGroupProps {
title: React_2.ReactNode;
}
+// @alpha (undocumented)
+export const TemplateGroups: (props: TemplateGroupsProps) => JSX.Element | null;
+
+// @alpha (undocumented)
+export interface TemplateGroupsProps {
+ // (undocumented)
+ additionalLinksForEntity?: (template: TemplateEntityV1beta3) => {
+ icon: IconComponent;
+ text: string;
+ url: string;
+ }[];
+ // (undocumented)
+ groups: TemplateGroupFilter[];
+ // (undocumented)
+ onTemplateSelected?: (template: TemplateEntityV1beta3) => void;
+ // (undocumented)
+ TemplateCardComponent?: React_2.ComponentType<{
+ template: TemplateEntityV1beta3;
+ }>;
+ // (undocumented)
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
+}
+
// @alpha
export const useFormDataFromQuery: (
initialState?: Record,
diff --git a/plugins/scaffolder-react/api-report.md b/plugins/scaffolder-react/api-report.md
index 0fbc8d0eed..da8309d5ec 100644
--- a/plugins/scaffolder-react/api-report.md
+++ b/plugins/scaffolder-react/api-report.md
@@ -113,7 +113,7 @@ export type ListActionsResponse = Array;
// @public
export type LogEvent = {
- type: 'log' | 'completion';
+ type: 'log' | 'completion' | 'cancelled';
body: {
message: string;
stepId?: string;
@@ -126,6 +126,7 @@ export type LogEvent = {
// @public
export interface ScaffolderApi {
+ cancelTask(taskId: string): Promise;
// (undocumented)
dryRun?(options: ScaffolderDryRunOptions): Promise;
// (undocumented)
@@ -266,10 +267,11 @@ export type ScaffolderTaskOutput = {
// @public
export type ScaffolderTaskStatus =
+ | 'cancelled'
+ | 'completed'
+ | 'failed'
| 'open'
| 'processing'
- | 'failed'
- | 'completed'
| 'skipped';
// @public
@@ -287,6 +289,7 @@ export const SecretsContextProvider: (
// @public
export type TaskStream = {
+ cancelled: boolean;
loading: boolean;
error?: Error;
stepLogs: {
diff --git a/plugins/scaffolder-react/package.json b/plugins/scaffolder-react/package.json
index 012baa6aa5..0cae342b8c 100644
--- a/plugins/scaffolder-react/package.json
+++ b/plugins/scaffolder-react/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder-react",
"description": "A frontend library that helps other Backstage plugins interact with the Scaffolder",
- "version": "1.2.0",
+ "version": "1.3.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -57,14 +57,14 @@
"@backstage/version-bridge": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@react-hookz/web": "^20.0.0",
"@rjsf/core": "^3.2.1",
- "@rjsf/core-v5": "npm:@rjsf/core@5.1.0",
+ "@rjsf/core-v5": "npm:@rjsf/core@5.3.1",
"@rjsf/material-ui": "^3.2.1",
- "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.1.0",
- "@rjsf/utils": "5.1.0",
- "@rjsf/validator-ajv8": "5.1.0",
+ "@rjsf/material-ui-v5": "npm:@rjsf/material-ui@5.3.1",
+ "@rjsf/utils": "5.3.1",
+ "@rjsf/validator-ajv8": "5.3.1",
"@types/json-schema": "^7.0.9",
"classnames": "^2.2.6",
"humanize-duration": "^3.25.1",
diff --git a/plugins/scaffolder-react/src/api/types.ts b/plugins/scaffolder-react/src/api/types.ts
index 0a517e7bff..b39555c14f 100644
--- a/plugins/scaffolder-react/src/api/types.ts
+++ b/plugins/scaffolder-react/src/api/types.ts
@@ -24,10 +24,11 @@ import { TemplateParameterSchema } from '../types';
* @public
*/
export type ScaffolderTaskStatus =
+ | 'cancelled'
+ | 'completed'
+ | 'failed'
| 'open'
| 'processing'
- | 'failed'
- | 'completed'
| 'skipped';
/**
@@ -96,7 +97,7 @@ export type ScaffolderTaskOutput = {
* @public
*/
export type LogEvent = {
- type: 'log' | 'completion';
+ type: 'log' | 'completion' | 'cancelled';
body: {
message: string;
stepId?: string;
@@ -196,6 +197,13 @@ export interface ScaffolderApi {
getTask(taskId: string): Promise;
+ /**
+ * Sends a signal to a task broker to cancel the running task by taskId.
+ *
+ * @param taskId - the id of the task
+ */
+ cancelTask(taskId: string): Promise;
+
listTasks?(options: {
filterByOwnership: 'owned' | 'all';
}): Promise<{ tasks: ScaffolderTask[] }>;
diff --git a/plugins/scaffolder-react/src/hooks/useEventStream.ts b/plugins/scaffolder-react/src/hooks/useEventStream.ts
index d8c9cf4c1f..ec49911471 100644
--- a/plugins/scaffolder-react/src/hooks/useEventStream.ts
+++ b/plugins/scaffolder-react/src/hooks/useEventStream.ts
@@ -44,6 +44,7 @@ export type ScaffolderStep = {
* @public
*/
export type TaskStream = {
+ cancelled: boolean;
loading: boolean;
error?: Error;
stepLogs: { [stepId in string]: string[] };
@@ -66,6 +67,7 @@ type ReducerLogEntry = {
type ReducerAction =
| { type: 'INIT'; data: ScaffolderTask }
+ | { type: 'CANCELLED' }
| { type: 'LOGS'; data: ReducerLogEntry[] }
| { type: 'COMPLETED'; data: ReducerLogEntry }
| { type: 'ERROR'; data: Error };
@@ -111,7 +113,7 @@ function reducer(draft: TaskStream, action: ReducerAction) {
}
if (
- ['cancelled', 'failed', 'completed'].includes(currentStep.status)
+ ['cancelled', 'completed', 'failed'].includes(currentStep.status)
) {
currentStep.endedAt = entry.createdAt;
}
@@ -131,6 +133,11 @@ function reducer(draft: TaskStream, action: ReducerAction) {
return;
}
+ case 'CANCELLED': {
+ draft.cancelled = true;
+ return;
+ }
+
case 'ERROR': {
draft.error = action.data;
draft.loading = false;
@@ -151,6 +158,7 @@ function reducer(draft: TaskStream, action: ReducerAction) {
export const useTaskEventStream = (taskId: string): TaskStream => {
const scaffolderApi = useApi(scaffolderApiRef);
const [state, dispatch] = useImmerReducer(reducer, {
+ cancelled: false,
loading: true,
completed: false,
stepLogs: {} as { [stepId in string]: string[] },
@@ -196,6 +204,9 @@ export const useTaskEventStream = (taskId: string): TaskStream => {
switch (event.type) {
case 'log':
return collectedLogEvents.push(event);
+ case 'cancelled':
+ dispatch({ type: 'CANCELLED' });
+ return undefined;
case 'completion':
emitLogs();
dispatch({ type: 'COMPLETED', data: event });
diff --git a/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx
similarity index 74%
rename from plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx
rename to plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx
index d78b8a62c4..fc3c0249a0 100644
--- a/plugins/scaffolder/src/next/TemplateListPage/ContextMenu.tsx
+++ b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/ScaffolderPageContextMenu.tsx
@@ -14,7 +14,6 @@
* limitations under the License.
*/
-import { useRouteRef } from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import IconButton from '@material-ui/core/IconButton';
import ListItemIcon from '@material-ui/core/ListItemIcon';
@@ -28,12 +27,6 @@ import Edit from '@material-ui/icons/Edit';
import List from '@material-ui/icons/List';
import MoreVert from '@material-ui/icons/MoreVert';
import React, { useState } from 'react';
-import { useNavigate } from 'react-router-dom';
-import {
- nextActionsRouteRef,
- nextEditRouteRef,
- nextScaffolderListTaskRouteRef,
-} from '../routes';
const useStyles = makeStyles((theme: BackstageTheme) => ({
button: {
@@ -41,26 +34,26 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({
},
}));
+/**
+ * @alpha
+ */
export type ScaffolderPageContextMenuProps = {
- editor?: boolean;
- actions?: boolean;
- tasks?: boolean;
+ onEditorClicked?: () => void;
+ onActionsClicked?: () => void;
+ onTasksClicked?: () => void;
};
-export function ContextMenu(props: ScaffolderPageContextMenuProps) {
+/**
+ * @alpha
+ */
+export function ScaffolderPageContextMenu(
+ props: ScaffolderPageContextMenuProps,
+) {
+ const { onEditorClicked, onActionsClicked, onTasksClicked } = props;
const classes = useStyles();
const [anchorEl, setAnchorEl] = useState();
- const editLink = useRouteRef(nextEditRouteRef);
- const actionsLink = useRouteRef(nextActionsRouteRef);
- const tasksLink = useRouteRef(nextScaffolderListTaskRouteRef);
- const navigate = useNavigate();
-
- const showEditor = props.editor !== false;
- const showActions = props.actions !== false;
- const showTasks = props.tasks !== false;
-
- if (!showEditor && !showActions) {
+ if (!onEditorClicked && !onActionsClicked) {
return null;
}
@@ -93,24 +86,24 @@ export function ContextMenu(props: ScaffolderPageContextMenuProps) {
transformOrigin={{ vertical: 'top', horizontal: 'right' }}
>
- {showEditor && (
- navigate(editLink())}>
+ {onEditorClicked && (
+
)}
- {showActions && (
- navigate(actionsLink())}>
+ {onActionsClicked && (
+
)}
- {showTasks && (
- navigate(tasksLink())}>
+ {onTasksClicked && (
+
diff --git a/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/index.ts b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/index.ts
new file mode 100644
index 0000000000..ae6774a1e6
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/ScaffolderPageContextMenu/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 {
+ ScaffolderPageContextMenu,
+ type ScaffolderPageContextMenuProps,
+} from './ScaffolderPageContextMenu';
diff --git a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx
index af44085151..2a0a34823d 100644
--- a/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx
+++ b/plugins/scaffolder-react/src/next/components/TemplateCard/TemplateCard.tsx
@@ -142,20 +142,22 @@ export const TemplateCard = (props: TemplateCardProps) => {
- {props.additionalLinks?.map(({ icon, text, url }) => (
-
+ {props.additionalLinks?.map(({ icon, text, url }, index) => (
+
))}
- {template.metadata.links?.map(({ url, icon, title }) => (
-
-
-
- ))}
+ {template.metadata.links?.map(
+ ({ url, icon, title }, index) => (
+
+
+
+ ),
+ )}
>
diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.test.tsx
similarity index 93%
rename from plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx
rename to plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.test.tsx
index f02ca51354..ce6e342809 100644
--- a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.test.tsx
+++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.test.tsx
@@ -16,7 +16,7 @@
import React from 'react';
import { useEntityTypeFilter } from '@backstage/plugin-catalog-react';
-import { CategoryPicker } from './CategoryPicker';
+import { TemplateCategoryPicker } from './TemplateCategoryPicker';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { alertApiRef } from '@backstage/core-plugin-api';
import { fireEvent } from '@testing-library/react';
@@ -25,7 +25,7 @@ jest.mock('@backstage/plugin-catalog-react', () => ({
useEntityTypeFilter: jest.fn(),
}));
-describe('CategoryPicker', () => {
+describe('TemplateCategoryPicker', () => {
const mockAlertApi = { post: jest.fn() };
beforeEach(() => {
@@ -39,7 +39,7 @@ describe('CategoryPicker', () => {
await renderInTestApp(
-
+
,
);
@@ -56,7 +56,7 @@ describe('CategoryPicker', () => {
const { findByTestId } = await renderInTestApp(
-
+
,
);
@@ -70,7 +70,7 @@ describe('CategoryPicker', () => {
const { queryByText } = await renderInTestApp(
-
+
,
);
@@ -86,7 +86,7 @@ describe('CategoryPicker', () => {
const { getByRole } = await renderInTestApp(
-
+
,
);
@@ -108,7 +108,7 @@ describe('CategoryPicker', () => {
const { getByRole } = await renderInTestApp(
-
+
,
);
@@ -139,7 +139,7 @@ describe('CategoryPicker', () => {
const { getByRole } = await renderInTestApp(
-
+
,
);
diff --git a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx
similarity index 97%
rename from plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx
rename to plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx
index 483751f2e2..028ce3a701 100644
--- a/plugins/scaffolder/src/next/TemplateListPage/CategoryPicker.tsx
+++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/TemplateCategoryPicker.tsx
@@ -37,8 +37,9 @@ const checkedIcon = ;
/**
* The Category Picker that is rendered on the left side for picking
* categories and filtering the template list.
+ * @alpha
*/
-export const CategoryPicker = () => {
+export const TemplateCategoryPicker = () => {
const alertApi = useApi(alertApiRef);
const { error, loading, availableTypes, selectedTypes, setSelectedTypes } =
useEntityTypeFilter();
diff --git a/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/index.ts b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/index.ts
new file mode 100644
index 0000000000..cf41f3f7f9
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/TemplateCategoryPicker/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export { TemplateCategoryPicker } from './TemplateCategoryPicker';
diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx b/plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.test.tsx
similarity index 83%
rename from plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx
rename to plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.test.tsx
index 6fe2d6beb9..c03af41f99 100644
--- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.test.tsx
+++ b/plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.test.tsx
@@ -28,7 +28,6 @@ import { TemplateGroups } from './TemplateGroups';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { errorApiRef } from '@backstage/core-plugin-api';
import { TemplateGroup } from '@backstage/plugin-scaffolder-react/alpha';
-import { nextRouteRef } from '../routes';
describe('TemplateGroups', () => {
beforeEach(() => jest.clearAllMocks());
@@ -40,11 +39,6 @@ describe('TemplateGroups', () => {
,
- {
- mountedRoutes: {
- '/next': nextRouteRef,
- },
- },
);
expect(await findByTestId('progress')).toBeInTheDocument();
@@ -62,11 +56,6 @@ describe('TemplateGroups', () => {
,
- {
- mountedRoutes: {
- '/next': nextRouteRef,
- },
- },
);
expect(errorApi.post).toHaveBeenCalledWith(mockError);
@@ -83,11 +72,6 @@ describe('TemplateGroups', () => {
,
- {
- mountedRoutes: {
- '/next': nextRouteRef,
- },
- },
);
expect(await findByText(/No templates found/)).toBeInTheDocument();
@@ -104,11 +88,6 @@ describe('TemplateGroups', () => {
,
- {
- mountedRoutes: {
- '/next': nextRouteRef,
- },
- },
);
expect(await findByText(/No templates found/)).toBeInTheDocument();
@@ -144,11 +123,6 @@ describe('TemplateGroups', () => {
true }]} />
,
- {
- mountedRoutes: {
- '/next': nextRouteRef,
- },
- },
);
expect(TemplateGroup).toHaveBeenCalledWith(
@@ -193,11 +167,6 @@ describe('TemplateGroups', () => {
groups={[{ title: 'all', filter: e => e.metadata.name === 't1' }]}
/>
,
- {
- mountedRoutes: {
- '/next': nextRouteRef,
- },
- },
);
expect(TemplateGroup).toHaveBeenCalledWith(
@@ -207,4 +176,47 @@ describe('TemplateGroups', () => {
{},
);
});
+
+ it('should filter out templates based on filter condition', async () => {
+ const mockEntities = [
+ {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
+ kind: 'Template',
+ metadata: {
+ name: 't1',
+ },
+ spec: {},
+ },
+ {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
+ kind: 'Template',
+ metadata: {
+ name: 't2',
+ },
+ spec: {},
+ },
+ ];
+
+ (useEntityList as jest.Mock).mockReturnValue({
+ entities: mockEntities,
+ loading: false,
+ error: null,
+ });
+
+ await renderInTestApp(
+
+ true }]}
+ templateFilter={e => e.metadata.name === 't1'}
+ />
+ ,
+ );
+
+ expect(TemplateGroup).toHaveBeenCalledWith(
+ expect.objectContaining({
+ templates: [expect.objectContaining({ template: mockEntities[1] })],
+ }),
+ {},
+ );
+ });
});
diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx b/plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.tsx
similarity index 60%
rename from plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx
rename to plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.tsx
index 590ef802bd..50be7a4bdf 100644
--- a/plugins/scaffolder/src/next/TemplateListPage/TemplateGroups.tsx
+++ b/plugins/scaffolder-react/src/next/components/TemplateGroups/TemplateGroups.tsx
@@ -15,55 +15,54 @@
*/
import React, { useCallback } from 'react';
-import {
- Entity,
- parseEntityRef,
- stringifyEntityRef,
-} from '@backstage/catalog-model';
import { useEntityList } from '@backstage/plugin-catalog-react';
-import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
-import { Progress, Link, DocsIcon } from '@backstage/core-components';
-import { Typography } from '@material-ui/core';
import {
- errorApiRef,
- useApi,
- useApp,
- useRouteRef,
-} from '@backstage/core-plugin-api';
+ isTemplateEntityV1beta3,
+ TemplateEntityV1beta3,
+} from '@backstage/plugin-scaffolder-common';
+import { Progress, Link } from '@backstage/core-components';
+import { Typography } from '@material-ui/core';
+import { errorApiRef, IconComponent, useApi } from '@backstage/core-plugin-api';
import { TemplateGroup } from '@backstage/plugin-scaffolder-react/alpha';
-import { viewTechDocRouteRef } from '../../routes';
-import { nextSelectedTemplateRouteRef } from '../routes';
-import { useNavigate } from 'react-router-dom';
/**
* @alpha
*/
export type TemplateGroupFilter = {
title?: React.ReactNode;
- filter: (entity: Entity) => boolean;
+ filter: (entity: TemplateEntityV1beta3) => boolean;
};
+/**
+ * @alpha
+ */
export interface TemplateGroupsProps {
groups: TemplateGroupFilter[];
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
TemplateCardComponent?: React.ComponentType<{
template: TemplateEntityV1beta3;
}>;
+ onTemplateSelected?: (template: TemplateEntityV1beta3) => void;
+ additionalLinksForEntity?: (template: TemplateEntityV1beta3) => {
+ icon: IconComponent;
+ text: string;
+ url: string;
+ }[];
}
+/**
+ * @alpha
+ */
export const TemplateGroups = (props: TemplateGroupsProps) => {
const { loading, error, entities } = useEntityList();
- const { groups, TemplateCardComponent } = props;
+ const { groups, templateFilter, TemplateCardComponent, onTemplateSelected } =
+ props;
const errorApi = useApi(errorApiRef);
- const app = useApp();
- const viewTechDocsLink = useRouteRef(viewTechDocRouteRef);
- const templateRoute = useRouteRef(nextSelectedTemplateRouteRef);
- const navigate = useNavigate();
const onSelected = useCallback(
(template: TemplateEntityV1beta3) => {
- const { namespace, name } = parseEntityRef(stringifyEntityRef(template));
- navigate(templateRoute({ namespace, templateName: name }));
+ onTemplateSelected?.(template);
},
- [navigate, templateRoute],
+ [onTemplateSelected],
);
if (loading) {
@@ -91,22 +90,12 @@ export const TemplateGroups = (props: TemplateGroupsProps) => {
<>
{groups.map(({ title, filter }, index) => {
const templates = entities
- .filter((e): e is TemplateEntityV1beta3 => filter(e))
+ .filter(isTemplateEntityV1beta3)
+ .filter(e => (templateFilter ? !templateFilter(e) : true))
+ .filter(filter)
.map(template => {
- const { kind, namespace, name } = parseEntityRef(
- stringifyEntityRef(template),
- );
const additionalLinks =
- template.metadata.annotations?.['backstage.io/techdocs-ref'] &&
- viewTechDocsLink
- ? [
- {
- icon: app.getSystemIcon('docs') ?? DocsIcon,
- text: 'View TechDocs',
- url: viewTechDocsLink({ kind, namespace, name }),
- },
- ]
- : [];
+ props.additionalLinksForEntity?.(template) ?? [];
return {
template,
diff --git a/plugins/scaffolder-react/src/next/components/TemplateGroups/index.ts b/plugins/scaffolder-react/src/next/components/TemplateGroups/index.ts
new file mode 100644
index 0000000000..765883916c
--- /dev/null
+++ b/plugins/scaffolder-react/src/next/components/TemplateGroups/index.ts
@@ -0,0 +1,16 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+export * from './TemplateGroups';
diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx
index 03604881d2..81c5a697e0 100644
--- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx
+++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.test.tsx
@@ -24,10 +24,10 @@ import { act, fireEvent } from '@testing-library/react';
import React from 'react';
import { Workflow } from './Workflow';
import { analyticsApiRef } from '@backstage/core-plugin-api';
-import { ScaffolderApi } from '../../../api/types';
-import { scaffolderApiRef } from '../../../api/ref';
+import { ScaffolderApi, scaffolderApiRef } from '../../../api';
const scaffolderApiMock: jest.Mocked = {
+ cancelTask: jest.fn(),
scaffold: jest.fn(),
getTemplateParameterSchema: jest.fn(),
getIntegrationsList: jest.fn(),
diff --git a/plugins/scaffolder-react/src/next/components/index.ts b/plugins/scaffolder-react/src/next/components/index.ts
index 083d88927e..5bfa5ccdc8 100644
--- a/plugins/scaffolder-react/src/next/components/index.ts
+++ b/plugins/scaffolder-react/src/next/components/index.ts
@@ -17,8 +17,11 @@ export * from './Stepper';
export * from './TemplateCard';
export * from './ReviewState';
export * from './TemplateGroup';
+export * from './TemplateGroups';
export * from './Workflow';
export * from './TemplateOutputs';
export * from './Form';
export * from './TaskSteps';
export * from './TaskLogStream';
+export * from './TemplateCategoryPicker';
+export * from './ScaffolderPageContextMenu';
diff --git a/plugins/scaffolder/CHANGELOG.md b/plugins/scaffolder/CHANGELOG.md
index f304d7b621..22741c6a14 100644
--- a/plugins/scaffolder/CHANGELOG.md
+++ b/plugins/scaffolder/CHANGELOG.md
@@ -1,5 +1,49 @@
# @backstage/plugin-scaffolder
+## 1.13.0-next.0
+
+### Minor Changes
+
+- cdab34fd9a2: scaffolder/next: removing the `routeRefs` and exporting the originals on `scaffolderPlugin.routes.x` instead
+- e5ad1bd61ec: Allow `TemplateListPage` and `TemplateWizardPage` to be passed in as props
+- 92cf86a4b5d: Added a `templateFilter` prop to the ` ` component to allow for filtering of templates through a function.
+- 259d3407b9b: Move `CategoryPicker` from `scaffolder` into `scaffolder-react`
+ Move `ContextMenu` into `scaffolder-react` and rename it to `ScaffolderPageContextMenu`
+
+### Patch Changes
+
+- e27ddc36dad: Added a possibility to cancel the running task (executing of a scaffolder template)
+- 57c1b4752fa: Allow use of `{ exists: true }` value inside filters to filter entities that has that key.
+
+ this example will filter all entities that has the annotation `someAnnotation` set to any value.
+
+ ```yaml
+ ui:options:
+ catalogFilter:
+ kind: Group
+ metadata.annotations.someAnnotation: { exists: true }
+ ```
+
+- 7a6b16cc506: `scaffolder/next`: Bump `@rjsf/*` deps to 5.3.1
+- f84fc7fd040: Updated dependency `@rjsf/validator-ajv8` to `5.3.0`.
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/plugin-scaffolder-react@1.3.0-next.0
+ - @backstage/plugin-scaffolder-common@1.2.7-next.0
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/plugin-catalog-react@1.4.1-next.0
+ - @backstage/integration-react@1.1.12-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/config@1.0.7
+ - @backstage/integration@1.4.3
+ - @backstage/plugin-permission-react@0.4.11
+ - @backstage/catalog-client@1.4.0
+ - @backstage/catalog-model@1.2.1
+ - @backstage/errors@1.1.5
+ - @backstage/theme@0.2.18
+ - @backstage/types@1.0.2
+ - @backstage/plugin-catalog-common@1.0.12
+
## 1.12.0
### Minor Changes
diff --git a/plugins/scaffolder/alpha-api-report.md b/plugins/scaffolder/alpha-api-report.md
index bf37acafc3..6ceacdc008 100644
--- a/plugins/scaffolder/alpha-api-report.md
+++ b/plugins/scaffolder/alpha-api-report.md
@@ -5,16 +5,15 @@
```ts
///
-import { Entity } from '@backstage/catalog-model';
import { FormProps as FormProps_2 } from '@backstage/plugin-scaffolder-react/alpha';
import type { FormProps as FormProps_3 } from '@rjsf/core-v5';
-import { PathParams } from '@backstage/core-plugin-api';
+import { LayoutOptions } from '@backstage/plugin-scaffolder-react';
+import { NextFieldExtensionOptions } from '@backstage/plugin-scaffolder-react/alpha';
import { PropsWithChildren } from 'react';
import { default as React_2 } from 'react';
-import { RouteRef } from '@backstage/core-plugin-api';
import { ScaffolderTaskOutput } from '@backstage/plugin-scaffolder-react';
-import { SubRouteRef } from '@backstage/core-plugin-api';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
+import { TemplateGroupFilter } from '@backstage/plugin-scaffolder-react/alpha';
// @alpha @deprecated
export type FormProps = Pick<
@@ -22,9 +21,6 @@ export type FormProps = Pick<
'transformErrors' | 'noHtml5Validate'
>;
-// @alpha (undocumented)
-export const nextRouteRef: RouteRef;
-
// @alpha
export type NextRouterProps = {
components?: {
@@ -35,8 +31,11 @@ export type NextRouterProps = {
TemplateOutputsComponent?: React_2.ComponentType<{
output?: ScaffolderTaskOutput;
}>;
+ TemplateListPageComponent?: React_2.ComponentType;
+ TemplateWizardPageComponent?: React_2.ComponentType;
};
groups?: TemplateGroupFilter[];
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
FormProps?: FormProps_2;
contextMenu?: {
editor?: boolean;
@@ -51,19 +50,24 @@ export const NextScaffolderPage: (
) => JSX.Element;
// @alpha (undocumented)
-export const nextScaffolderTaskRouteRef: SubRouteRef<
- PathParams<'/tasks/:taskId'>
->;
+export type TemplateListPageProps = {
+ TemplateCardComponent?: React_2.ComponentType<{
+ template: TemplateEntityV1beta3;
+ }>;
+ groups?: TemplateGroupFilter[];
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
+ contextMenu?: {
+ editor?: boolean;
+ actions?: boolean;
+ tasks?: boolean;
+ };
+};
// @alpha (undocumented)
-export const nextSelectedTemplateRouteRef: SubRouteRef<
- PathParams<'/templates/:namespace/:templateName'>
->;
-
-// @alpha (undocumented)
-export type TemplateGroupFilter = {
- title?: React_2.ReactNode;
- filter: (entity: Entity) => boolean;
+export type TemplateWizardPageProps = {
+ customFieldExtensions: NextFieldExtensionOptions[];
+ layouts?: LayoutOptions[];
+ FormProps?: FormProps_2;
};
// (No @packageDocumentation comment for this package)
diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md
index 6b8d6d345f..8f37f7cf50 100644
--- a/plugins/scaffolder/api-report.md
+++ b/plugins/scaffolder/api-report.md
@@ -80,8 +80,22 @@ export const EntityPickerFieldExtension: FieldExtensionComponent_2<
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
catalogFilter?:
- | Record
- | Record[]
+ | Record<
+ string,
+ | string
+ | string[]
+ | {
+ exists?: boolean | undefined;
+ }
+ >
+ | Record<
+ string,
+ | string
+ | string[]
+ | {
+ exists?: boolean | undefined;
+ }
+ >[]
| undefined;
}
>;
@@ -95,8 +109,22 @@ export const EntityPickerFieldSchema: FieldSchema<
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
catalogFilter?:
- | Record
- | Record[]
+ | Record<
+ string,
+ | string
+ | string[]
+ | {
+ exists?: boolean | undefined;
+ }
+ >
+ | Record<
+ string,
+ | string
+ | string[]
+ | {
+ exists?: boolean | undefined;
+ }
+ >[]
| undefined;
}
>;
@@ -212,8 +240,22 @@ export const OwnerPickerFieldExtension: FieldExtensionComponent_2<
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
catalogFilter?:
- | Record
- | Record[]
+ | Record<
+ string,
+ | string
+ | string[]
+ | {
+ exists?: boolean | undefined;
+ }
+ >
+ | Record<
+ string,
+ | string
+ | string[]
+ | {
+ exists?: boolean | undefined;
+ }
+ >[]
| undefined;
}
>;
@@ -226,8 +268,22 @@ export const OwnerPickerFieldSchema: FieldSchema<
allowedKinds?: string[] | undefined;
allowArbitraryValues?: boolean | undefined;
catalogFilter?:
- | Record
- | Record[]
+ | Record<
+ string,
+ | string
+ | string[]
+ | {
+ exists?: boolean | undefined;
+ }
+ >
+ | Record<
+ string,
+ | string
+ | string[]
+ | {
+ exists?: boolean | undefined;
+ }
+ >[]
| undefined;
}
>;
@@ -332,6 +388,7 @@ export type RouterProps = {
title?: React_2.ReactNode;
filter: (entity: Entity) => boolean;
}>;
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
defaultPreviewTemplate?: string;
headerOptions?: {
pageTitleOverride?: string;
@@ -360,6 +417,8 @@ export class ScaffolderClient implements ScaffolderApi_2 {
useLongPollingLogs?: boolean;
});
// (undocumented)
+ cancelTask(taskId: string): Promise;
+ // (undocumented)
dryRun(
options: ScaffolderDryRunOptions_2,
): Promise;
@@ -423,6 +482,9 @@ export const scaffolderPlugin: BackstagePlugin<
PathParams<'/templates/:namespace/:templateName'>
>;
ongoingTask: SubRouteRef>;
+ actions: SubRouteRef;
+ listTasks: SubRouteRef;
+ edit: SubRouteRef;
},
{
registerComponent: ExternalRouteRef;
diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json
index dc75dd1d01..357b727c95 100644
--- a/plugins/scaffolder/package.json
+++ b/plugins/scaffolder/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-scaffolder",
"description": "The Backstage plugin that helps you create new things",
- "version": "1.12.0",
+ "version": "1.13.0-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -65,12 +65,12 @@
"@codemirror/view": "^6.0.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"@react-hookz/web": "^20.0.0",
"@rjsf/core": "^3.2.1",
"@rjsf/material-ui": "^3.2.1",
- "@rjsf/utils": "5.1.0",
- "@rjsf/validator-ajv8": "5.1.0",
+ "@rjsf/utils": "5.3.1",
+ "@rjsf/validator-ajv8": "5.3.1",
"@uiw/react-codemirror": "^4.9.3",
"classnames": "^2.2.6",
"git-url-parse": "^13.0.0",
diff --git a/plugins/scaffolder/src/alpha.ts b/plugins/scaffolder/src/alpha.ts
index a460b0d7cc..d2bd762190 100644
--- a/plugins/scaffolder/src/alpha.ts
+++ b/plugins/scaffolder/src/alpha.ts
@@ -16,10 +16,8 @@
export { NextScaffolderPage } from './plugin';
export {
- nextRouteRef,
- nextScaffolderTaskRouteRef,
- nextSelectedTemplateRouteRef,
- type TemplateGroupFilter,
type NextRouterProps,
type FormProps,
+ type TemplateListPageProps,
+ type TemplateWizardPageProps,
} from './next';
diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts
index 15bd1925b3..7a3f6c14bb 100644
--- a/plugins/scaffolder/src/api.ts
+++ b/plugins/scaffolder/src/api.ts
@@ -229,24 +229,22 @@ export class ScaffolderClient implements ScaffolderApi {
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(
taskId,
)}/eventstream`;
+
+ const processEvent = (event: any) => {
+ if (event.data) {
+ try {
+ subscriber.next(JSON.parse(event.data));
+ } catch (ex) {
+ subscriber.error(ex);
+ }
+ }
+ };
+
const eventSource = new EventSource(url, { withCredentials: true });
- eventSource.addEventListener('log', (event: any) => {
- if (event.data) {
- try {
- subscriber.next(JSON.parse(event.data));
- } catch (ex) {
- subscriber.error(ex);
- }
- }
- });
+ eventSource.addEventListener('log', processEvent);
+ eventSource.addEventListener('cancelled', processEvent);
eventSource.addEventListener('completion', (event: any) => {
- if (event.data) {
- try {
- subscriber.next(JSON.parse(event.data));
- } catch (ex) {
- subscriber.error(ex);
- }
- }
+ processEvent(event);
eventSource.close();
subscriber.complete();
});
@@ -310,4 +308,19 @@ export class ScaffolderClient implements ScaffolderApi {
return await response.json();
}
+
+ async cancelTask(taskId: string): Promise {
+ const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
+ const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/cancel`;
+
+ const response = await this.fetchApi.fetch(url, {
+ method: 'POST',
+ });
+
+ if (!response.ok) {
+ throw await ResponseError.fromResponse(response);
+ }
+
+ return await response.json();
+ }
}
diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx
index 552d0cea6a..90f6f9b393 100644
--- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx
+++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.test.tsx
@@ -25,6 +25,7 @@ import { rootRouteRef } from '../../routes';
const scaffolderApiMock: jest.Mocked = {
scaffold: jest.fn(),
+ cancelTask: jest.fn(),
getTemplateParameterSchema: jest.fn(),
getIntegrationsList: jest.fn(),
getTask: jest.fn(),
diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx
index ccc2bd283b..05d5cc9f0a 100644
--- a/plugins/scaffolder/src/components/Router.tsx
+++ b/plugins/scaffolder/src/components/Router.tsx
@@ -58,6 +58,7 @@ export type RouterProps = {
title?: React.ReactNode;
filter: (entity: Entity) => boolean;
}>;
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
defaultPreviewTemplate?: string;
headerOptions?: {
pageTitleOverride?: string;
@@ -81,7 +82,12 @@ export type RouterProps = {
* @public
*/
export const Router = (props: RouterProps) => {
- const { groups, components = {}, defaultPreviewTemplate } = props;
+ const {
+ groups,
+ templateFilter,
+ components = {},
+ defaultPreviewTemplate,
+ } = props;
const { ReviewStepComponent, TemplateCardComponent, TaskPageComponent } =
components;
@@ -126,6 +132,7 @@ export const Router = (props: RouterProps) => {
element={
boolean;
+ filter: (entity: TemplateEntityV1beta3) => boolean;
}>;
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
contextMenu?: {
editor?: boolean;
actions?: boolean;
@@ -64,13 +64,14 @@ export type ScaffolderPageProps = {
export const ScaffolderPageContents = ({
TemplateCardComponent,
groups,
+ templateFilter,
contextMenu,
headerOptions,
}: ScaffolderPageProps) => {
const registerComponentLink = useRouteRef(registerComponentRouteRef);
const otherTemplatesGroup = {
title: groups ? 'Other Templates' : 'Templates',
- filter: (entity: Entity) => {
+ filter: (entity: TemplateEntityV1beta3) => {
const filtered = (groups ?? []).map(group => group.filter(entity));
return !filtered.some(result => result === true);
},
@@ -123,11 +124,13 @@ export const ScaffolderPageContents = ({
key={index}
TemplateCardComponent={TemplateCardComponent}
group={group}
+ templateFilter={templateFilter}
/>
))}
@@ -140,6 +143,7 @@ export const ScaffolderPageContents = ({
export const ScaffolderPage = ({
TemplateCardComponent,
groups,
+ templateFilter,
contextMenu,
headerOptions,
}: ScaffolderPageProps) => (
@@ -147,6 +151,7 @@ export const ScaffolderPage = ({
diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx
index f78d48e686..78cdc5d935 100644
--- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx
+++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx
@@ -19,11 +19,15 @@ import {
Content,
ErrorPage,
Header,
- Page,
LogViewer,
+ Page,
Progress,
} from '@backstage/core-components';
-import { useRouteRef, useRouteRefParams } from '@backstage/core-plugin-api';
+import {
+ useApi,
+ useRouteRef,
+ useRouteRefParams,
+} from '@backstage/core-plugin-api';
import { BackstageTheme } from '@backstage/theme';
import {
Button,
@@ -59,6 +63,7 @@ import {
scaffolderTaskRouteRef,
selectedTemplateRouteRef,
} from '../../routes';
+import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
// typings are wrong for this library, so fallback to not parsing types.
const humanizeDuration = require('humanize-duration');
@@ -188,9 +193,10 @@ export const TaskStatusStepper = memo(
nonLinear
>
{steps.map((step, index) => {
+ const isCancelled = step.status === 'cancelled';
+ const isActive = step.status === 'processing';
const isCompleted = step.status === 'completed';
const isFailed = step.status === 'failed';
- const isActive = step.status === 'processing';
const isSkipped = step.status === 'skipped';
return (
@@ -199,7 +205,7 @@ export const TaskStatusStepper = memo(
{
const { loadingText } = props;
-
const classes = useStyles();
const navigate = useNavigate();
const rootPath = useRouteRef(rootRouteRef);
+ const scaffolderApi = useApi(scaffolderApiRef);
const templateRoute = useRouteRef(selectedTemplateRouteRef);
const [userSelectedStepId, setUserSelectedStepId] = useState<
string | undefined
>(undefined);
+ const [clickedToCancel, setClickedToCancel] = useState(false);
const [lastActiveStepId, setLastActiveStepId] = useState(
undefined,
);
const { taskId } = useRouteRefParams(scaffolderTaskRouteRef);
const taskStream = useTaskEventStream(taskId);
const completed = taskStream.completed;
+ const taskCancelled = taskStream.cancelled;
const steps = useMemo(
() =>
taskStream.task?.spec.steps.map(step => ({
@@ -295,9 +302,7 @@ export const TaskPage = (props: TaskPageProps) => {
}, [taskStream.stepLogs, currentStepId, loadingText]);
const taskNotFound =
- taskStream.completed === true &&
- taskStream.loading === false &&
- !taskStream.task;
+ taskStream.completed && !taskStream.loading && !taskStream.task;
const { output } = taskStream;
@@ -320,6 +325,11 @@ export const TaskPage = (props: TaskPageProps) => {
);
};
+ const handleCancel = async () => {
+ setClickedToCancel(true);
+ await scaffolderApi.cancelTask(taskId);
+ };
+
return (
{
>
Start Over
+
+ {(taskCancelled || clickedToCancel) && !completed
+ ? 'Cancelling...'
+ : 'Cancel'}
+
diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.test.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.test.tsx
new file mode 100644
index 0000000000..2ea417ad68
--- /dev/null
+++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.test.tsx
@@ -0,0 +1,83 @@
+/*
+ * 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 { screen } from '@testing-library/react';
+import React from 'react';
+import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
+import { TemplateList } from './TemplateList';
+import { rootRouteRef } from '../../routes';
+import {
+ ScmIntegrationsApi,
+ scmIntegrationsApiRef,
+} from '@backstage/integration-react';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
+
+jest.mock('@backstage/plugin-catalog-react', () => ({
+ useEntityList: jest.fn().mockReturnValue({
+ loading: false,
+ entities: [
+ {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
+ kind: 'Template',
+ metadata: {
+ name: 't1',
+ },
+ spec: {},
+ },
+ {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
+ kind: 'Template',
+ metadata: {
+ name: 't2',
+ },
+ spec: {},
+ },
+ ],
+ }),
+ getEntityRelations: jest.fn().mockImplementation(() => []),
+ getEntitySourceLocation: jest.fn().mockImplementation(() => ({})),
+}));
+
+describe('TemplateList', () => {
+ const mockIntegrationsApi: Partial = {
+ byHost: () => ({ type: 'github' }),
+ };
+
+ it('should filter out templates based on provided filter condition', async () => {
+ const TemplateCardComponent = ({
+ template,
+ }: {
+ template: TemplateEntityV1beta3;
+ }) => (
+ {template.metadata.name}
+ );
+
+ await renderInTestApp(
+
+
+ e.metadata.name === 't1'}
+ TemplateCardComponent={TemplateCardComponent}
+ />
+
+ ,
+ { mountedRoutes: { '/': rootRouteRef } },
+ );
+
+ expect(() => screen.getByTestId('t1')).toThrow();
+ expect(screen.getByTestId('t2')).toBeDefined();
+ });
+});
diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx
index 90d1c8cceb..9a88050d74 100644
--- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx
+++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx
@@ -16,7 +16,10 @@
import React, { ComponentType } from 'react';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
-import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
+import {
+ isTemplateEntityV1beta3,
+ TemplateEntityV1beta3,
+} from '@backstage/plugin-scaffolder-common';
import {
Content,
ContentHeader,
@@ -38,8 +41,9 @@ export type TemplateListProps = {
| undefined;
group?: {
title?: React.ReactNode;
- filter: (entity: Entity) => boolean;
+ filter: (entity: TemplateEntityV1beta3) => boolean;
};
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
};
/**
@@ -48,12 +52,14 @@ export type TemplateListProps = {
export const TemplateList = ({
TemplateCardComponent,
group,
+ templateFilter,
}: TemplateListProps) => {
const { loading, error, entities } = useEntityList();
const Card = TemplateCardComponent || TemplateCard;
- const maybeFilteredEntities = group
- ? entities.filter(e => group.filter(e))
- : entities;
+ const templateEntities = entities.filter(isTemplateEntityV1beta3);
+ const maybeFilteredEntities = (
+ group ? templateEntities.filter(group.filter) : templateEntities
+ ).filter(e => (templateFilter ? !templateFilter(e) : true));
const titleComponent: React.ReactNode = (() => {
if (group && group.title) {
diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx
index b4358146fd..344929fa43 100644
--- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx
+++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.test.tsx
@@ -47,6 +47,7 @@ jest.mock('react-router-dom', () => {
});
const scaffolderApiMock: jest.Mocked = {
+ cancelTask: jest.fn(),
scaffold: jest.fn(),
getTemplateParameterSchema: jest.fn(),
getIntegrationsList: jest.fn(),
@@ -338,10 +339,7 @@ describe('TemplatePage', () => {
it('should display a section or property based on a feature flag', async () => {
featureFlagsApiMock.isActive.mockImplementation(flag => {
- if (flag === 'experimental-feature') {
- return true;
- }
- return false;
+ return flag === 'experimental-feature';
});
scaffolderApiMock.getTemplateParameterSchema.mockResolvedValue(
schemaMockValue,
diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx
index 3cc45e6412..937ff7d07a 100644
--- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx
+++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { EntityFilterQuery } from '@backstage/catalog-client';
+import { CATALOG_FILTER_EXISTS } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
@@ -22,6 +22,7 @@ import { FieldProps } from '@rjsf/core';
import { fireEvent } from '@testing-library/react';
import React from 'react';
import { EntityPicker } from './EntityPicker';
+import { EntityPickerProps } from './schema';
const makeEntity = (kind: string, namespace: string, name: string): Entity => ({
apiVersion: 'backstage.io/v1beta1',
@@ -34,15 +35,7 @@ describe(' ', () => {
const onChange = jest.fn();
const schema = {};
const required = false;
- let uiSchema: {
- 'ui:options': {
- allowedKinds?: string[];
- defaultKind?: string;
- allowArbitraryValues?: boolean;
- defaultNamespace?: string | false;
- catalogFilter?: EntityFilterQuery;
- };
- };
+ let uiSchema: EntityPickerProps['uiSchema'];
const rawErrors: string[] = [];
const formData = undefined;
@@ -192,6 +185,59 @@ describe(' ', () => {
],
});
});
+ it('allow single top level filter', async () => {
+ uiSchema = {
+ 'ui:options': {
+ catalogFilter: {
+ kind: ['Group'],
+ 'metadata.name': 'test-entity',
+ },
+ },
+ };
+
+ catalogApi.getEntities.mockResolvedValue({ items: entities });
+
+ await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(catalogApi.getEntities).toHaveBeenCalledWith({
+ filter: {
+ kind: ['Group'],
+ 'metadata.name': 'test-entity',
+ },
+ });
+ });
+
+ it('search for entitities containing an specific key', async () => {
+ const uiSchemaWithBoolean = {
+ 'ui:options': {
+ catalogFilter: [
+ {
+ kind: ['User'],
+ 'metadata.annotation.some/anotation': { exists: true },
+ },
+ ],
+ },
+ };
+
+ await renderInTestApp(
+
+
+ ,
+ );
+
+ expect(catalogApi.getEntities).toHaveBeenCalledWith({
+ filter: [
+ {
+ kind: ['User'],
+ 'metadata.annotation.some/anotation': CATALOG_FILTER_EXISTS,
+ },
+ ],
+ });
+ });
});
describe('catalogFilter should take precedence over allowedKinds', () => {
diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx
index 48d6453ab9..aeb4a52f77 100644
--- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx
+++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx
@@ -13,7 +13,10 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { type EntityFilterQuery } from '@backstage/catalog-client';
+import {
+ type EntityFilterQuery,
+ CATALOG_FILTER_EXISTS,
+} from '@backstage/catalog-client';
import {
Entity,
parseEntityRef,
@@ -31,7 +34,12 @@ import Autocomplete, {
} from '@material-ui/lab/Autocomplete';
import React, { useCallback, useEffect } from 'react';
import useAsync from 'react-use/lib/useAsync';
-import { EntityPickerProps } from './schema';
+import {
+ EntityPickerFilterQueryValue,
+ EntityPickerProps,
+ EntityPickerUiOptions,
+ EntityPickerFilterQuery,
+} from './schema';
export { EntityPickerSchema } from './schema';
@@ -51,12 +59,7 @@ export const EntityPicker = (props: EntityPickerProps) => {
formData,
idSchema,
} = props;
- const allowedKinds = uiSchema['ui:options']?.allowedKinds;
-
- const catalogFilter: EntityFilterQuery | undefined =
- uiSchema['ui:options']?.catalogFilter ||
- (allowedKinds && { kind: allowedKinds });
-
+ const catalogFilter = buildCatalogFilter(uiSchema);
const defaultKind = uiSchema['ui:options']?.defaultKind;
const defaultNamespace =
uiSchema['ui:options']?.defaultNamespace || undefined;
@@ -167,3 +170,71 @@ export const EntityPicker = (props: EntityPickerProps) => {
);
};
+
+/**
+ * Converts a especial `{exists: true}` value to the `CATALOG_FILTER_EXISTS` symbol.
+ *
+ * @param value - The value to convert.
+ * @returns The converted value.
+ */
+function convertOpsValues(
+ value: Exclude>,
+): string | symbol {
+ if (typeof value === 'object' && value.exists) {
+ return CATALOG_FILTER_EXISTS;
+ }
+ return value?.toString();
+}
+
+/**
+ * Converts schema filters to entity filter query, replacing `{exists:true}` values
+ * with the constant `CATALOG_FILTER_EXISTS`.
+ *
+ * @param schemaFilters - An object containing schema filters with keys as filter names
+ * and values as filter values.
+ * @returns An object with the same keys as the input object, but with `{exists:true}` values
+ * transformed to `CATALOG_FILTER_EXISTS` symbol.
+ */
+function convertSchemaFiltersToQuery(
+ schemaFilters: EntityPickerFilterQuery,
+): Exclude> {
+ const query: EntityFilterQuery = {};
+
+ for (const [key, value] of Object.entries(schemaFilters)) {
+ if (Array.isArray(value)) {
+ query[key] = value;
+ } else {
+ query[key] = convertOpsValues(value);
+ }
+ }
+
+ return query;
+}
+
+/**
+ * Builds an `EntityFilterQuery` based on the `uiSchema` passed in.
+ * If `catalogFilter` is specified in the `uiSchema`, it is converted to a `EntityFilterQuery`.
+ * If `allowedKinds` is specified in the `uiSchema` will support the legacy `allowedKinds` option.
+ *
+ * @param uiSchema The `uiSchema` of an `EntityPicker` component.
+ * @returns An `EntityFilterQuery` based on the `uiSchema`, or `undefined` if `catalogFilter` is not specified in the `uiSchema`.
+ */
+function buildCatalogFilter(
+ uiSchema: EntityPickerProps['uiSchema'],
+): EntityFilterQuery | undefined {
+ const allowedKinds = uiSchema['ui:options']?.allowedKinds;
+
+ const catalogFilter: EntityPickerUiOptions['catalogFilter'] | undefined =
+ uiSchema['ui:options']?.catalogFilter ||
+ (allowedKinds && { kind: allowedKinds });
+
+ if (!catalogFilter) {
+ return undefined;
+ }
+
+ if (Array.isArray(catalogFilter)) {
+ return catalogFilter.map(convertSchemaFiltersToQuery);
+ }
+
+ return convertSchemaFiltersToQuery(catalogFilter);
+}
diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts
index f83dfbf817..eb1bba364b 100644
--- a/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts
+++ b/plugins/scaffolder/src/components/fields/EntityPicker/schema.ts
@@ -20,7 +20,10 @@ import { makeFieldSchemaFromZod } from '../utils';
* @public
*/
export const entityQueryFilterExpressionSchema = z.record(
- z.string().or(z.array(z.string())),
+ z
+ .string()
+ .or(z.object({ exists: z.boolean().optional() }))
+ .or(z.array(z.string())),
);
/**
@@ -74,3 +77,10 @@ export type EntityPickerUiOptions =
export type EntityPickerProps = typeof EntityPickerFieldSchema.type;
export const EntityPickerSchema = EntityPickerFieldSchema.schema;
+
+export type EntityPickerFilterQuery = z.TypeOf<
+ typeof entityQueryFilterExpressionSchema
+>;
+
+export type EntityPickerFilterQueryValue =
+ EntityPickerFilterQuery[keyof EntityPickerFilterQuery];
diff --git a/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx
index d4c771a9f8..985dd98903 100644
--- a/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx
+++ b/plugins/scaffolder/src/next/OngoingTask/ContextMenu.tsx
@@ -23,15 +23,21 @@ import {
MenuList,
Popover,
} from '@material-ui/core';
+import { useAsync } from '@react-hookz/web';
+import Cancel from '@material-ui/icons/Cancel';
import Retry from '@material-ui/icons/Repeat';
import Toc from '@material-ui/icons/Toc';
import MoreVert from '@material-ui/icons/MoreVert';
import React, { useState } from 'react';
+import { useApi } from '@backstage/core-plugin-api';
+import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
type ContextMenuProps = {
+ cancelEnabled?: boolean;
logsVisible?: boolean;
- onToggleLogs?: (state: boolean) => void;
onStartOver?: () => void;
+ onToggleLogs?: (state: boolean) => void;
+ taskId?: string;
};
const useStyles = makeStyles((theme: BackstageTheme) => ({
@@ -41,10 +47,18 @@ const useStyles = makeStyles((theme: BackstageTheme) => ({
}));
export const ContextMenu = (props: ContextMenuProps) => {
- const { logsVisible, onToggleLogs, onStartOver } = props;
+ const { cancelEnabled, logsVisible, onStartOver, onToggleLogs, taskId } =
+ props;
const classes = useStyles();
+ const scaffolderApi = useApi(scaffolderApiRef);
const [anchorEl, setAnchorEl] = useState();
+ const [{ status: cancelStatus }, { execute: cancel }] = useAsync(async () => {
+ if (taskId) {
+ await scaffolderApi.cancelTask(taskId);
+ }
+ });
+
return (
<>
{
+
+
+
+
+
+
>
diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.test.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.test.tsx
new file mode 100644
index 0000000000..c31322a9eb
--- /dev/null
+++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.test.tsx
@@ -0,0 +1,84 @@
+/*
+ * 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 { OngoingTask } from './OngoingTask';
+import React from 'react';
+import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
+import { scaffolderApiRef } from '@backstage/plugin-scaffolder-react';
+import { act, fireEvent, waitFor } from '@testing-library/react';
+import { rootRouteRef } from '../../routes';
+
+jest.mock('react-router-dom', () => ({
+ ...jest.requireActual('react-router-dom'),
+ useParams: () => ({ taskId: 'my-task' }),
+}));
+
+jest.mock('@backstage/plugin-scaffolder-react', () => ({
+ ...jest.requireActual('@backstage/plugin-scaffolder-react'),
+ useTaskEventStream: () => ({
+ cancelled: false,
+ loading: true,
+ stepLogs: {},
+ completed: false,
+ steps: {},
+ task: {
+ spec: {
+ steps: [],
+ templateInfo: { entity: { metadata: { name: 'my-template' } } },
+ },
+ },
+ }),
+}));
+
+describe('OngoingTask', () => {
+ const mockScaffolderApi = {
+ cancelTask: jest.fn(),
+ getTask: jest.fn().mockImplementation(async () => {}),
+ };
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('should trigger cancel api on "Cancel" click in context menu', async () => {
+ const cancelOptionLabel = 'Cancel';
+ const rendered = await renderInTestApp(
+
+
+ ,
+ { mountedRoutes: { '/': rootRouteRef } },
+ );
+ const { getByText, getByTestId } = rendered;
+
+ await act(async () => {
+ fireEvent.click(getByTestId('menu-button'));
+ });
+ expect(getByTestId('cancel-task')).not.toHaveClass('Mui-disabled');
+
+ await act(async () => {
+ fireEvent.click(getByText(cancelOptionLabel));
+ });
+
+ expect(mockScaffolderApi.cancelTask).toHaveBeenCalled();
+ await act(async () => {
+ fireEvent.click(getByTestId('menu-button'));
+ });
+
+ await waitFor(() => {
+ expect(getByTestId('cancel-task')).toHaveClass('Mui-disabled');
+ });
+ });
+});
diff --git a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx
index fc1c1a7413..f8026cf073 100644
--- a/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx
+++ b/plugins/scaffolder/src/next/OngoingTask/OngoingTask.tsx
@@ -13,15 +13,15 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import React, { useEffect, useMemo, useState, useCallback } from 'react';
-import { Page, Header, Content, ErrorPanel } from '@backstage/core-components';
+import React, { useCallback, useEffect, useMemo, useState } from 'react';
+import { Content, ErrorPanel, Header, Page } from '@backstage/core-components';
import { useNavigate, useParams } from 'react-router-dom';
import { Box, makeStyles, Paper } from '@material-ui/core';
import {
ScaffolderTaskOutput,
useTaskEventStream,
} from '@backstage/plugin-scaffolder-react';
-import { nextSelectedTemplateRouteRef } from '../routes';
+import { selectedTemplateRouteRef } from '../../routes';
import { useRouteRef } from '@backstage/core-plugin-api';
import qs from 'qs';
import { ContextMenu } from './ContextMenu';
@@ -45,7 +45,7 @@ export const OngoingTask = (props: {
}) => {
// todo(blam): check that task Id actually exists, and that it's valid. otherwise redirect to something more useful.
const { taskId } = useParams();
- const templateRouteRef = useRouteRef(nextSelectedTemplateRouteRef);
+ const templateRouteRef = useRouteRef(selectedTemplateRouteRef);
const navigate = useNavigate();
const taskStream = useTaskEventStream(taskId!);
const classes = useStyles();
@@ -105,6 +105,8 @@ export const OngoingTask = (props: {
const templateName =
taskStream.task?.spec.templateInfo?.entity?.metadata.name;
+ const cancelEnabled = !(taskStream.cancelled || taskStream.completed);
+
return (
diff --git a/plugins/scaffolder/src/next/Router/Router.test.tsx b/plugins/scaffolder/src/next/Router/Router.test.tsx
index 9565572a2f..3d52dd0dd0 100644
--- a/plugins/scaffolder/src/next/Router/Router.test.tsx
+++ b/plugins/scaffolder/src/next/Router/Router.test.tsx
@@ -47,6 +47,21 @@ describe('Router', () => {
expect(TemplateListPage).toHaveBeenCalled();
});
+
+ it('should render user-provided TemplateListPage', async () => {
+ const { getByText } = await renderInTestApp(
+ <>foobar>,
+ }}
+ />,
+ {
+ routeEntries: ['/'],
+ },
+ );
+ expect(getByText('foobar')).toBeInTheDocument();
+ expect(TemplateListPage).not.toHaveBeenCalled();
+ });
});
describe('/templates/:templateName', () => {
@@ -58,6 +73,21 @@ describe('Router', () => {
expect(TemplateWizardPage).toHaveBeenCalled();
});
+ it('should render user-provided TemplateWizardPage', async () => {
+ const { getByText } = await renderInTestApp(
+ <>foobar>,
+ }}
+ />,
+ {
+ routeEntries: ['/templates/default/foo'],
+ },
+ );
+ expect(getByText('foobar')).toBeInTheDocument();
+ expect(TemplateWizardPage).not.toHaveBeenCalled();
+ });
+
it('should pass through the FormProps property', async () => {
const transformErrorsMock = jest.fn();
diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx
index 5e199cae9b..9c50d0d979 100644
--- a/plugins/scaffolder/src/next/Router/Router.tsx
+++ b/plugins/scaffolder/src/next/Router/Router.tsx
@@ -15,11 +15,15 @@
*/
import React, { PropsWithChildren } from 'react';
import { Routes, Route, useOutlet } from 'react-router-dom';
-import { TemplateListPage } from '../TemplateListPage';
-import { TemplateWizardPage } from '../TemplateWizardPage';
+import { TemplateListPage, TemplateListPageProps } from '../TemplateListPage';
+import {
+ TemplateWizardPage,
+ TemplateWizardPageProps,
+} from '../TemplateWizardPage';
import {
NextFieldExtensionOptions,
FormProps,
+ TemplateGroupFilter,
} from '@backstage/plugin-scaffolder-react/alpha';
import {
ScaffolderTaskOutput,
@@ -29,16 +33,15 @@ import {
} from '@backstage/plugin-scaffolder-react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
-import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups';
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default';
import {
- nextActionsRouteRef,
- nextEditRouteRef,
- nextScaffolderListTaskRouteRef,
- nextScaffolderTaskRouteRef,
- nextSelectedTemplateRouteRef,
-} from '../routes';
+ actionsRouteRef,
+ editRouteRef,
+ scaffolderListTaskRouteRef,
+ scaffolderTaskRouteRef,
+ selectedTemplateRouteRef,
+} from '../../routes';
import { ErrorPage } from '@backstage/core-components';
import { OngoingTask } from '../OngoingTask';
import { ActionsPage } from '../../components/ActionsPage';
@@ -59,8 +62,11 @@ export type NextRouterProps = {
TemplateOutputsComponent?: React.ComponentType<{
output?: ScaffolderTaskOutput;
}>;
+ TemplateListPageComponent?: React.ComponentType;
+ TemplateWizardPageComponent?: React.ComponentType;
};
groups?: TemplateGroupFilter[];
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
// todo(blam): rename this to formProps
FormProps?: FormProps;
contextMenu?: {
@@ -84,6 +90,8 @@ export const Router = (props: PropsWithChildren) => {
TemplateCardComponent,
TemplateOutputsComponent,
TaskPageComponent = OngoingTask,
+ TemplateListPageComponent = TemplateListPage,
+ TemplateWizardPageComponent = TemplateWizardPage,
} = {},
} = props;
const outlet = useOutlet() || props.children;
@@ -107,18 +115,19 @@ export const Router = (props: PropsWithChildren) => {
}
/>
- ) => {
}
/>
) => {
}
/>
) => {
}
/>
- } />
+ } />
}
/>
{
@@ -63,7 +63,7 @@ describe('TemplateListPage', () => {
>
,
- { mountedRoutes: { '/': nextRouteRef } },
+ { mountedRoutes: { '/': rootRouteRef } },
);
expect(getByPlaceholderText('Search')).toBeInTheDocument();
@@ -85,7 +85,7 @@ describe('TemplateListPage', () => {
>
,
- { mountedRoutes: { '/': nextRouteRef } },
+ { mountedRoutes: { '/': rootRouteRef } },
);
expect(getByRole('menuitem', { name: /All/ })).toBeInTheDocument();
@@ -108,7 +108,7 @@ describe('TemplateListPage', () => {
>
,
- { mountedRoutes: { '/': nextRouteRef } },
+ { mountedRoutes: { '/': rootRouteRef } },
);
expect(getByText('Categories')).toBeInTheDocument();
@@ -135,4 +135,54 @@ describe('TemplateListPage', () => {
expect(getByText('Tags')).toBeInTheDocument();
});
+
+ describe('scaffolder page context menu', () => {
+ it('should render if context menu props are not set to false', async () => {
+ const { queryByTestId } = await renderInTestApp(
+
+
+ ,
+ { mountedRoutes: { '/': rootRouteRef } },
+ );
+ expect(queryByTestId('menu-button')).toBeInTheDocument();
+ });
+
+ it('should not render if context menu props are set to false', async () => {
+ const { queryByTestId } = await renderInTestApp(
+
+
+ ,
+ { mountedRoutes: { '/': rootRouteRef } },
+ );
+ expect(queryByTestId('menu-button')).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx
index bddc15a61b..863d3009e1 100644
--- a/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx
+++ b/plugins/scaffolder/src/next/TemplateListPage/TemplateListPage.tsx
@@ -14,12 +14,15 @@
* limitations under the License.
*/
-import React from 'react';
+import React, { useCallback } from 'react';
+import { useNavigate } from 'react-router-dom';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
+import { useApp, useRouteRef } from '@backstage/core-plugin-api';
import {
Content,
ContentHeader,
+ DocsIcon,
Header,
Page,
SupportButton,
@@ -32,18 +35,33 @@ import {
CatalogFilterLayout,
UserListPicker,
} from '@backstage/plugin-catalog-react';
-import { CategoryPicker } from './CategoryPicker';
-import { RegisterExistingButton } from './RegisterExistingButton';
-import { useRouteRef } from '@backstage/core-plugin-api';
-import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups';
-import { registerComponentRouteRef } from '../../routes';
-import { ContextMenu } from './ContextMenu';
+import {
+ ScaffolderPageContextMenu,
+ TemplateCategoryPicker,
+ TemplateGroupFilter,
+ TemplateGroups,
+} from '@backstage/plugin-scaffolder-react/alpha';
+import { RegisterExistingButton } from './RegisterExistingButton';
+import {
+ actionsRouteRef,
+ editRouteRef,
+ registerComponentRouteRef,
+ scaffolderListTaskRouteRef,
+ selectedTemplateRouteRef,
+ viewTechDocRouteRef,
+} from '../../routes';
+import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model';
+
+/**
+ * @alpha
+ */
export type TemplateListPageProps = {
TemplateCardComponent?: React.ComponentType<{
template: TemplateEntityV1beta3;
}>;
groups?: TemplateGroupFilter[];
+ templateFilter?: (entity: TemplateEntityV1beta3) => boolean;
contextMenu?: {
editor?: boolean;
actions?: boolean;
@@ -66,14 +84,71 @@ const createGroupsWithOther = (
},
];
+/**
+ * @alpha
+ */
export const TemplateListPage = (props: TemplateListPageProps) => {
const registerComponentLink = useRouteRef(registerComponentRouteRef);
- const { TemplateCardComponent, groups: givenGroups = [] } = props;
+ const {
+ TemplateCardComponent,
+ groups: givenGroups = [],
+ templateFilter,
+ } = props;
+ const navigate = useNavigate();
+ const editorLink = useRouteRef(editRouteRef);
+ const actionsLink = useRouteRef(actionsRouteRef);
+ const tasksLink = useRouteRef(scaffolderListTaskRouteRef);
+ const viewTechDocsLink = useRouteRef(viewTechDocRouteRef);
+ const templateRoute = useRouteRef(selectedTemplateRouteRef);
+ const app = useApp();
const groups = givenGroups.length
? createGroupsWithOther(givenGroups)
: [defaultGroup];
+ const scaffolderPageContextMenuProps = {
+ onEditorClicked:
+ props?.contextMenu?.editor !== false
+ ? () => navigate(editorLink())
+ : undefined,
+ onActionsClicked:
+ props?.contextMenu?.actions !== false
+ ? () => navigate(actionsLink())
+ : undefined,
+ onTasksClicked:
+ props?.contextMenu?.tasks !== false
+ ? () => navigate(tasksLink())
+ : undefined,
+ };
+
+ const additionalLinksForEntity = useCallback(
+ (template: TemplateEntityV1beta3) => {
+ const { kind, namespace, name } = parseEntityRef(
+ stringifyEntityRef(template),
+ );
+ return template.metadata.annotations?.['backstage.io/techdocs-ref'] &&
+ viewTechDocsLink
+ ? [
+ {
+ icon: app.getSystemIcon('docs') ?? DocsIcon,
+ text: 'View TechDocs',
+ url: viewTechDocsLink({ kind, namespace, name }),
+ },
+ ]
+ : [];
+ },
+ [app, viewTechDocsLink],
+ );
+
+ const onTemplateSelected = useCallback(
+ (template: TemplateEntityV1beta3) => {
+ const { namespace, name } = parseEntityRef(stringifyEntityRef(template));
+
+ navigate(templateRoute({ namespace, templateName: name }));
+ },
+ [navigate, templateRoute],
+ );
+
return (
@@ -82,7 +157,7 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
title="Create a new component"
subtitle="Create new software components using standard templates in your organization"
>
-
+
@@ -105,13 +180,16 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
initialFilter="all"
availableFilters={['all', 'starred']}
/>
-
+
diff --git a/plugins/scaffolder/src/next/TemplateListPage/index.ts b/plugins/scaffolder/src/next/TemplateListPage/index.ts
index 0436cfeeeb..c404a9352a 100644
--- a/plugins/scaffolder/src/next/TemplateListPage/index.ts
+++ b/plugins/scaffolder/src/next/TemplateListPage/index.ts
@@ -14,4 +14,4 @@
* limitations under the License.
*/
export { TemplateListPage } from './TemplateListPage';
-export type { TemplateGroupFilter } from './TemplateGroups';
+export type { TemplateListPageProps } from './TemplateListPage';
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx
index c4f9c75ceb..26a9dd946c 100644
--- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.test.tsx
@@ -29,7 +29,6 @@ import {
} from '@backstage/plugin-scaffolder-react';
import { TemplateWizardPage } from './TemplateWizardPage';
import { rootRouteRef } from '../../routes';
-import { nextRouteRef } from '../routes';
jest.mock('react-router-dom', () => {
return {
@@ -41,6 +40,7 @@ jest.mock('react-router-dom', () => {
});
const scaffolderApiMock: jest.Mocked = {
+ cancelTask: jest.fn(),
scaffold: jest.fn(),
getTemplateParameterSchema: jest.fn(),
getIntegrationsList: jest.fn(),
@@ -83,8 +83,7 @@ describe('TemplateWizardPage', () => {
,
{
mountedRoutes: {
- '/create': nextRouteRef,
- '/create-legacy': rootRouteRef,
+ '/create': rootRouteRef,
},
},
);
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx
index 4825715fcb..a7133ae8c7 100644
--- a/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/TemplateWizardPage.tsx
@@ -34,12 +34,16 @@ import {
} from '@backstage/plugin-scaffolder-react/alpha';
import { JsonValue } from '@backstage/types';
import { Header, Page } from '@backstage/core-components';
-import {
- nextRouteRef,
- nextScaffolderTaskRouteRef,
- nextSelectedTemplateRouteRef,
-} from '../routes';
+import {
+ rootRouteRef,
+ scaffolderTaskRouteRef,
+ selectedTemplateRouteRef,
+} from '../../routes';
+
+/**
+ * @alpha
+ */
export type TemplateWizardPageProps = {
customFieldExtensions: NextFieldExtensionOptions[];
layouts?: LayoutOptions[];
@@ -47,13 +51,13 @@ export type TemplateWizardPageProps = {
};
export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
- const rootRef = useRouteRef(nextRouteRef);
- const taskRoute = useRouteRef(nextScaffolderTaskRouteRef);
+ const rootRef = useRouteRef(rootRouteRef);
+ const taskRoute = useRouteRef(scaffolderTaskRouteRef);
const { secrets } = useTemplateSecrets();
const scaffolderApi = useApi(scaffolderApiRef);
const navigate = useNavigate();
const { templateName, namespace } = useRouteRefParams(
- nextSelectedTemplateRouteRef,
+ selectedTemplateRouteRef,
);
const templateRef = stringifyEntityRef({
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/index.ts b/plugins/scaffolder/src/next/TemplateWizardPage/index.ts
index 146a7a9afc..991b754ec9 100644
--- a/plugins/scaffolder/src/next/TemplateWizardPage/index.ts
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/index.ts
@@ -14,3 +14,4 @@
* limitations under the License.
*/
export { TemplateWizardPage } from './TemplateWizardPage';
+export type { TemplateWizardPageProps } from './TemplateWizardPage';
diff --git a/plugins/scaffolder/src/next/index.ts b/plugins/scaffolder/src/next/index.ts
index 1ca9096459..6cef3eaa03 100644
--- a/plugins/scaffolder/src/next/index.ts
+++ b/plugins/scaffolder/src/next/index.ts
@@ -17,4 +17,3 @@ export * from './Router';
export * from './TemplateListPage';
export * from './TemplateWizardPage';
export * from './types';
-export * from './routes';
diff --git a/plugins/scaffolder/src/next/routes.ts b/plugins/scaffolder/src/next/routes.ts
deleted file mode 100644
index 12fad4210a..0000000000
--- a/plugins/scaffolder/src/next/routes.ts
+++ /dev/null
@@ -1,57 +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 { createRouteRef, createSubRouteRef } from '@backstage/core-plugin-api';
-
-/** @alpha */
-export const nextRouteRef = createRouteRef({
- id: 'scaffolder/next',
-});
-
-/** @alpha */
-export const nextSelectedTemplateRouteRef = createSubRouteRef({
- id: 'scaffolder/next/selected-template',
- parent: nextRouteRef,
- path: '/templates/:namespace/:templateName',
-});
-
-/** @alpha */
-export const nextScaffolderTaskRouteRef = createSubRouteRef({
- id: 'scaffolder/next/task',
- parent: nextRouteRef,
- path: '/tasks/:taskId',
-});
-
-/** @alpha */
-export const nextScaffolderListTaskRouteRef = createSubRouteRef({
- id: 'scaffolder/next/list-tasks',
- parent: nextRouteRef,
- path: '/tasks',
-});
-
-/** @alpha */
-export const nextActionsRouteRef = createSubRouteRef({
- id: 'scaffolder/next/actions',
- parent: nextRouteRef,
- path: '/actions',
-});
-
-/** @alpha */
-export const nextEditRouteRef = createSubRouteRef({
- id: 'scaffolder/next/edit',
- parent: nextRouteRef,
- path: '/edit',
-});
diff --git a/plugins/scaffolder/src/plugin.tsx b/plugins/scaffolder/src/plugin.tsx
index f6801c6371..dfe87a3780 100644
--- a/plugins/scaffolder/src/plugin.tsx
+++ b/plugins/scaffolder/src/plugin.tsx
@@ -60,8 +60,10 @@ import {
viewTechDocRouteRef,
selectedTemplateRouteRef,
scaffolderTaskRouteRef,
+ scaffolderListTaskRouteRef,
+ actionsRouteRef,
+ editRouteRef,
} from './routes';
-import { nextRouteRef } from './next';
/**
* The main plugin export for the scaffolder.
@@ -91,6 +93,9 @@ export const scaffolderPlugin = createPlugin({
root: rootRouteRef,
selectedTemplate: selectedTemplateRouteRef,
ongoingTask: scaffolderTaskRouteRef,
+ actions: actionsRouteRef,
+ listTasks: scaffolderListTaskRouteRef,
+ edit: editRouteRef,
},
externalRoutes: {
registerComponent: registerComponentRouteRef,
@@ -199,6 +204,6 @@ export const NextScaffolderPage = scaffolderPlugin.provide(
createRoutableExtension({
name: 'NextScaffolderPage',
component: () => import('./next/Router').then(m => m.Router),
- mountPoint: nextRouteRef,
+ mountPoint: rootRouteRef,
}),
);
diff --git a/plugins/search-backend-module-catalog/.eslintrc.js b/plugins/search-backend-module-catalog/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/search-backend-module-catalog/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/search-backend-module-catalog/README.md b/plugins/search-backend-module-catalog/README.md
new file mode 100644
index 0000000000..b5960e2789
--- /dev/null
+++ b/plugins/search-backend-module-catalog/README.md
@@ -0,0 +1,41 @@
+# search-backend-module-catalog
+
+> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below.
+
+This package exports catalog backend modules responsible for extending search.
+
+## Example
+
+### Use default schedule
+
+```tsx
+// packages/backend-next/src/index.ts
+import { createBackend } from '@backstage/backend-defaults';
+import { searchPlugin } from '@backstage/plugin-search-backend/alpha';
+import { searchModuleCatalogCollator } from '@backstage/plugin-search-backend-module-catalog/alpha';
+
+const backend = createBackend();
+backend.add(searchPlugin());
+backend.add(searchModuleCatalogCollator());
+backend.start();
+```
+
+### Use custom schedule
+
+```tsx
+// packages/backend-next/src/index.ts
+import { createBackend } from '@backstage/backend-defaults';
+import { searchPlugin } from '@backstage/plugin-search-backend/alpha';
+import { searchModuleCatalogCollator } from '@backstage/plugin-search-backend-module-catalog/alpha';
+
+const schedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+};
+
+const backend = createBackend();
+backend.add(searchPlugin());
+backend.add(searchModuleCatalogCollator({ schedule }));
+backend.start();
+```
diff --git a/plugins/search-backend-module-catalog/alpha-api-report.md b/plugins/search-backend-module-catalog/alpha-api-report.md
new file mode 100644
index 0000000000..70fae9ef93
--- /dev/null
+++ b/plugins/search-backend-module-catalog/alpha-api-report.md
@@ -0,0 +1,24 @@
+## API Report File for "@backstage/plugin-search-backend-module-catalog"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { DefaultCatalogCollatorFactoryOptions } from '@backstage/plugin-search-backend-module-catalog';
+import { TaskScheduleDefinition } from '@backstage/backend-tasks';
+
+// @alpha
+export const searchModuleCatalogCollator: (
+ options?: SearchModuleCatalogCollatorOptions | undefined,
+) => BackendFeature;
+
+// @alpha
+export type SearchModuleCatalogCollatorOptions = Omit<
+ DefaultCatalogCollatorFactoryOptions,
+ 'discovery' | 'tokenManager'
+> & {
+ schedule?: TaskScheduleDefinition;
+};
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/search-backend-module-catalog/api-report.md b/plugins/search-backend-module-catalog/api-report.md
new file mode 100644
index 0000000000..b773860fe8
--- /dev/null
+++ b/plugins/search-backend-module-catalog/api-report.md
@@ -0,0 +1,52 @@
+## API Report File for "@backstage/plugin-search-backend-module-catalog"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { CatalogApi } from '@backstage/catalog-client';
+import { CatalogEntityDocument } from '@backstage/plugin-catalog-common';
+import { Config } from '@backstage/config';
+import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
+import { Entity } from '@backstage/catalog-model';
+import { GetEntitiesRequest } from '@backstage/catalog-client';
+import { Permission } from '@backstage/plugin-permission-common';
+import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { Readable } from 'stream';
+import { TokenManager } from '@backstage/backend-common';
+
+// @public (undocumented)
+export type CatalogCollatorEntityTransformer = (
+ entity: Entity,
+) => Omit;
+
+// @public (undocumented)
+export const defaultCatalogCollatorEntityTransformer: CatalogCollatorEntityTransformer;
+
+// @public (undocumented)
+export class DefaultCatalogCollatorFactory implements DocumentCollatorFactory {
+ // (undocumented)
+ static fromConfig(
+ _config: Config,
+ options: DefaultCatalogCollatorFactoryOptions,
+ ): DefaultCatalogCollatorFactory;
+ // (undocumented)
+ getCollator(): Promise;
+ // (undocumented)
+ readonly type = 'software-catalog';
+ // (undocumented)
+ readonly visibilityPermission: Permission;
+}
+
+// @public (undocumented)
+export type DefaultCatalogCollatorFactoryOptions = {
+ discovery: PluginEndpointDiscovery;
+ tokenManager: TokenManager;
+ locationTemplate?: string;
+ filter?: GetEntitiesRequest['filter'];
+ batchSize?: number;
+ catalogClient?: CatalogApi;
+ entityTransformer?: CatalogCollatorEntityTransformer;
+};
+```
diff --git a/plugins/search-backend-module-catalog/package.json b/plugins/search-backend-module-catalog/package.json
new file mode 100644
index 0000000000..4c323ae7fd
--- /dev/null
+++ b/plugins/search-backend-module-catalog/package.json
@@ -0,0 +1,59 @@
+{
+ "name": "@backstage/plugin-search-backend-module-catalog",
+ "description": "A module for the search backend that exports catalog modules",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/backend-tasks": "workspace:^",
+ "@backstage/catalog-client": "workspace:^",
+ "@backstage/catalog-model": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "@backstage/plugin-catalog-common": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
+ "@backstage/plugin-search-backend-node": "workspace:^",
+ "@backstage/plugin-search-common": "workspace:^"
+ },
+ "devDependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "msw": "^1.0.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/search-backend-module-catalog/src/alpha.test.ts b/plugins/search-backend-module-catalog/src/alpha.test.ts
new file mode 100644
index 0000000000..8a3082c6aa
--- /dev/null
+++ b/plugins/search-backend-module-catalog/src/alpha.test.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { startTestBackend } from '@backstage/backend-test-utils';
+import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+import { searchModuleCatalogCollator } from './alpha';
+
+describe('searchModuleCatalogCollator', () => {
+ const schedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+ };
+
+ it('should register the catalog collator to the search index registry extension point with factory and schedule', async () => {
+ const extensionPointMock = {
+ addCollator: jest.fn(),
+ };
+
+ await startTestBackend({
+ extensionPoints: [
+ [searchIndexRegistryExtensionPoint, extensionPointMock],
+ ],
+ features: [searchModuleCatalogCollator({ schedule })],
+ });
+
+ expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1);
+ expect(extensionPointMock.addCollator).toHaveBeenCalledWith({
+ factory: expect.objectContaining({ type: 'software-catalog' }),
+ schedule: expect.objectContaining({ run: expect.any(Function) }),
+ });
+ });
+});
diff --git a/plugins/search-backend-module-catalog/src/alpha.ts b/plugins/search-backend-module-catalog/src/alpha.ts
new file mode 100644
index 0000000000..c633dc71b2
--- /dev/null
+++ b/plugins/search-backend-module-catalog/src/alpha.ts
@@ -0,0 +1,89 @@
+/*
+ * 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.
+ */
+
+/**
+ * @packageDocumentation
+ * A module for the search backend that exports Catalog modules.
+ */
+
+import {
+ coreServices,
+ createBackendModule,
+} from '@backstage/backend-plugin-api';
+import { TaskScheduleDefinition } from '@backstage/backend-tasks';
+import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+
+import {
+ DefaultCatalogCollatorFactory,
+ DefaultCatalogCollatorFactoryOptions,
+} from '@backstage/plugin-search-backend-module-catalog';
+
+/**
+ * @alpha
+ * Options for {@link searchModuleCatalogCollator}.
+ */
+export type SearchModuleCatalogCollatorOptions = Omit<
+ DefaultCatalogCollatorFactoryOptions,
+ 'discovery' | 'tokenManager'
+> & {
+ schedule?: TaskScheduleDefinition;
+};
+
+/**
+ * @alpha
+ * Search backend module for the Catalog index.
+ */
+export const searchModuleCatalogCollator = createBackendModule(
+ (options?: SearchModuleCatalogCollatorOptions) => ({
+ moduleId: 'catalogCollator',
+ pluginId: 'search',
+ register(env) {
+ env.registerInit({
+ deps: {
+ config: coreServices.config,
+ discovery: coreServices.discovery,
+ tokenManager: coreServices.tokenManager,
+ scheduler: coreServices.scheduler,
+ indexRegistry: searchIndexRegistryExtensionPoint,
+ },
+ async init({
+ config,
+ discovery,
+ tokenManager,
+ scheduler,
+ indexRegistry,
+ }) {
+ const defaultSchedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+ };
+
+ indexRegistry.addCollator({
+ schedule: scheduler.createScheduledTaskRunner(
+ options?.schedule ?? defaultSchedule,
+ ),
+ factory: DefaultCatalogCollatorFactory.fromConfig(config, {
+ ...options,
+ discovery,
+ tokenManager,
+ }),
+ });
+ },
+ });
+ },
+ }),
+);
diff --git a/plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts b/plugins/search-backend-module-catalog/src/collators/CatalogCollatorEntityTransformer.ts
similarity index 100%
rename from plugins/catalog-backend/src/search/CatalogCollatorEntityTransformer.ts
rename to plugins/search-backend-module-catalog/src/collators/CatalogCollatorEntityTransformer.ts
diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts
similarity index 100%
rename from plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.test.ts
rename to plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.test.ts
diff --git a/plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts b/plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts
similarity index 100%
rename from plugins/catalog-backend/src/search/DefaultCatalogCollatorFactory.ts
rename to plugins/search-backend-module-catalog/src/collators/DefaultCatalogCollatorFactory.ts
diff --git a/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.test.ts b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts
similarity index 100%
rename from plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.test.ts
rename to plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.test.ts
diff --git a/plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts b/plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts
similarity index 100%
rename from plugins/catalog-backend/src/search/defaultCatalogCollatorEntityTransformer.ts
rename to plugins/search-backend-module-catalog/src/collators/defaultCatalogCollatorEntityTransformer.ts
diff --git a/plugins/search-backend-module-catalog/src/collators/index.ts b/plugins/search-backend-module-catalog/src/collators/index.ts
new file mode 100644
index 0000000000..5876e71a02
--- /dev/null
+++ b/plugins/search-backend-module-catalog/src/collators/index.ts
@@ -0,0 +1,21 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { DefaultCatalogCollatorFactory } from './DefaultCatalogCollatorFactory';
+export type { DefaultCatalogCollatorFactoryOptions } from './DefaultCatalogCollatorFactory';
+
+export { defaultCatalogCollatorEntityTransformer } from './defaultCatalogCollatorEntityTransformer';
+export type { CatalogCollatorEntityTransformer } from './CatalogCollatorEntityTransformer';
diff --git a/plugins/search-backend-module-catalog/src/index.ts b/plugins/search-backend-module-catalog/src/index.ts
new file mode 100644
index 0000000000..7998562311
--- /dev/null
+++ b/plugins/search-backend-module-catalog/src/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @packageDocumentation
+ * A module for the search backend that exports Catalog modules.
+ */
+
+export * from './collators';
diff --git a/plugins/search-backend-module-elasticsearch/CHANGELOG.md b/plugins/search-backend-module-elasticsearch/CHANGELOG.md
index b72c36dec2..a7ef318ec1 100644
--- a/plugins/search-backend-module-elasticsearch/CHANGELOG.md
+++ b/plugins/search-backend-module-elasticsearch/CHANGELOG.md
@@ -1,5 +1,14 @@
# @backstage/plugin-search-backend-module-elasticsearch
+## 1.1.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/config@1.0.7
+ - @backstage/plugin-search-backend-node@1.1.5-next.0
+ - @backstage/plugin-search-common@1.2.2
+
## 1.1.4
### Patch Changes
diff --git a/plugins/search-backend-module-elasticsearch/alpha-api-report.md b/plugins/search-backend-module-elasticsearch/alpha-api-report.md
new file mode 100644
index 0000000000..91007c20de
--- /dev/null
+++ b/plugins/search-backend-module-elasticsearch/alpha-api-report.md
@@ -0,0 +1,22 @@
+## API Report File for "@backstage/plugin-search-backend-module-elasticsearch"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { ElasticSearchCustomIndexTemplate } from '@backstage/plugin-search-backend-module-elasticsearch';
+import { ElasticSearchQueryTranslator } from '@backstage/plugin-search-backend-module-elasticsearch';
+
+// @alpha
+export const searchModuleElasticsearchEngine: (
+ options?: SearchModuleElasticsearchEngineOptions | undefined,
+) => BackendFeature;
+
+// @alpha
+export type SearchModuleElasticsearchEngineOptions = {
+ translator?: ElasticSearchQueryTranslator;
+ indexTemplate?: ElasticSearchCustomIndexTemplate;
+};
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/search-backend-module-elasticsearch/package.json b/plugins/search-backend-module-elasticsearch/package.json
index e854687360..285ab4e2fe 100644
--- a/plugins/search-backend-module-elasticsearch/package.json
+++ b/plugins/search-backend-module-elasticsearch/package.json
@@ -1,14 +1,27 @@
{
"name": "@backstage/plugin-search-backend-module-elasticsearch",
"description": "A module for the search backend that implements search using ElasticSearch",
- "version": "1.1.4",
+ "version": "1.1.5-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"
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
},
"backstage": {
"role": "backend-plugin-module"
@@ -23,6 +36,8 @@
"clean": "backstage-cli package clean"
},
"dependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-search-backend-node": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
diff --git a/plugins/search-backend-module-elasticsearch/src/alpha.ts b/plugins/search-backend-module-elasticsearch/src/alpha.ts
new file mode 100644
index 0000000000..538124f004
--- /dev/null
+++ b/plugins/search-backend-module-elasticsearch/src/alpha.ts
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ coreServices,
+ createBackendModule,
+} from '@backstage/backend-plugin-api';
+import { loggerToWinstonLogger } from '@backstage/backend-common';
+import { searchEngineRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+
+import {
+ ElasticSearchCustomIndexTemplate,
+ ElasticSearchQueryTranslator,
+ ElasticSearchSearchEngine,
+} from '@backstage/plugin-search-backend-module-elasticsearch';
+
+/**
+ * @alpha
+ * Options for {@link searchModuleElasticsearchEngine}.
+ */
+export type SearchModuleElasticsearchEngineOptions = {
+ translator?: ElasticSearchQueryTranslator;
+ indexTemplate?: ElasticSearchCustomIndexTemplate;
+};
+
+/**
+ * @alpha
+ * Search backend module for the Elasticsearch engine.
+ */
+export const searchModuleElasticsearchEngine = createBackendModule(
+ (options?: SearchModuleElasticsearchEngineOptions) => ({
+ moduleId: 'elasticsearchEngine',
+ pluginId: 'search',
+ register(env) {
+ env.registerInit({
+ deps: {
+ searchEngineRegistry: searchEngineRegistryExtensionPoint,
+ logger: coreServices.logger,
+ config: coreServices.config,
+ },
+ async init({ searchEngineRegistry, logger, config }) {
+ const searchEngine = await ElasticSearchSearchEngine.fromConfig({
+ logger: loggerToWinstonLogger(logger),
+ config: config,
+ });
+
+ // set custom translator if available
+ if (options?.translator) {
+ searchEngine.setTranslator(options.translator);
+ }
+
+ // set custom index template if available
+ if (options?.indexTemplate) {
+ searchEngine.setIndexTemplate(options.indexTemplate);
+ }
+
+ searchEngineRegistry.setSearchEngine(searchEngine);
+ },
+ });
+ },
+ }),
+);
diff --git a/plugins/search-backend-module-explore/.eslintrc.js b/plugins/search-backend-module-explore/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/search-backend-module-explore/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/search-backend-module-explore/README.md b/plugins/search-backend-module-explore/README.md
new file mode 100644
index 0000000000..a87f2d78f9
--- /dev/null
+++ b/plugins/search-backend-module-explore/README.md
@@ -0,0 +1,41 @@
+# search-backend-module-explore
+
+> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below.
+
+This package exports explore backend modules responsible for extending search.
+
+## Example
+
+### Use default schedule
+
+```tsx
+// packages/backend-next/src/index.ts
+import { createBackend } from '@backstage/backend-defaults';
+import { searchPlugin } from '@backstage/plugin-search-backend/alpha';
+import { searchModuleExploreCollator } from '@backstage/plugin-search-backend-module-explore/alpha';
+
+const backend = createBackend();
+backend.add(searchPlugin());
+backend.add(searchModuleExploreCollator());
+backend.start();
+```
+
+### Use custom schedule
+
+```tsx
+// packages/backend-next/src/index.ts
+import { createBackend } from '@backstage/backend-defaults';
+import { searchPlugin } from '@backstage/plugin-search-backend/alpha';
+import { searchModuleExploreCollator } from '@backstage/plugin-search-backend-module-explore/alpha';
+
+const schedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+};
+
+const backend = createBackend();
+backend.add(searchPlugin());
+backend.add(searchModuleExploreCollator({ schedule }));
+backend.start();
+```
diff --git a/plugins/search-backend-module-explore/alpha-api-report.md b/plugins/search-backend-module-explore/alpha-api-report.md
new file mode 100644
index 0000000000..cf32b59dd3
--- /dev/null
+++ b/plugins/search-backend-module-explore/alpha-api-report.md
@@ -0,0 +1,24 @@
+## API Report File for "@backstage/plugin-search-backend-module-explore"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { TaskScheduleDefinition } from '@backstage/backend-tasks';
+import { ToolDocumentCollatorFactoryOptions } from '@backstage/plugin-search-backend-module-explore';
+
+// @alpha
+export const searchModuleExploreCollator: (
+ options?: SearchModuleExploreCollatorOptions | undefined,
+) => BackendFeature;
+
+// @alpha
+export type SearchModuleExploreCollatorOptions = Omit<
+ ToolDocumentCollatorFactoryOptions,
+ 'logger' | 'discovery'
+> & {
+ schedule?: TaskScheduleDefinition;
+};
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/search-backend-module-explore/api-report.md b/plugins/search-backend-module-explore/api-report.md
new file mode 100644
index 0000000000..80e8c0c06b
--- /dev/null
+++ b/plugins/search-backend-module-explore/api-report.md
@@ -0,0 +1,39 @@
+## API Report File for "@backstage/plugin-search-backend-module-explore"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { Config } from '@backstage/config';
+import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
+import { ExploreTool } from '@backstage/plugin-explore-common';
+import { IndexableDocument } from '@backstage/plugin-search-common';
+import { Logger } from 'winston';
+import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { Readable } from 'stream';
+
+// @public
+export interface ToolDocument extends IndexableDocument, ExploreTool {}
+
+// @public
+export class ToolDocumentCollatorFactory implements DocumentCollatorFactory {
+ // (undocumented)
+ execute(): AsyncGenerator;
+ // (undocumented)
+ static fromConfig(
+ _config: Config,
+ options: ToolDocumentCollatorFactoryOptions,
+ ): ToolDocumentCollatorFactory;
+ // (undocumented)
+ getCollator(): Promise;
+ // (undocumented)
+ readonly type: string;
+}
+
+// @public
+export type ToolDocumentCollatorFactoryOptions = {
+ discovery: PluginEndpointDiscovery;
+ logger: Logger;
+};
+```
diff --git a/plugins/search-backend-module-explore/package.json b/plugins/search-backend-module-explore/package.json
new file mode 100644
index 0000000000..e4c71448ff
--- /dev/null
+++ b/plugins/search-backend-module-explore/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "@backstage/plugin-search-backend-module-explore",
+ "description": "A module for the search backend that exports explore modules",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/backend-tasks": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "@backstage/plugin-explore-common": "workspace:^",
+ "@backstage/plugin-search-backend-node": "workspace:^",
+ "@backstage/plugin-search-common": "workspace:^",
+ "node-fetch": "^2.6.7",
+ "winston": "^3.2.1"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/search-backend-module-explore/src/alpha.test.ts b/plugins/search-backend-module-explore/src/alpha.test.ts
new file mode 100644
index 0000000000..9b133f0fd5
--- /dev/null
+++ b/plugins/search-backend-module-explore/src/alpha.test.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { startTestBackend } from '@backstage/backend-test-utils';
+import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+import { searchModuleExploreCollator } from './alpha';
+
+describe('searchModuleExploreCollator', () => {
+ const schedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+ };
+
+ it('should register the explore collator to the search index registry extension point with factory and schedule', async () => {
+ const extensionPointMock = {
+ addCollator: jest.fn(),
+ };
+
+ await startTestBackend({
+ extensionPoints: [
+ [searchIndexRegistryExtensionPoint, extensionPointMock],
+ ],
+ features: [searchModuleExploreCollator({ schedule })],
+ });
+
+ expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1);
+ expect(extensionPointMock.addCollator).toHaveBeenCalledWith({
+ factory: expect.objectContaining({ type: 'tools' }),
+ schedule: expect.objectContaining({ run: expect.any(Function) }),
+ });
+ });
+});
diff --git a/plugins/search-backend-module-explore/src/alpha.ts b/plugins/search-backend-module-explore/src/alpha.ts
new file mode 100644
index 0000000000..f13c3c2406
--- /dev/null
+++ b/plugins/search-backend-module-explore/src/alpha.ts
@@ -0,0 +1,84 @@
+/*
+ * 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.
+ */
+
+/**
+ * @packageDocumentation
+ * A module for the search backend that exports Explore modules.
+ */
+
+import {
+ coreServices,
+ createBackendModule,
+} from '@backstage/backend-plugin-api';
+import { TaskScheduleDefinition } from '@backstage/backend-tasks';
+import { loggerToWinstonLogger } from '@backstage/backend-common';
+import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+
+import {
+ ToolDocumentCollatorFactory,
+ ToolDocumentCollatorFactoryOptions,
+} from '@backstage/plugin-search-backend-module-explore';
+
+/**
+ * @alpha
+ * Options for {@link searchModuleExploreCollator}.
+ */
+export type SearchModuleExploreCollatorOptions = Omit<
+ ToolDocumentCollatorFactoryOptions,
+ 'logger' | 'discovery'
+> & {
+ schedule?: TaskScheduleDefinition;
+};
+
+/**
+ * @alpha
+ * Search backend module for the Explore index.
+ */
+export const searchModuleExploreCollator = createBackendModule(
+ (options?: SearchModuleExploreCollatorOptions) => ({
+ moduleId: 'exploreCollator',
+ pluginId: 'search',
+ register(env) {
+ env.registerInit({
+ deps: {
+ config: coreServices.config,
+ logger: coreServices.logger,
+ discovery: coreServices.discovery,
+ scheduler: coreServices.scheduler,
+ indexRegistry: searchIndexRegistryExtensionPoint,
+ },
+ async init({ config, logger, discovery, scheduler, indexRegistry }) {
+ const defaultSchedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+ };
+
+ indexRegistry.addCollator({
+ schedule: scheduler.createScheduledTaskRunner(
+ options?.schedule ?? defaultSchedule,
+ ),
+ factory: ToolDocumentCollatorFactory.fromConfig(config, {
+ ...options,
+ discovery,
+ logger: loggerToWinstonLogger(logger),
+ }),
+ });
+ },
+ });
+ },
+ }),
+);
diff --git a/plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts b/plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts
similarity index 100%
rename from plugins/explore-backend/src/search/ToolDocumentCollatorFactory.ts
rename to plugins/search-backend-module-explore/src/collators/ToolDocumentCollatorFactory.ts
diff --git a/plugins/explore-backend/src/search/index.ts b/plugins/search-backend-module-explore/src/collators/index.ts
similarity index 99%
rename from plugins/explore-backend/src/search/index.ts
rename to plugins/search-backend-module-explore/src/collators/index.ts
index 70e9414652..45f0c933e2 100644
--- a/plugins/explore-backend/src/search/index.ts
+++ b/plugins/search-backend-module-explore/src/collators/index.ts
@@ -15,6 +15,7 @@
*/
export { ToolDocumentCollatorFactory } from './ToolDocumentCollatorFactory';
+
export type {
ToolDocument,
ToolDocumentCollatorFactoryOptions,
diff --git a/plugins/search-backend-module-explore/src/index.ts b/plugins/search-backend-module-explore/src/index.ts
new file mode 100644
index 0000000000..ea94071984
--- /dev/null
+++ b/plugins/search-backend-module-explore/src/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @packageDocumentation
+ * A module for the search backend that exports Explore modules.
+ */
+
+export * from './collators';
diff --git a/plugins/search-backend-module-pg/CHANGELOG.md b/plugins/search-backend-module-pg/CHANGELOG.md
index 50a9fc9d91..8f3f01e83c 100644
--- a/plugins/search-backend-module-pg/CHANGELOG.md
+++ b/plugins/search-backend-module-pg/CHANGELOG.md
@@ -1,5 +1,15 @@
# @backstage/plugin-search-backend-module-pg
+## 0.5.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/plugin-search-backend-node@1.1.5-next.0
+ - @backstage/plugin-search-common@1.2.2
+
## 0.5.4
### Patch Changes
diff --git a/plugins/search-backend-module-pg/alpha-api-report.md b/plugins/search-backend-module-pg/alpha-api-report.md
new file mode 100644
index 0000000000..eaee78e944
--- /dev/null
+++ b/plugins/search-backend-module-pg/alpha-api-report.md
@@ -0,0 +1,12 @@
+## API Report File for "@backstage/plugin-search-backend-module-pg"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+
+// @alpha
+export const searchModulePostgresEngine: () => BackendFeature;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/search-backend-module-pg/package.json b/plugins/search-backend-module-pg/package.json
index b284557369..28432f2025 100644
--- a/plugins/search-backend-module-pg/package.json
+++ b/plugins/search-backend-module-pg/package.json
@@ -1,14 +1,27 @@
{
"name": "@backstage/plugin-search-backend-module-pg",
"description": "A module for the search backend that implements search using PostgreSQL",
- "version": "0.5.4",
+ "version": "0.5.5-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"
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
},
"backstage": {
"role": "backend-plugin-module"
@@ -24,6 +37,7 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/plugin-search-backend-node": "workspace:^",
"@backstage/plugin-search-common": "workspace:^",
diff --git a/plugins/search-backend-module-pg/src/alpha.ts b/plugins/search-backend-module-pg/src/alpha.ts
new file mode 100644
index 0000000000..e3e0087957
--- /dev/null
+++ b/plugins/search-backend-module-pg/src/alpha.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+import {
+ coreServices,
+ createBackendModule,
+} from '@backstage/backend-plugin-api';
+import { searchEngineRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+import { PgSearchEngine } from './PgSearchEngine';
+
+/**
+ * @alpha
+ * Search backend module for the Postgres engine.
+ */
+export const searchModulePostgresEngine = createBackendModule({
+ moduleId: 'postgresEngine',
+ pluginId: 'search',
+ register(env) {
+ env.registerInit({
+ deps: {
+ searchEngineRegistry: searchEngineRegistryExtensionPoint,
+ database: coreServices.database,
+ config: coreServices.config,
+ },
+ async init({ searchEngineRegistry, database, config }) {
+ searchEngineRegistry.setSearchEngine(
+ await PgSearchEngine.fromConfig(config, {
+ database: database,
+ }),
+ );
+ },
+ });
+ },
+});
diff --git a/plugins/search-backend-module-techdocs/.eslintrc.js b/plugins/search-backend-module-techdocs/.eslintrc.js
new file mode 100644
index 0000000000..e2a53a6ad2
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/.eslintrc.js
@@ -0,0 +1 @@
+module.exports = require('@backstage/cli/config/eslint-factory')(__dirname);
diff --git a/plugins/search-backend-module-techdocs/README.md b/plugins/search-backend-module-techdocs/README.md
new file mode 100644
index 0000000000..5de310466b
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/README.md
@@ -0,0 +1,41 @@
+# search-backend-module-techdocs
+
+> DISCLAIMER: The new backend system is in alpha, and so are the search backend module support for the new backend system. We don't recommend you to migrate your backend installations to the new system yet. But if you want to experiment, you can find getting started guides below.
+
+This package exports techdocs backend modules responsible for extending search.
+
+## Example
+
+### Use default schedule
+
+```tsx
+// packages/backend-next/src/index.ts
+import { createBackend } from '@backstage/backend-defaults';
+import { searchPlugin } from '@backstage/plugin-search-backend/alpha';
+import { searchModuleTechDocsCollator } from '@backstage/plugin-search-backend-module-techdocs/alpha';
+
+const backend = createBackend();
+backend.add(searchPlugin());
+backend.add(searchModuleTechDocsCollator());
+backend.start();
+```
+
+### Use custom schedule
+
+```tsx
+// packages/backend-next/src/index.ts
+import { createBackend } from '@backstage/backend-defaults';
+import { searchPlugin } from '@backstage/plugin-search-backend/alpha';
+import { searchModuleTechDocsCollator } from '@backstage/plugin-search-backend-module-techdocs/alpha';
+
+const schedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+};
+
+const backend = createBackend();
+backend.add(searchPlugin());
+backend.add(searchModuleTechDocsCollator({ schedule }));
+backend.start();
+```
diff --git a/plugins/search-backend-module-techdocs/alpha-api-report.md b/plugins/search-backend-module-techdocs/alpha-api-report.md
new file mode 100644
index 0000000000..71ce09b744
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/alpha-api-report.md
@@ -0,0 +1,24 @@
+## API Report File for "@backstage/plugin-search-backend-module-techdocs"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+import { TaskScheduleDefinition } from '@backstage/backend-tasks';
+import { TechDocsCollatorFactoryOptions } from '@backstage/plugin-search-backend-module-techdocs';
+
+// @alpha
+export const searchModuleTechDocsCollator: (
+ options?: SearchModuleTechDocsCollatorOptions | undefined,
+) => BackendFeature;
+
+// @alpha
+export type SearchModuleTechDocsCollatorOptions = Omit<
+ TechDocsCollatorFactoryOptions,
+ 'logger' | 'discovery' | 'tokenManager'
+> & {
+ schedule?: TaskScheduleDefinition;
+};
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/search-backend-module-techdocs/api-report.md b/plugins/search-backend-module-techdocs/api-report.md
new file mode 100644
index 0000000000..9f72b1c487
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/api-report.md
@@ -0,0 +1,42 @@
+## API Report File for "@backstage/plugin-search-backend-module-techdocs"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+///
+
+import { CatalogApi } from '@backstage/catalog-client';
+import { Config } from '@backstage/config';
+import { DocumentCollatorFactory } from '@backstage/plugin-search-common';
+import { Logger } from 'winston';
+import { Permission } from '@backstage/plugin-permission-common';
+import { PluginEndpointDiscovery } from '@backstage/backend-common';
+import { Readable } from 'stream';
+import { TokenManager } from '@backstage/backend-common';
+
+// @public
+export class DefaultTechDocsCollatorFactory implements DocumentCollatorFactory {
+ // (undocumented)
+ static fromConfig(
+ config: Config,
+ options: TechDocsCollatorFactoryOptions,
+ ): DefaultTechDocsCollatorFactory;
+ // (undocumented)
+ getCollator(): Promise;
+ // (undocumented)
+ readonly type: string;
+ // (undocumented)
+ readonly visibilityPermission: Permission;
+}
+
+// @public
+export type TechDocsCollatorFactoryOptions = {
+ discovery: PluginEndpointDiscovery;
+ logger: Logger;
+ tokenManager: TokenManager;
+ locationTemplate?: string;
+ catalogClient?: CatalogApi;
+ parallelismLimit?: number;
+ legacyPathCasing?: boolean;
+};
+```
diff --git a/plugins/search-backend-module-techdocs/package.json b/plugins/search-backend-module-techdocs/package.json
new file mode 100644
index 0000000000..d08193be29
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/package.json
@@ -0,0 +1,63 @@
+{
+ "name": "@backstage/plugin-search-backend-module-techdocs",
+ "description": "A module for the search backend that exports techdocs modules",
+ "version": "0.0.0",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "license": "Apache-2.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
+ },
+ "backstage": {
+ "role": "backend-plugin-module"
+ },
+ "scripts": {
+ "start": "backstage-cli package start",
+ "build": "backstage-cli package build",
+ "lint": "backstage-cli package lint",
+ "test": "backstage-cli package test",
+ "prepack": "backstage-cli package prepack",
+ "postpack": "backstage-cli package postpack",
+ "clean": "backstage-cli package clean"
+ },
+ "dependencies": {
+ "@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
+ "@backstage/backend-tasks": "workspace:^",
+ "@backstage/catalog-client": "workspace:^",
+ "@backstage/catalog-model": "workspace:^",
+ "@backstage/config": "workspace:^",
+ "@backstage/plugin-catalog-common": "workspace:^",
+ "@backstage/plugin-permission-common": "workspace:^",
+ "@backstage/plugin-search-backend-node": "workspace:^",
+ "@backstage/plugin-search-common": "workspace:^",
+ "@backstage/plugin-techdocs-node": "workspace:^",
+ "lodash": "^4.17.21",
+ "node-fetch": "^2.6.7",
+ "p-limit": "^3.1.0",
+ "winston": "^3.2.1"
+ },
+ "devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
+ "@backstage/cli": "workspace:^",
+ "msw": "^1.0.0"
+ },
+ "files": [
+ "dist"
+ ]
+}
diff --git a/plugins/search-backend-module-techdocs/src/alpha.test.ts b/plugins/search-backend-module-techdocs/src/alpha.test.ts
new file mode 100644
index 0000000000..011b00b12c
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/src/alpha.test.ts
@@ -0,0 +1,46 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { startTestBackend } from '@backstage/backend-test-utils';
+import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+import { searchModuleTechDocsCollator } from './alpha';
+
+describe('searchModuleTechDocsCollator', () => {
+ const schedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+ };
+
+ it('should register the techdocs collator to the search index registry extension point with factory and schedule', async () => {
+ const extensionPointMock = {
+ addCollator: jest.fn(),
+ };
+
+ await startTestBackend({
+ extensionPoints: [
+ [searchIndexRegistryExtensionPoint, extensionPointMock],
+ ],
+ features: [searchModuleTechDocsCollator({ schedule })],
+ });
+
+ expect(extensionPointMock.addCollator).toHaveBeenCalledTimes(1);
+ expect(extensionPointMock.addCollator).toHaveBeenCalledWith({
+ factory: expect.objectContaining({ type: 'techdocs' }),
+ schedule: expect.objectContaining({ run: expect.any(Function) }),
+ });
+ });
+});
diff --git a/plugins/search-backend-module-techdocs/src/alpha.ts b/plugins/search-backend-module-techdocs/src/alpha.ts
new file mode 100644
index 0000000000..1432cc88fb
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/src/alpha.ts
@@ -0,0 +1,93 @@
+/*
+ * 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.
+ */
+
+/**
+ * @packageDocumentation
+ * A module for the search backend that exports TechDocs modules.
+ */
+
+import {
+ coreServices,
+ createBackendModule,
+} from '@backstage/backend-plugin-api';
+import { TaskScheduleDefinition } from '@backstage/backend-tasks';
+import { loggerToWinstonLogger } from '@backstage/backend-common';
+import { searchIndexRegistryExtensionPoint } from '@backstage/plugin-search-backend-node/alpha';
+
+import {
+ DefaultTechDocsCollatorFactory,
+ TechDocsCollatorFactoryOptions,
+} from '@backstage/plugin-search-backend-module-techdocs';
+
+/**
+ * @alpha
+ * Options for {@link searchModuleTechDocsCollator}.
+ */
+export type SearchModuleTechDocsCollatorOptions = Omit<
+ TechDocsCollatorFactoryOptions,
+ 'logger' | 'discovery' | 'tokenManager'
+> & {
+ schedule?: TaskScheduleDefinition;
+};
+
+/**
+ * @alpha
+ * Search backend module for the TechDocs index.
+ */
+export const searchModuleTechDocsCollator = createBackendModule(
+ (options?: SearchModuleTechDocsCollatorOptions) => ({
+ moduleId: 'techDocsCollator',
+ pluginId: 'search',
+ register(env) {
+ env.registerInit({
+ deps: {
+ config: coreServices.config,
+ logger: coreServices.logger,
+ discovery: coreServices.discovery,
+ tokenManager: coreServices.tokenManager,
+ scheduler: coreServices.scheduler,
+ indexRegistry: searchIndexRegistryExtensionPoint,
+ },
+ async init({
+ config,
+ logger,
+ discovery,
+ tokenManager,
+ scheduler,
+ indexRegistry,
+ }) {
+ const defaultSchedule = {
+ frequency: { minutes: 10 },
+ timeout: { minutes: 15 },
+ initialDelay: { seconds: 3 },
+ };
+
+ indexRegistry.addCollator({
+ schedule: scheduler.createScheduledTaskRunner(
+ options?.schedule ?? defaultSchedule,
+ ),
+ factory: DefaultTechDocsCollatorFactory.fromConfig(config, {
+ ...options,
+ discovery,
+ tokenManager,
+ logger: loggerToWinstonLogger(logger),
+ }),
+ });
+ },
+ });
+ },
+ }),
+);
diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts
similarity index 100%
rename from plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.test.ts
rename to plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.test.ts
diff --git a/plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts b/plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts
similarity index 100%
rename from plugins/techdocs-backend/src/search/DefaultTechDocsCollatorFactory.ts
rename to plugins/search-backend-module-techdocs/src/collators/DefaultTechDocsCollatorFactory.ts
diff --git a/plugins/search-backend-module-techdocs/src/collators/index.ts b/plugins/search-backend-module-techdocs/src/collators/index.ts
new file mode 100644
index 0000000000..8c5fd122f1
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/src/collators/index.ts
@@ -0,0 +1,19 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+export { DefaultTechDocsCollatorFactory } from './DefaultTechDocsCollatorFactory';
+
+export type { TechDocsCollatorFactoryOptions } from './DefaultTechDocsCollatorFactory';
diff --git a/plugins/search-backend-module-techdocs/src/index.ts b/plugins/search-backend-module-techdocs/src/index.ts
new file mode 100644
index 0000000000..c28175fd61
--- /dev/null
+++ b/plugins/search-backend-module-techdocs/src/index.ts
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+/**
+ * @packageDocumentation
+ * A module for the search backend that exports TechDocs modules.
+ */
+
+export * from './collators';
diff --git a/plugins/search-backend-node/CHANGELOG.md b/plugins/search-backend-node/CHANGELOG.md
index 69cc5ac718..ed51a90369 100644
--- a/plugins/search-backend-node/CHANGELOG.md
+++ b/plugins/search-backend-node/CHANGELOG.md
@@ -1,5 +1,17 @@
# @backstage/plugin-search-backend-node
+## 1.1.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/backend-tasks@0.5.1-next.0
+ - @backstage/errors@1.1.5
+ - @backstage/plugin-permission-common@0.7.4
+ - @backstage/plugin-search-common@1.2.2
+
## 1.1.4
### Patch Changes
diff --git a/plugins/search-backend-node/alpha-api-report.md b/plugins/search-backend-node/alpha-api-report.md
new file mode 100644
index 0000000000..74b7a4b4bc
--- /dev/null
+++ b/plugins/search-backend-node/alpha-api-report.md
@@ -0,0 +1,50 @@
+## API Report File for "@backstage/plugin-search-backend-node"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { DocumentTypeInfo } from '@backstage/plugin-search-common';
+import { ExtensionPoint } from '@backstage/backend-plugin-api';
+import { RegisterCollatorParameters } from '@backstage/plugin-search-backend-node';
+import { RegisterDecoratorParameters } from '@backstage/plugin-search-backend-node';
+import { SearchEngine } from '@backstage/plugin-search-common';
+import { ServiceRef } from '@backstage/backend-plugin-api';
+
+// @alpha
+export interface SearchEngineRegistryExtensionPoint {
+ // (undocumented)
+ setSearchEngine(searchEngine: SearchEngine): void;
+}
+
+// @alpha
+export const searchEngineRegistryExtensionPoint: ExtensionPoint;
+
+// @alpha
+export interface SearchIndexRegistryExtensionPoint {
+ // (undocumented)
+ addCollator(options: RegisterCollatorParameters): void;
+ // (undocumented)
+ addDecorator(options: RegisterDecoratorParameters): void;
+}
+
+// @alpha
+export const searchIndexRegistryExtensionPoint: ExtensionPoint;
+
+// @alpha
+export interface SearchIndexService {
+ getDocumentTypes(): Record;
+ start(options: SearchIndexServiceStartOptions): Promise;
+}
+
+// @alpha
+export const searchIndexServiceRef: ServiceRef;
+
+// @alpha
+export type SearchIndexServiceStartOptions = {
+ searchEngine: SearchEngine;
+ collators: RegisterCollatorParameters[];
+ decorators: RegisterDecoratorParameters[];
+};
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/search-backend-node/package.json b/plugins/search-backend-node/package.json
index 2abc0a8ef0..a49b6088ab 100644
--- a/plugins/search-backend-node/package.json
+++ b/plugins/search-backend-node/package.json
@@ -1,14 +1,27 @@
{
"name": "@backstage/plugin-search-backend-node",
"description": "A library for Backstage backend plugins that want to interact with the search backend plugin",
- "version": "1.1.4",
+ "version": "1.1.5-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"
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
},
"backstage": {
"role": "node-library"
@@ -24,6 +37,7 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
"@backstage/backend-tasks": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
diff --git a/plugins/search-backend-node/src/alpha.ts b/plugins/search-backend-node/src/alpha.ts
new file mode 100644
index 0000000000..943e11b580
--- /dev/null
+++ b/plugins/search-backend-node/src/alpha.ts
@@ -0,0 +1,159 @@
+/*
+ * Copyright 2023 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { Logger } from 'winston';
+
+import {
+ createServiceRef,
+ createServiceFactory,
+ coreServices,
+} from '@backstage/backend-plugin-api';
+import { loggerToWinstonLogger } from '@backstage/backend-common';
+import {
+ DocumentTypeInfo,
+ SearchEngine,
+} from '@backstage/plugin-search-common';
+import { createExtensionPoint } from '@backstage/backend-plugin-api';
+
+import {
+ RegisterCollatorParameters,
+ RegisterDecoratorParameters,
+} from '@backstage/plugin-search-backend-node';
+
+import { IndexBuilder } from './IndexBuilder';
+
+/**
+ * @alpha
+ * Options for build method on {@link SearchIndexService}.
+ */
+export type SearchIndexServiceStartOptions = {
+ searchEngine: SearchEngine;
+ collators: RegisterCollatorParameters[];
+ decorators: RegisterDecoratorParameters[];
+};
+
+/**
+ * @alpha
+ * Interface for implementation of index service.
+ */
+export interface SearchIndexService {
+ /**
+ * Starts indexing process
+ */
+ start(options: SearchIndexServiceStartOptions): Promise;
+ /**
+ * Returns an index types list.
+ */
+ getDocumentTypes(): Record;
+}
+
+/**
+ * @alpha
+ * Interface for search index registry extension point.
+ */
+export interface SearchIndexRegistryExtensionPoint {
+ addCollator(options: RegisterCollatorParameters): void;
+ addDecorator(options: RegisterDecoratorParameters): void;
+}
+
+/**
+ * @alpha
+ * Interface for search engine registry extension point.
+ */
+export interface SearchEngineRegistryExtensionPoint {
+ setSearchEngine(searchEngine: SearchEngine): void;
+}
+
+type DefaultSearchIndexServiceOptions = {
+ logger: Logger;
+};
+
+/**
+ * @alpha
+ * Reponsible for register the indexing task and start the schedule.
+ */
+class DefaultSearchIndexService implements SearchIndexService {
+ private logger: Logger;
+ private indexBuilder: IndexBuilder | null = null;
+
+ private constructor(options: DefaultSearchIndexServiceOptions) {
+ this.logger = options.logger;
+ }
+
+ static fromConfig(options: DefaultSearchIndexServiceOptions) {
+ return new DefaultSearchIndexService(options);
+ }
+
+ async start(options: SearchIndexServiceStartOptions): Promise {
+ this.indexBuilder = new IndexBuilder({
+ logger: this.logger,
+ searchEngine: options.searchEngine,
+ });
+
+ options.collators.forEach(collator =>
+ this.indexBuilder?.addCollator(collator),
+ );
+
+ options.decorators.forEach(decorator =>
+ this.indexBuilder?.addDecorator(decorator),
+ );
+
+ const { scheduler } = await this.indexBuilder?.build();
+ scheduler.start();
+ }
+
+ getDocumentTypes(): Record {
+ return this.indexBuilder?.getDocumentTypes() ?? {};
+ }
+}
+
+/**
+ * @alpha
+ * Service that builds a search index.
+ */
+export const searchIndexServiceRef = createServiceRef({
+ id: 'search.index.service',
+ defaultFactory: async service =>
+ createServiceFactory({
+ service,
+ deps: {
+ logger: coreServices.logger,
+ },
+ factory({ logger }) {
+ return DefaultSearchIndexService.fromConfig({
+ logger: loggerToWinstonLogger(logger),
+ });
+ },
+ }),
+});
+
+/**
+ * @alpha
+ * Extension point for register a search engine.
+ */
+export const searchEngineRegistryExtensionPoint =
+ createExtensionPoint({
+ id: 'search.engine.registry',
+ });
+
+/**
+ * @alpha
+ * Extension point for registering collators and decorators
+ */
+export const searchIndexRegistryExtensionPoint =
+ createExtensionPoint({
+ id: 'search.index.registry',
+ });
diff --git a/plugins/search-backend/CHANGELOG.md b/plugins/search-backend/CHANGELOG.md
index 72d9e53c08..d6985a70f1 100644
--- a/plugins/search-backend/CHANGELOG.md
+++ b/plugins/search-backend/CHANGELOG.md
@@ -1,5 +1,20 @@
# @backstage/plugin-search-backend
+## 1.2.5-next.0
+
+### Patch Changes
+
+- Updated dependencies
+ - @backstage/backend-common@0.18.4-next.0
+ - @backstage/config@1.0.7
+ - @backstage/errors@1.1.5
+ - @backstage/types@1.0.2
+ - @backstage/plugin-auth-node@0.2.13-next.0
+ - @backstage/plugin-permission-common@0.7.4
+ - @backstage/plugin-permission-node@0.7.7-next.0
+ - @backstage/plugin-search-backend-node@1.1.5-next.0
+ - @backstage/plugin-search-common@1.2.2
+
## 1.2.4
### Patch Changes
diff --git a/plugins/search-backend/alpha-api-report.md b/plugins/search-backend/alpha-api-report.md
new file mode 100644
index 0000000000..6ee94a0e29
--- /dev/null
+++ b/plugins/search-backend/alpha-api-report.md
@@ -0,0 +1,12 @@
+## API Report File for "@backstage/plugin-search-backend"
+
+> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/).
+
+```ts
+import { BackendFeature } from '@backstage/backend-plugin-api';
+
+// @alpha
+export const searchPlugin: () => BackendFeature;
+
+// (No @packageDocumentation comment for this package)
+```
diff --git a/plugins/search-backend/package.json b/plugins/search-backend/package.json
index 02d4d53a92..3091e0e8b4 100644
--- a/plugins/search-backend/package.json
+++ b/plugins/search-backend/package.json
@@ -1,14 +1,27 @@
{
"name": "@backstage/plugin-search-backend",
"description": "The Backstage backend plugin that provides your backstage app with search",
- "version": "1.2.4",
+ "version": "1.2.5-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"
+ "access": "public"
+ },
+ "exports": {
+ ".": "./src/index.ts",
+ "./alpha": "./src/alpha.ts",
+ "./package.json": "./package.json"
+ },
+ "typesVersions": {
+ "*": {
+ "alpha": [
+ "src/alpha.ts"
+ ],
+ "package.json": [
+ "package.json"
+ ]
+ }
},
"backstage": {
"role": "backend-plugin"
@@ -24,6 +37,7 @@
},
"dependencies": {
"@backstage/backend-common": "workspace:^",
+ "@backstage/backend-plugin-api": "workspace:^",
"@backstage/config": "workspace:^",
"@backstage/errors": "workspace:^",
"@backstage/plugin-auth-node": "workspace:^",
@@ -43,6 +57,7 @@
"zod": "~3.18.0"
},
"devDependencies": {
+ "@backstage/backend-test-utils": "workspace:^",
"@backstage/cli": "workspace:^",
"@types/supertest": "^2.0.8",
"supertest": "^6.1.3"
diff --git a/plugins/search-backend/src/alpha.test.ts b/plugins/search-backend/src/alpha.test.ts
new file mode 100644
index 0000000000..527f099236
--- /dev/null
+++ b/plugins/search-backend/src/alpha.test.ts
@@ -0,0 +1,31 @@
+/*
+ * 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 { startTestBackend } from '@backstage/backend-test-utils';
+import request from 'supertest';
+import { searchPlugin } from './alpha';
+
+describe('searchPlugin', () => {
+ it('should serve search results on query endpoint', async () => {
+ const { server } = await startTestBackend({
+ features: [searchPlugin()],
+ });
+
+ const response = await request(server).get('/api/search/query');
+ expect(response.status).toBe(200);
+ expect(response.body).toEqual({ numberOfResults: 0, results: [] });
+ });
+});
diff --git a/plugins/search-backend/src/alpha.ts b/plugins/search-backend/src/alpha.ts
new file mode 100644
index 0000000000..0c14852875
--- /dev/null
+++ b/plugins/search-backend/src/alpha.ts
@@ -0,0 +1,130 @@
+/*
+ * 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 {
+ coreServices,
+ createBackendPlugin,
+} from '@backstage/backend-plugin-api';
+import { loggerToWinstonLogger } from '@backstage/backend-common';
+import {
+ RegisterCollatorParameters,
+ RegisterDecoratorParameters,
+ LunrSearchEngine,
+} from '@backstage/plugin-search-backend-node';
+import {
+ searchIndexServiceRef,
+ searchIndexRegistryExtensionPoint,
+ SearchIndexRegistryExtensionPoint,
+ SearchEngineRegistryExtensionPoint,
+ searchEngineRegistryExtensionPoint,
+} from '@backstage/plugin-search-backend-node/alpha';
+
+import { createRouter } from './service/router';
+import { SearchEngine } from '@backstage/plugin-search-common';
+
+class SearchIndexRegistry implements SearchIndexRegistryExtensionPoint {
+ private collators: RegisterCollatorParameters[] = [];
+ private decorators: RegisterDecoratorParameters[] = [];
+
+ public addCollator(options: RegisterCollatorParameters): void {
+ this.collators.push(options);
+ }
+
+ public addDecorator(options: RegisterDecoratorParameters): void {
+ this.decorators.push(options);
+ }
+
+ public getCollators(): RegisterCollatorParameters[] {
+ return this.collators;
+ }
+
+ public getDecorators(): RegisterDecoratorParameters[] {
+ return this.decorators;
+ }
+}
+
+class SearchEngineRegistry implements SearchEngineRegistryExtensionPoint {
+ private searchEngine: SearchEngine | null = null;
+
+ public setSearchEngine(searchEngine: SearchEngine): void {
+ if (this.searchEngine) {
+ throw new Error('Multiple Search engines is not supported at this time');
+ }
+ this.searchEngine = searchEngine;
+ }
+
+ public getSearchEngine(): SearchEngine | null {
+ return this.searchEngine;
+ }
+}
+
+/**
+ * The Search plugin is responsible for starting search indexing processes and return search results.
+ * @alpha
+ */
+export const searchPlugin = createBackendPlugin({
+ pluginId: 'search',
+ register(env) {
+ const searchIndexRegistry = new SearchIndexRegistry();
+ env.registerExtensionPoint(
+ searchIndexRegistryExtensionPoint,
+ searchIndexRegistry,
+ );
+
+ const searchEngineRegistry = new SearchEngineRegistry();
+ env.registerExtensionPoint(
+ searchEngineRegistryExtensionPoint,
+ searchEngineRegistry,
+ );
+
+ env.registerInit({
+ deps: {
+ logger: coreServices.logger,
+ config: coreServices.config,
+ permissions: coreServices.permissions,
+ http: coreServices.httpRouter,
+ searchIndexService: searchIndexServiceRef,
+ },
+ async init({ config, logger, permissions, http, searchIndexService }) {
+ let searchEngine = searchEngineRegistry.getSearchEngine();
+ if (!searchEngine) {
+ searchEngine = new LunrSearchEngine({
+ logger: loggerToWinstonLogger(logger),
+ });
+ }
+
+ const collators = searchIndexRegistry.getCollators();
+ const decorators = searchIndexRegistry.getDecorators();
+
+ await searchIndexService.start({
+ searchEngine,
+ collators,
+ decorators,
+ });
+
+ const router = await createRouter({
+ config,
+ permissions,
+ logger: loggerToWinstonLogger(logger),
+ engine: searchEngine,
+ types: searchIndexService.getDocumentTypes(),
+ });
+
+ http.use(router);
+ },
+ });
+ },
+});
diff --git a/plugins/search-react/CHANGELOG.md b/plugins/search-react/CHANGELOG.md
index cf6138956c..22bf885caf 100644
--- a/plugins/search-react/CHANGELOG.md
+++ b/plugins/search-react/CHANGELOG.md
@@ -1,5 +1,19 @@
# @backstage/plugin-search-react
+## 1.5.2-next.0
+
+### Patch Changes
+
+- b2e182cdfa4: Fixes a UI bug in search result item which rendered the item text with incorrect font size and color
+- 8e00acb28db: Small tweaks to remove warnings in the console during development (mainly focusing on techdocs)
+- Updated dependencies
+ - @backstage/core-components@0.12.6-next.0
+ - @backstage/core-plugin-api@1.5.0
+ - @backstage/theme@0.2.18
+ - @backstage/types@1.0.2
+ - @backstage/version-bridge@1.0.3
+ - @backstage/plugin-search-common@1.2.2
+
## 1.5.1
### Patch Changes
diff --git a/plugins/search-react/package.json b/plugins/search-react/package.json
index f14a62ccea..0a8864392b 100644
--- a/plugins/search-react/package.json
+++ b/plugins/search-react/package.json
@@ -1,6 +1,6 @@
{
"name": "@backstage/plugin-search-react",
- "version": "1.5.1",
+ "version": "1.5.2-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -39,7 +39,7 @@
"@backstage/version-bridge": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"lodash": "^4.17.21",
"qs": "^6.9.4",
"react-use": "^17.3.2"
diff --git a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx
index cf89b6a9cd..0d2c59e173 100644
--- a/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx
+++ b/plugins/search-react/src/components/DefaultResultListItem/DefaultResultListItem.tsx
@@ -81,6 +81,8 @@ export const DefaultResultListItemComponent = ({
WebkitLineClamp: lineClamp,
overflow: 'hidden',
}}
+ color="textSecondary"
+ variant="body2"
>
{highlight?.fields.text ? (
(
);
```
+To allow external links to be added as shortcut, you can add `allowExternalLinks` property to the ` ` component.
+
The plugin exports a `shortcutApiRef` but the plugin includes a default implementation of the `ShortcutApi` that uses `localStorage` to store each user's shortcuts.
To overwrite the default implementation add it to the App's `apis.ts`:
diff --git a/plugins/shortcuts/api-report.md b/plugins/shortcuts/api-report.md
index 19c0035a55..0dd42f5344 100644
--- a/plugins/shortcuts/api-report.md
+++ b/plugins/shortcuts/api-report.md
@@ -58,6 +58,8 @@ export const shortcutsPlugin: BackstagePlugin<{}, {}, {}>;
// @public
export interface ShortcutsProps {
+ // (undocumented)
+ allowExternalLinks?: boolean;
// (undocumented)
icon?: IconComponent;
}
diff --git a/plugins/shortcuts/package.json b/plugins/shortcuts/package.json
index 1901198b80..d84cf27223 100644
--- a/plugins/shortcuts/package.json
+++ b/plugins/shortcuts/package.json
@@ -1,7 +1,7 @@
{
"name": "@backstage/plugin-shortcuts",
"description": "A Backstage plugin that provides a shortcuts feature to the sidebar",
- "version": "0.3.8",
+ "version": "0.3.9-next.0",
"main": "src/index.ts",
"types": "src/index.ts",
"license": "Apache-2.0",
@@ -29,7 +29,7 @@
"@backstage/types": "workspace:^",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
- "@material-ui/lab": "4.0.0-alpha.57",
+ "@material-ui/lab": "4.0.0-alpha.61",
"react-hook-form": "^7.12.2",
"react-use": "^17.2.4",
"uuid": "^8.3.2",
diff --git a/plugins/shortcuts/src/AddShortcut.tsx b/plugins/shortcuts/src/AddShortcut.tsx
index e4b1ba84ce..1e14767531 100644
--- a/plugins/shortcuts/src/AddShortcut.tsx
+++ b/plugins/shortcuts/src/AddShortcut.tsx
@@ -45,9 +45,16 @@ type Props = {
onClose: () => void;
anchorEl?: Element;
api: ShortcutApi;
+
+ allowExternalLinks?: boolean;
};
-export const AddShortcut = ({ onClose, anchorEl, api }: Props) => {
+export const AddShortcut = ({
+ onClose,
+ anchorEl,
+ api,
+ allowExternalLinks,
+}: Props) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const { pathname } = useLocation();
@@ -114,6 +121,7 @@ export const AddShortcut = ({ onClose, anchorEl, api }: Props) => {
onClose={handleClose}
onSave={handleSave}
formValues={formValues}
+ allowExternalLinks={allowExternalLinks}
/>
diff --git a/plugins/shortcuts/src/EditShortcut.tsx b/plugins/shortcuts/src/EditShortcut.tsx
index 702275e90e..5336841633 100644
--- a/plugins/shortcuts/src/EditShortcut.tsx
+++ b/plugins/shortcuts/src/EditShortcut.tsx
@@ -46,9 +46,16 @@ type Props = {
onClose: () => void;
anchorEl?: Element;
api: ShortcutApi;
+ allowExternalLinks?: boolean;
};
-export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => {
+export const EditShortcut = ({
+ shortcut,
+ onClose,
+ anchorEl,
+ api,
+ allowExternalLinks,
+}: Props) => {
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const open = Boolean(anchorEl);
@@ -127,6 +134,7 @@ export const EditShortcut = ({ shortcut, onClose, anchorEl, api }: Props) => {
formValues={{ url: shortcut.url, title: shortcut.title }}
onClose={handleClose}
onSave={handleSave}
+ allowExternalLinks={allowExternalLinks}
/>
diff --git a/plugins/shortcuts/src/ShortcutForm.test.tsx b/plugins/shortcuts/src/ShortcutForm.test.tsx
index 2582148fc7..3a52356b5d 100644
--- a/plugins/shortcuts/src/ShortcutForm.test.tsx
+++ b/plugins/shortcuts/src/ShortcutForm.test.tsx
@@ -43,6 +43,50 @@ describe('ShortcutForm', () => {
});
});
+ it('allows external links', async () => {
+ await renderInTestApp( );
+
+ const urlInput = screen.getByPlaceholderText('Enter a URL');
+ const titleInput = screen.getByPlaceholderText('Enter a display name');
+ fireEvent.change(urlInput, {
+ target: { value: 'https://www.backstage.io' },
+ });
+ fireEvent.change(titleInput, { target: { value: 'Backstage' } });
+
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(props.onSave).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'Backstage',
+ url: 'https://www.backstage.io',
+ }),
+ expect.anything(),
+ );
+ });
+ });
+
+ it('allows relative links when external links are enabled', async () => {
+ await renderInTestApp( );
+
+ const urlInput = screen.getByPlaceholderText('Enter a URL');
+ const titleInput = screen.getByPlaceholderText('Enter a display name');
+ fireEvent.change(urlInput, {
+ target: { value: '/catalog' },
+ });
+ fireEvent.change(titleInput, { target: { value: 'Catalog' } });
+
+ fireEvent.click(screen.getByText('Save'));
+ await waitFor(() => {
+ expect(props.onSave).toHaveBeenCalledWith(
+ expect.objectContaining({
+ title: 'Catalog',
+ url: '/catalog',
+ }),
+ expect.anything(),
+ );
+ });
+ });
+
it('calls the save handler', async () => {
await renderInTestApp(
;
onClose: () => void;
+ allowExternalLinks?: boolean;
};
-export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => {
+export const ShortcutForm = ({
+ formValues,
+ onSave,
+ onClose,
+ allowExternalLinks,
+}: Props) => {
const classes = useStyles();
-
const {
handleSubmit,
reset,
@@ -70,10 +75,19 @@ export const ShortcutForm = ({ formValues, onSave, onClose }: Props) => {
control={control}
rules={{
required: true,
- pattern: {
- value: /^\//,
- message: 'Must be a relative URL (starts with a /)',
- },
+ ...(allowExternalLinks
+ ? {
+ pattern: {
+ value: /^(https?:\/\/)|\//,
+ message: 'Must start with http(s):// or /',
+ },
+ }
+ : {
+ pattern: {
+ value: /^\//,
+ message: 'Must be a relative URL (starts with a /)',
+ },
+ }),
}}
render={({ field }) => (
type Props = {
shortcut: Shortcut;
api: ShortcutApi;
+ allowExternalLinks?: boolean;
};
-export const ShortcutItem = ({ shortcut, api }: Props) => {
+export const ShortcutItem = ({ shortcut, api, allowExternalLinks }: Props) => {
const classes = useStyles();
const [anchorEl, setAnchorEl] = React.useState();
@@ -100,6 +101,7 @@ export const ShortcutItem = ({ shortcut, api }: Props) => {
anchorEl={anchorEl}
api={api}
shortcut={shortcut}
+ allowExternalLinks={allowExternalLinks}
/>
>
);
diff --git a/plugins/shortcuts/src/Shortcuts.tsx b/plugins/shortcuts/src/Shortcuts.tsx
index 28aa33e8f2..6b9ea4d79b 100644
--- a/plugins/shortcuts/src/Shortcuts.tsx
+++ b/plugins/shortcuts/src/Shortcuts.tsx
@@ -35,6 +35,7 @@ import { IconComponent, useApi } from '@backstage/core-plugin-api';
*/
export interface ShortcutsProps {
icon?: IconComponent;
+ allowExternalLinks?: boolean;
}
export const Shortcuts = (props: ShortcutsProps) => {
@@ -62,6 +63,7 @@ export const Shortcuts = (props: ShortcutsProps) => {
onClose={handleClose}
anchorEl={anchorEl}
api={shortcutApi}
+ allowExternalLinks={props.allowExternalLinks}
/>
{loading ? (