From 5fd7316f5d285d2d1ebb53a3f4ba04e3d9054e8f Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 5 Aug 2021 21:19:51 -0400 Subject: [PATCH 001/102] Improve search logging Signed-off-by: Adam Harvey --- plugins/search-backend/src/service/router.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/search-backend/src/service/router.ts b/plugins/search-backend/src/service/router.ts index 27b8acf9cb..3a0a43522e 100644 --- a/plugins/search-backend/src/service/router.ts +++ b/plugins/search-backend/src/service/router.ts @@ -38,7 +38,7 @@ export async function createRouter({ ) => { const { term, filters = {}, pageCursor = '' } = req.query; logger.info( - `Search request received: ${term}, ${JSON.stringify( + `Search request received: term="${term}", filters=${JSON.stringify( filters, )}, ${pageCursor}`, ); From 64baedea523bd5a7b73452bec7f524cad1c4c0bd Mon Sep 17 00:00:00 2001 From: Adam Harvey Date: Thu, 5 Aug 2021 21:20:59 -0400 Subject: [PATCH 002/102] Add changeset Signed-off-by: Adam Harvey --- .changeset/nasty-queens-juggle.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/nasty-queens-juggle.md diff --git a/.changeset/nasty-queens-juggle.md b/.changeset/nasty-queens-juggle.md new file mode 100644 index 0000000000..8204190cee --- /dev/null +++ b/.changeset/nasty-queens-juggle.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search-backend': patch +--- + +Improve search query logging message From 2986f503e4b952a128eb10b36c487f27e77d17ff Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 10 Aug 2021 17:23:50 +0200 Subject: [PATCH 003/102] Minor adjustments to Sidebar UX Signed-off-by: Philipp Hugenroth --- .../core-components/src/layout/Sidebar/Bar.tsx | 11 ++++------- .../core-components/src/layout/Sidebar/Items.tsx | 16 +++++++++------- .../core-components/src/layout/Sidebar/config.ts | 2 +- packages/theme/src/themes.ts | 2 ++ packages/theme/src/types.ts | 1 + 5 files changed, 17 insertions(+), 15 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 65da4d2d75..05bb751cc8 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -42,6 +42,7 @@ const useStyles = makeStyles(theme => ({ msOverflowStyle: 'none', scrollbarWidth: 'none', width: sidebarConfig.drawerWidthClosed, + borderRight: `1px solid ${theme.palette.navigation.divider}`, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.shortest, @@ -60,14 +61,11 @@ const useStyles = makeStyles(theme => ({ duration: theme.transitions.duration.shorter, }), }, - drawerPeek: { - width: sidebarConfig.drawerWidthClosed + 4, - }, })); enum State { Closed, - Peek, + Idle, Open, } @@ -103,7 +101,7 @@ export const Sidebar = ({ setState(State.Open); }, openDelayMs); - setState(State.Peek); + setState(State.Idle); } }; @@ -115,7 +113,7 @@ export const Sidebar = ({ clearTimeout(hoverTimerRef.current); hoverTimerRef.current = undefined; } - if (state === State.Peek) { + if (state === State.Idle) { setState(State.Closed); } else if (state === State.Open) { hoverTimerRef.current = window.setTimeout(() => { @@ -143,7 +141,6 @@ export const Sidebar = ({ >
diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 1068a877bd..00771c837b 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -344,13 +344,15 @@ export const SidebarSpacer = styled('div')({ height: 8, }); -export const SidebarDivider = styled('hr')({ - height: 1, - width: '100%', - background: '#383838', - border: 'none', - margin: '12px 0px', -}); +export const SidebarDivider = styled('hr')( + ({ theme }: { theme: BackstageTheme }) => ({ + height: 1, + width: '100%', + background: theme.palette.navigation.divider, + border: 'none', + margin: '12px 0px', + }), +); const styledScrollbar = (theme: Theme): CreateCSSProperties => ({ overflowY: 'auto', diff --git a/packages/core-components/src/layout/Sidebar/config.ts b/packages/core-components/src/layout/Sidebar/config.ts index 3d574d03bb..d2a690e38f 100644 --- a/packages/core-components/src/layout/Sidebar/config.ts +++ b/packages/core-components/src/layout/Sidebar/config.ts @@ -25,7 +25,7 @@ export const sidebarConfig = { drawerWidthOpen: 224, // As per NN/g's guidance on timing for exposing hidden content // See https://www.nngroup.com/articles/timing-exposing-content/ - defaultOpenDelayMs: 300, + defaultOpenDelayMs: 100, defaultCloseDelayMs: 0, defaultFadeDuration: 200, logoHeight: 32, diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index b31786aae9..03a8fad2a2 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -66,6 +66,7 @@ export const lightTheme = createTheme({ background: '#171717', indicator: '#9BF0E1', color: '#b5b5b5', + divider: '#383838', selectedColor: '#FFF', }, pinSidebarButton: { @@ -132,6 +133,7 @@ export const darkTheme = createTheme({ background: '#424242', indicator: '#9BF0E1', color: '#b5b5b5', + divider: '#383838', selectedColor: '#FFF', }, pinSidebarButton: { diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index c2758c29e7..2c4d198a4f 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -47,6 +47,7 @@ type PaletteAdditions = { background: string; indicator: string; color: string; + divider: string; selectedColor: string; }; tabbar: { From a3f3cff3bc8a91d678ec7a5b97ee5a88f65335e9 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Tue, 10 Aug 2021 18:15:37 +0200 Subject: [PATCH 004/102] Add changeset Signed-off-by: Philipp Hugenroth --- .changeset/tender-dingos-burn.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/tender-dingos-burn.md diff --git a/.changeset/tender-dingos-burn.md b/.changeset/tender-dingos-burn.md new file mode 100644 index 0000000000..fa52bfcead --- /dev/null +++ b/.changeset/tender-dingos-burn.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/theme': patch +--- + +Change the default hover experience for the sidebar to be not jumpy & add visual separation between sidebar & Entity Page tabs for dark mode. From ae5891ac61f4640376938e0a5c88b4b6818fa61b Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Wed, 11 Aug 2021 12:31:42 +0200 Subject: [PATCH 005/102] Add BackstageTheme to core-components imports Signed-off-by: Philipp Hugenroth --- packages/core-components/api-report.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 42404fb0c0..14ed06d37c 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -7,6 +7,7 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; +import { BackstageTheme } from '@backstage/theme'; import { Breadcrumbs as Breadcrumbs_2 } from '@material-ui/core'; import { ButtonProps } from '@material-ui/core'; import { ButtonTypeMap } from '@material-ui/core'; @@ -1604,6 +1605,13 @@ export const SidebarDivider: React_2.ComponentType< > & StyledComponentProps<'root'> & { className?: string | undefined; + } & Pick< + { + theme: BackstageTheme; + }, + never + > & { + theme?: BackstageTheme | undefined; } >; From b3bf8967a4715ca7e329d688e812d69b807fdadf Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 10 Aug 2021 12:49:43 +0200 Subject: [PATCH 006/102] Auth backend: add support for authenticating from additional app origins Co-authored-by: Patrik Oldsberg Signed-off-by: Samira Mokaram --- .../lib/AuthConnector/DefaultAuthConnector.ts | 8 ++++-- plugins/auth-backend/package.json | 1 + .../src/lib/oauth/OAuthAdapter.ts | 28 +++++++++++++++---- plugins/auth-backend/src/lib/oauth/types.ts | 5 +++- plugins/auth-backend/src/providers/types.ts | 5 ++++ plugins/auth-backend/src/service/router.ts | 20 ++++++++++++- 6 files changed, 57 insertions(+), 10 deletions(-) diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts index d707acdc9b..88156fedad 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -61,8 +61,7 @@ function defaultJoinScopes(scopes: Set) { * via the OAuthRequestApi. */ export class DefaultAuthConnector - implements AuthConnector -{ + implements AuthConnector { private readonly discoveryApi: DiscoveryApi; private readonly environment: string; private readonly provider: AuthProvider & { id: string }; @@ -152,7 +151,10 @@ export class DefaultAuthConnector private async showPopup(scopes: Set): Promise { const scope = this.joinScopesFunc(scopes); - const popupUrl = await this.buildUrl('/start', { scope }); + const popupUrl = await this.buildUrl('/start', { + scope, + origin: location.origin, + }); const payload = await showLoginPopup({ url: popupUrl, diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index c9219ddfae..4ea799c37c 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -51,6 +51,7 @@ "jwt-decode": "^3.1.0", "knex": "^0.95.1", "luxon": "^1.25.0", + "minimatch": "^3.0.3", "morgan": "^1.10.0", "node-cache": "^5.1.2", "openid-client": "^4.2.1", diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index d9a49e17e8..6397c09233 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -22,9 +22,9 @@ import { BackstageIdentity, AuthProviderConfig, } from '../../providers/types'; -import { InputError } from '@backstage/errors'; +import { InputError, NotAllowedError } from '@backstage/errors'; import { TokenIssuer } from '../../identity/types'; -import { verifyNonce } from './helpers'; +import { readState, verifyNonce } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; import { OAuthHandlers, OAuthStartRequest, OAuthRefreshRequest } from './types'; @@ -40,6 +40,7 @@ export type Options = { cookiePath: string; appOrigin: string; tokenIssuer: TokenIssuer; + isOriginAllowed: (origin: string) => boolean; }; export class OAuthAdapter implements AuthProviderRouteHandlers { @@ -61,6 +62,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { cookieDomain: url.hostname, cookiePath, secure, + isOriginAllowed: config.isOriginAllowed, }); } @@ -73,6 +75,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { // retrieve scopes from request const scope = req.query.scope?.toString() ?? ''; const env = req.query.env?.toString(); + const origin = req.query.origin?.toString(); if (!env) { throw new InputError('No env provided in request query parameters'); @@ -86,7 +89,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { // set a nonce cookie before redirecting to oauth provider this.setNonceCookie(res, nonce); - const state = { nonce: nonce, env: env }; + const state = { nonce, env, origin }; const forwardReq = Object.assign(req, { scope, state }); const { url, status } = await this.handlers.start( @@ -103,7 +106,22 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { req: express.Request, res: express.Response, ): Promise { + let appOrigin = this.options.appOrigin; + try { + const state: any = readState(req.query.state?.toString() ?? ''); + + if (state.origin) { + try { + appOrigin = new URL(state.origin).origin; + } catch { + throw new NotAllowedError('App origin is invalid, failed to parse'); + } + if (!this.options.isOriginAllowed(appOrigin)) { + throw new NotAllowedError(`Origin '${appOrigin}' is not allowed`); + } + } + // verify nonce cookie and state cookie on callback verifyNonce(req, this.options.providerId); @@ -129,13 +147,13 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { await this.populateIdentity(response.backstageIdentity); // post message back to popup if successful - return postMessageResponse(res, this.options.appOrigin, { + return postMessageResponse(res, appOrigin, { type: 'authorization_response', response, }); } catch (error) { // post error message back to popup if failure - return postMessageResponse(res, this.options.appOrigin, { + return postMessageResponse(res, appOrigin, { type: 'authorization_response', error: { name: error.name, diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index c5ece2b921..989b76c0a4 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -77,6 +77,7 @@ export type OAuthState = { */ nonce: string; env: string; + origin?: string; }; export type OAuthStartRequest = express.Request<{}> & { @@ -106,7 +107,9 @@ export interface OAuthHandlers { * Handles the redirect from the auth provider when the user has signed in. * @param {express.Request} req */ - handler(req: express.Request): Promise<{ + handler( + req: express.Request, + ): Promise<{ response: AuthResponse; refreshToken?: string; }>; diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index c838005c00..66928ab4a7 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -34,6 +34,11 @@ export type AuthProviderConfig = { * The base URL of the app as provided by app.baseUrl */ appUrl: string; + + /** + * A function that is called to check whether an origin other than the apps default one is allowed. + */ + isOriginAllowed: (origin: string) => boolean; }; export type RedirectInfo = { diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 8173efcbc1..b314f2fd2f 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -32,6 +32,7 @@ import { Config } from '@backstage/config'; import { createOidcRouter, DatabaseKeyStore, TokenFactory } from '../identity'; import session from 'express-session'; import passport from 'passport'; +import { Minimatch } from 'minimatch'; type ProviderFactories = { [s: string]: AuthProviderFactory }; @@ -88,6 +89,8 @@ export async function createRouter({ const providersConfig = config.getConfig('auth.providers'); const configuredProviders = providersConfig.keys(); + const isOriginAllowed = createOriginFilter(config); + for (const [providerId, providerFactory] of Object.entries( allProviderFactories, )) { @@ -96,7 +99,7 @@ export async function createRouter({ try { const provider = providerFactory({ providerId, - globalConfig: { baseUrl: authUrl, appUrl }, + globalConfig: { baseUrl: authUrl, appUrl, isOriginAllowed }, config: providersConfig.getConfig(providerId), logger, tokenIssuer, @@ -158,3 +161,18 @@ export async function createRouter({ return router; } + +function createOriginFilter(config: Config): (origin: string) => boolean { + const allowedOrigins = config.getOptionalStringArray('auth.allowedOrigins'); + if (!allowedOrigins || allowedOrigins.length === 0) { + return () => false; + } + + const allowedOriginPatterns = allowedOrigins.map( + pattern => new Minimatch(pattern, { nocase: true, noglobstar: true }), + ); + + return origin => { + return allowedOriginPatterns.some(pattern => pattern.match(origin)); + }; +} From a90523497f825e5e98598c2ced22c8032e7cf0af Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Wed, 11 Aug 2021 10:13:52 +0200 Subject: [PATCH 007/102] address comments Signed-off-by: Samira Mokaram --- plugins/auth-backend/src/service/router.ts | 4 +++- yarn.lock | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index b314f2fd2f..3c2ed31b94 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -163,7 +163,9 @@ export async function createRouter({ } function createOriginFilter(config: Config): (origin: string) => boolean { - const allowedOrigins = config.getOptionalStringArray('auth.allowedOrigins'); + const allowedOrigins = config.getOptionalStringArray( + 'auth.experimentalExtraAllowedOrigins', + ); if (!allowedOrigins || allowedOrigins.length === 0) { return () => false; } diff --git a/yarn.lock b/yarn.lock index 6568aed665..e48efe4e81 100644 --- a/yarn.lock +++ b/yarn.lock @@ -19330,7 +19330,7 @@ minimatch@0.3: lru-cache "2" sigmund "~1.0.0" -"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.4: +"minimatch@2 || 3", minimatch@3.0.4, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" resolved "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz#5166e286457f03306064be5497e8dbb0c3d32083" integrity sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA== From efbb82dd3006d42258338487138e2566591f2f01 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Thu, 12 Aug 2021 14:23:50 +0200 Subject: [PATCH 008/102] Extract `JenkinsConfig` to make writing a custom `JenkinsInfoProvider` easier Signed-off-by: Oliver Sand --- .changeset/long-rats-flow.md | 5 + plugins/jenkins-backend/api-report.md | 25 ++ plugins/jenkins-backend/src/service/index.ts | 13 +- .../src/service/jenkinsInfoProvider.test.ts | 143 ++++++++- .../src/service/jenkinsInfoProvider.ts | 286 +++++++++--------- 5 files changed, 330 insertions(+), 142 deletions(-) create mode 100644 .changeset/long-rats-flow.md diff --git a/.changeset/long-rats-flow.md b/.changeset/long-rats-flow.md new file mode 100644 index 0000000000..61673f8bf9 --- /dev/null +++ b/.changeset/long-rats-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-jenkins-backend': patch +--- + +Extract `JenkinsConfig` to make writing a custom `JenkinsInfoProvider` easier. diff --git a/plugins/jenkins-backend/api-report.md b/plugins/jenkins-backend/api-report.md index b94931e000..01114e74d6 100644 --- a/plugins/jenkins-backend/api-report.md +++ b/plugins/jenkins-backend/api-report.md @@ -34,6 +34,17 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; } +// Warning: (ae-missing-release-tag) "JenkinsConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class JenkinsConfig { + constructor(instances: JenkinsInstanceConfig[]); + static fromConfig(config: Config): JenkinsConfig; + getInstanceConfig(jenkinsName?: string): JenkinsInstanceConfig; + // (undocumented) + readonly instances: JenkinsInstanceConfig[]; +} + // Warning: (ae-missing-release-tag) "JenkinsInfo" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -57,6 +68,20 @@ export interface JenkinsInfoProvider { }): Promise; } +// Warning: (ae-missing-release-tag) "JenkinsInstanceConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface JenkinsInstanceConfig { + // (undocumented) + apiKey: string; + // (undocumented) + baseUrl: string; + // (undocumented) + name: string; + // (undocumented) + username: string; +} + // Warning: (ae-missing-release-tag) "RouterOptions" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) diff --git a/plugins/jenkins-backend/src/service/index.ts b/plugins/jenkins-backend/src/service/index.ts index 329092e881..af9986c492 100644 --- a/plugins/jenkins-backend/src/service/index.ts +++ b/plugins/jenkins-backend/src/service/index.ts @@ -14,7 +14,14 @@ * limitations under the License. */ -export type { RouterOptions } from './router'; +export { + DefaultJenkinsInfoProvider, + JenkinsConfig, +} from './jenkinsInfoProvider'; +export type { + JenkinsInfo, + JenkinsInfoProvider, + JenkinsInstanceConfig, +} from './jenkinsInfoProvider'; export { createRouter } from './router'; -export type { JenkinsInfo, JenkinsInfoProvider } from './jenkinsInfoProvider'; -export { DefaultJenkinsInfoProvider } from './jenkinsInfoProvider'; +export type { RouterOptions } from './router'; diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts index 7d1615ac27..08439297a7 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.test.ts @@ -14,10 +14,149 @@ * limitations under the License. */ -import { DefaultJenkinsInfoProvider, JenkinsInfo } from './jenkinsInfoProvider'; import { CatalogClient } from '@backstage/catalog-client'; -import { ConfigReader } from '@backstage/config'; import { Entity, EntityName } from '@backstage/catalog-model'; +import { ConfigReader } from '@backstage/config'; +import { + DefaultJenkinsInfoProvider, + JenkinsConfig, + JenkinsInfo, +} from './jenkinsInfoProvider'; + +describe('JenkinsConfig', () => { + it('Reads simple config and annotation', async () => { + const config = JenkinsConfig.fromConfig( + new ConfigReader({ + jenkins: { + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + }), + ); + + expect(config.instances).toEqual([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + }); + + it('Reads named default config and annotation', async () => { + const config = JenkinsConfig.fromConfig( + new ConfigReader({ + jenkins: { + instances: [ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ], + }, + }), + ); + + expect(config.instances).toEqual([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + }); + + it('Parses named default config (amongst named other configs)', async () => { + const config = JenkinsConfig.fromConfig( + new ConfigReader({ + jenkins: { + instances: [ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ], + }, + }), + ); + + expect(config.instances).toEqual([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + }); + + it('Gets default Jenkins instance', async () => { + const config = new JenkinsConfig([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + + expect(config.getInstanceConfig()).toEqual({ + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }); + }); + + it('Gets named Jenkins instance', async () => { + const config = new JenkinsConfig([ + { + name: 'default', + baseUrl: 'https://jenkins.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + { + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }, + ]); + + expect(config.getInstanceConfig('other')).toEqual({ + name: 'other', + baseUrl: 'https://jenkins-other.example.com', + username: 'backstage - bot', + apiKey: '123456789abcdef0123456789abcedf012', + }); + }); +}); describe('DefaultJenkinsInfoProvider', () => { const mockCatalog: jest.Mocked = { diff --git a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts index 50875cde99..1a992e73f1 100644 --- a/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts +++ b/plugins/jenkins-backend/src/service/jenkinsInfoProvider.ts @@ -13,12 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { CatalogClient } from '@backstage/catalog-client'; import { Entity, EntityName, stringifyEntityRef, } from '@backstage/catalog-model'; -import { CatalogClient } from '@backstage/catalog-client'; import { Config } from '@backstage/config'; export interface JenkinsInfoProvider { @@ -40,145 +40,28 @@ export interface JenkinsInfo { jobFullName: string; // TODO: make this an array } +export interface JenkinsInstanceConfig { + name: string; + baseUrl: string; + username: string; + apiKey: string; +} + /** - * Use default config and annotations, build using fromConfig static function. - * - * This will fallback through various deprecated config and annotation schemes. + * Holds multiple Jenkins configurations. */ -export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { - static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; - static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-full-name'; +export class JenkinsConfig { + constructor(public readonly instances: JenkinsInstanceConfig[]) {} - private constructor( - private readonly config: { - name: string; - baseUrl: string; - username: string; - apiKey: string; - }[], - private readonly catalog: CatalogClient, - ) {} - - static fromConfig(options: { - config: Config; - catalog: CatalogClient; - }): DefaultJenkinsInfoProvider { - return new DefaultJenkinsInfoProvider( - this.loadConfig(options.config), - options.catalog, - ); - } - - async getInstance(opt: { - entityRef: EntityName; - jobFullName?: string; - }): Promise { - // load entity - const entity = await this.catalog.getEntityByName(opt.entityRef); - if (!entity) { - throw new Error( - `Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`, - ); - } - - // lookup `[jenkinsName#]jobFullName` from entity annotation - const jenkinsAndJobName = - DefaultJenkinsInfoProvider.getEntityAnnotationValue(entity); - if (!jenkinsAndJobName) { - throw new Error( - `Couldn't find jenkins annotation (${ - DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION - }) on entity with name: ${stringifyEntityRef(opt.entityRef)}`, - ); - } - - let jobFullName; - let jenkinsName: string | undefined; - const splitIndex = jenkinsAndJobName.indexOf(':'); - if (splitIndex === -1) { - // no jenkinsName specified, use default - jobFullName = jenkinsAndJobName; - } else { - // There is a jenkinsName specified - jenkinsName = jenkinsAndJobName.substring(0, splitIndex); - jobFullName = jenkinsAndJobName.substring( - splitIndex + 1, - jenkinsAndJobName.length, - ); - } - - // lookup baseURL + creds from config - const instanceConfig = DefaultJenkinsInfoProvider.getInstanceConfig( - jenkinsName, - this.config, - ); - - const creds = Buffer.from( - `${instanceConfig.username}:${instanceConfig.apiKey}`, - 'binary', - ).toString('base64'); - - return { - baseUrl: instanceConfig.baseUrl, - headers: { - Authorization: `Basic ${creds}`, - }, - jobFullName, - }; - } - - private static getEntityAnnotationValue(entity: Entity) { - return ( - entity.metadata.annotations?.[ - DefaultJenkinsInfoProvider.OLD_JENKINS_ANNOTATION - ] || - entity.metadata.annotations?.[ - DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION - ] - ); - } - - private static getInstanceConfig( - jenkinsName: string | undefined, - config: { - name: string; - baseUrl: string; - username: string; - apiKey: string; - }[], - ): { name: string; baseUrl: string; username: string; apiKey: string } { + /** + * Read all Jenkins instance configurations. + * @param config - Root configuration + * @returns A JenkinsConfig that contains all configured Jenkins instances. + */ + static fromConfig(config: Config): JenkinsConfig { const DEFAULT_JENKINS_NAME = 'default'; - if (!jenkinsName || jenkinsName === DEFAULT_JENKINS_NAME) { - // no name provided, use default - const instanceConfig = config.find(c => c.name === DEFAULT_JENKINS_NAME); - - if (!instanceConfig) { - throw new Error( - `Couldn't find a default jenkins instance in the config. Either configure an instance with name ${DEFAULT_JENKINS_NAME} or add a prefix to your annotation value.`, - ); - } - - return instanceConfig; - } - - // A name is provided, look it up. - const instanceConfig = config.find(c => c.name === jenkinsName); - - if (!instanceConfig) { - throw new Error( - `Couldn't find a jenkins instance in the config with name ${jenkinsName}`, - ); - } - return instanceConfig; - } - - private static loadConfig( - rootConfig: Config, - ): { name: string; baseUrl: string; username: string; apiKey: string }[] { - const DEFAULT_JENKINS_NAME = 'default'; - - const jenkinsConfig = rootConfig.getConfig('jenkins'); + const jenkinsConfig = config.getConfig('jenkins'); // load all named instance config const namedInstanceConfig = @@ -223,9 +106,138 @@ export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { apiKey: string; }[]; - return [...namedInstanceConfig, ...unnamedInstanceConfig]; + return new JenkinsConfig([ + ...namedInstanceConfig, + ...unnamedInstanceConfig, + ]); } - return namedInstanceConfig; + return new JenkinsConfig(namedInstanceConfig); + } + + /** + * Gets a Jenkins instance configuration by name, or the default one if no + * name is provided. + * @param jenkinsName - Optional name of the Jenkins instance. + * @returns The requested Jenkins instance. + */ + getInstanceConfig(jenkinsName?: string): JenkinsInstanceConfig { + const DEFAULT_JENKINS_NAME = 'default'; + + if (!jenkinsName || jenkinsName === DEFAULT_JENKINS_NAME) { + // no name provided, use default + const instanceConfig = this.instances.find( + c => c.name === DEFAULT_JENKINS_NAME, + ); + + if (!instanceConfig) { + throw new Error( + `Couldn't find a default jenkins instance in the config. Either configure an instance with name ${DEFAULT_JENKINS_NAME} or add a prefix to your annotation value.`, + ); + } + + return instanceConfig; + } + + // A name is provided, look it up. + const instanceConfig = this.instances.find(c => c.name === jenkinsName); + + if (!instanceConfig) { + throw new Error( + `Couldn't find a jenkins instance in the config with name ${jenkinsName}`, + ); + } + return instanceConfig; + } +} + +/** + * Use default config and annotations, build using fromConfig static function. + * + * This will fallback through various deprecated config and annotation schemes. + */ +export class DefaultJenkinsInfoProvider implements JenkinsInfoProvider { + static readonly OLD_JENKINS_ANNOTATION = 'jenkins.io/github-folder'; + static readonly NEW_JENKINS_ANNOTATION = 'jenkins.io/job-full-name'; + + private constructor( + private readonly config: JenkinsConfig, + private readonly catalog: CatalogClient, + ) {} + + static fromConfig(options: { + config: Config; + catalog: CatalogClient; + }): DefaultJenkinsInfoProvider { + return new DefaultJenkinsInfoProvider( + JenkinsConfig.fromConfig(options.config), + options.catalog, + ); + } + + async getInstance(opt: { + entityRef: EntityName; + jobFullName?: string; + }): Promise { + // load entity + const entity = await this.catalog.getEntityByName(opt.entityRef); + if (!entity) { + throw new Error( + `Couldn't find entity with name: ${stringifyEntityRef(opt.entityRef)}`, + ); + } + + // lookup `[jenkinsName#]jobFullName` from entity annotation + const jenkinsAndJobName = + DefaultJenkinsInfoProvider.getEntityAnnotationValue(entity); + if (!jenkinsAndJobName) { + throw new Error( + `Couldn't find jenkins annotation (${ + DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION + }) on entity with name: ${stringifyEntityRef(opt.entityRef)}`, + ); + } + + let jobFullName; + let jenkinsName: string | undefined; + const splitIndex = jenkinsAndJobName.indexOf(':'); + if (splitIndex === -1) { + // no jenkinsName specified, use default + jobFullName = jenkinsAndJobName; + } else { + // There is a jenkinsName specified + jenkinsName = jenkinsAndJobName.substring(0, splitIndex); + jobFullName = jenkinsAndJobName.substring( + splitIndex + 1, + jenkinsAndJobName.length, + ); + } + + // lookup baseURL + creds from config + const instanceConfig = this.config.getInstanceConfig(jenkinsName); + + const creds = Buffer.from( + `${instanceConfig.username}:${instanceConfig.apiKey}`, + 'binary', + ).toString('base64'); + + return { + baseUrl: instanceConfig.baseUrl, + headers: { + Authorization: `Basic ${creds}`, + }, + jobFullName, + }; + } + + private static getEntityAnnotationValue(entity: Entity) { + return ( + entity.metadata.annotations?.[ + DefaultJenkinsInfoProvider.OLD_JENKINS_ANNOTATION + ] || + entity.metadata.annotations?.[ + DefaultJenkinsInfoProvider.NEW_JENKINS_ANNOTATION + ] + ); } } From 3201f09734dd2213ac2d6fcad4c87617b413c1d1 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Thu, 12 Aug 2021 16:09:55 +0200 Subject: [PATCH 009/102] add test Signed-off-by: Samira Mokaram --- .../src/lib/oauth/OAuthAdapter.ts | 9 +++- .../auth-backend/src/service/router.test.ts | 46 +++++++++++++++++++ plugins/auth-backend/src/service/router.ts | 4 +- 3 files changed, 56 insertions(+), 3 deletions(-) create mode 100644 plugins/auth-backend/src/service/router.test.ts diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts index 6397c09233..df9fb43cbe 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.ts @@ -26,7 +26,12 @@ import { InputError, NotAllowedError } from '@backstage/errors'; import { TokenIssuer } from '../../identity/types'; import { readState, verifyNonce } from './helpers'; import { postMessageResponse, ensuresXRequestedWith } from '../flow'; -import { OAuthHandlers, OAuthStartRequest, OAuthRefreshRequest } from './types'; +import { + OAuthHandlers, + OAuthStartRequest, + OAuthRefreshRequest, + OAuthState, +} from './types'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export const TEN_MINUTES_MS = 600 * 1000; @@ -109,7 +114,7 @@ export class OAuthAdapter implements AuthProviderRouteHandlers { let appOrigin = this.options.appOrigin; try { - const state: any = readState(req.query.state?.toString() ?? ''); + const state: OAuthState = readState(req.query.state?.toString() ?? ''); if (state.origin) { try { diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/service/router.test.ts new file mode 100644 index 0000000000..f0f5134848 --- /dev/null +++ b/plugins/auth-backend/src/service/router.test.ts @@ -0,0 +1,46 @@ +/* + * 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 { ConfigReader } from '@backstage/config'; +import { createOriginFilter } from './router'; + +describe('Auth origin filtering', () => { + const defaultConfigOptions = { + auth: { + experimentalExtraAllowedOrigins: ['https://test-*.example.net'], + }, + }; + const defaultConfig = () => new ConfigReader(defaultConfigOptions); + const getOptionalString = jest.fn(); + const config = defaultConfig(); + config.getOptionalString = getOptionalString; + it('Will explode, invalid origin', () => { + const origin = 'https://test.example.net'; + expect(createOriginFilter(config)(origin)).toBeFalsy(); + }); + it('Will explode, invalid origin domain', () => { + const origin = 'https://test-1234.examplee.net'; + expect(createOriginFilter(config)(origin)).toBeFalsy(); + }); + it("Won't explode, valid origin with numbers", () => { + const origin = 'https://test-1234.example.net'; + expect(createOriginFilter(config)(origin)).toBeTruthy(); + }); + it("Won't explode, valid origin with chars and numbers", () => { + const origin = 'https://test-test1234.example.net'; + expect(createOriginFilter(config)(origin)).toBeTruthy(); + }); +}); diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index 3c2ed31b94..de6c1ffe03 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -162,7 +162,9 @@ export async function createRouter({ return router; } -function createOriginFilter(config: Config): (origin: string) => boolean { +export function createOriginFilter( + config: Config, +): (origin: string) => boolean { const allowedOrigins = config.getOptionalStringArray( 'auth.experimentalExtraAllowedOrigins', ); From 03121606fcb041138be223e2c40743fa6a370661 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Thu, 12 Aug 2021 16:47:31 +0200 Subject: [PATCH 010/102] fix prettier issue Signed-off-by: Samira Mokaram --- .../src/lib/AuthConnector/DefaultAuthConnector.ts | 3 ++- plugins/auth-backend/src/lib/oauth/types.ts | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts index 88156fedad..261a008d4b 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -61,7 +61,8 @@ function defaultJoinScopes(scopes: Set) { * via the OAuthRequestApi. */ export class DefaultAuthConnector - implements AuthConnector { + implements AuthConnector +{ private readonly discoveryApi: DiscoveryApi; private readonly environment: string; private readonly provider: AuthProvider & { id: string }; diff --git a/plugins/auth-backend/src/lib/oauth/types.ts b/plugins/auth-backend/src/lib/oauth/types.ts index 989b76c0a4..7912dd16a5 100644 --- a/plugins/auth-backend/src/lib/oauth/types.ts +++ b/plugins/auth-backend/src/lib/oauth/types.ts @@ -107,9 +107,7 @@ export interface OAuthHandlers { * Handles the redirect from the auth provider when the user has signed in. * @param {express.Request} req */ - handler( - req: express.Request, - ): Promise<{ + handler(req: express.Request): Promise<{ response: AuthResponse; refreshToken?: string; }>; From 5bab4fe2a5b7a796bc728ae4d29ddffc2039fee2 Mon Sep 17 00:00:00 2001 From: Mike Lewis Date: Thu, 5 Aug 2021 14:58:00 +0100 Subject: [PATCH 011/102] scaffolder: merge default field extensions with custom field extensions Signed-off-by: Mike Lewis --- .changeset/afraid-pots-protect.md | 19 +++++++++++++++++++ packages/app/src/App.tsx | 8 -------- plugins/scaffolder/src/components/Router.tsx | 14 ++++++++++---- 3 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 .changeset/afraid-pots-protect.md diff --git a/.changeset/afraid-pots-protect.md b/.changeset/afraid-pots-protect.md new file mode 100644 index 0000000000..35ba1dc861 --- /dev/null +++ b/.changeset/afraid-pots-protect.md @@ -0,0 +1,19 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Previously when supplying custom scaffolder field extensions, it was necessary to also include the default ones if they were needed. Since the field extensions are keyed by name, there's no harm in leaving the default ones in place when adding custom ones - if templates don't refer to them they will be ignored, and if custom ones are introduced with the same name, the custom ones will take priority over the default ones. + +Users configuring custom field extensions can remove the default ones from the scaffolder route after this change, and they'll still be available: + +```diff + }> + +- +- +- +- + + + +``` diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 1132368a91..f0ee099924 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -45,10 +45,6 @@ import { ScaffolderPage, scaffolderPlugin, ScaffolderFieldExtensions, - RepoUrlPickerFieldExtension, - OwnerPickerFieldExtension, - EntityPickerFieldExtension, - EntityNamePickerFieldExtension, } from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; @@ -129,10 +125,6 @@ const routes = ( /> }> - - - - diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 43c8dd6ebb..3d7bbcaf5f 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -32,7 +32,7 @@ import { useElementFilter } from '@backstage/core-plugin-api'; export const Router = () => { const outlet = useOutlet(); - const foundExtensions = useElementFilter(outlet, elements => + const customFieldExtensions = useElementFilter(outlet, elements => elements .selectByComponentData({ key: FIELD_EXTENSION_WRAPPER_KEY, @@ -42,9 +42,15 @@ export const Router = () => { }), ); - const fieldExtensions = foundExtensions.length - ? foundExtensions - : DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS; + const fieldExtensions = [ + ...customFieldExtensions, + ...DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS.filter( + ({ name }) => + !customFieldExtensions.some( + customFieldExtension => customFieldExtension.name === name, + ), + ), + ]; return ( From 782cd0ed1ea95ef90aa912482bb05d51f7054a81 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Fri, 13 Aug 2021 09:58:46 +0200 Subject: [PATCH 012/102] add scape Signed-off-by: Samira Mokaram --- plugins/auth-backend/src/service/router.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/service/router.test.ts index f0f5134848..ca8a14f4e6 100644 --- a/plugins/auth-backend/src/service/router.test.ts +++ b/plugins/auth-backend/src/service/router.test.ts @@ -28,19 +28,19 @@ describe('Auth origin filtering', () => { const config = defaultConfig(); config.getOptionalString = getOptionalString; it('Will explode, invalid origin', () => { - const origin = 'https://test.example.net'; + const origin = 'https://test\\.example.net'; expect(createOriginFilter(config)(origin)).toBeFalsy(); }); it('Will explode, invalid origin domain', () => { - const origin = 'https://test-1234.examplee.net'; + const origin = 'https://test-1234\\.examplee.net'; expect(createOriginFilter(config)(origin)).toBeFalsy(); }); it("Won't explode, valid origin with numbers", () => { - const origin = 'https://test-1234.example.net'; + const origin = 'https://test-1234\\.example.net'; expect(createOriginFilter(config)(origin)).toBeTruthy(); }); it("Won't explode, valid origin with chars and numbers", () => { - const origin = 'https://test-test1234.example.net'; + const origin = 'https://test-test1234\\.example.net'; expect(createOriginFilter(config)(origin)).toBeTruthy(); }); }); From fe506a0cf88959c44206a1af81619cf8d4ce03cc Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 13 Aug 2021 11:05:59 +0200 Subject: [PATCH 013/102] cli/webpack: replace depreacted hash with fullhash Signed-off-by: Johan Haals --- .changeset/thirty-gorillas-pull.md | 5 +++++ packages/cli/src/lib/bundler/config.ts | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 .changeset/thirty-gorillas-pull.md diff --git a/.changeset/thirty-gorillas-pull.md b/.changeset/thirty-gorillas-pull.md new file mode 100644 index 0000000000..3d105d4e8e --- /dev/null +++ b/.changeset/thirty-gorillas-pull.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Remove webpack deprecation message when running build. diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index c9e858a31c..adb3a668d7 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -209,7 +209,7 @@ export async function createConfig( output: { path: paths.targetDist, publicPath: validBaseUrl.pathname, - filename: isDev ? '[name].js' : 'static/[name].[hash:8].js', + filename: isDev ? '[name].js' : 'static/[name].[fullhash:8].js', chunkFilename: isDev ? '[name].chunk.js' : 'static/[name].[chunkhash:8].chunk.js', From 2518aab58822f4a57c1f4c24c1043d41ea0693e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 13 Aug 2021 11:11:06 +0200 Subject: [PATCH 014/102] Compensate for error formatting mismatch between Webpack 5 and react-dev-utils MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/neat-days-sip.md | 5 +++++ packages/cli/src/lib/bundler/bundle.ts | 19 ++++++++++++++++--- 2 files changed, 21 insertions(+), 3 deletions(-) create mode 100644 .changeset/neat-days-sip.md diff --git a/.changeset/neat-days-sip.md b/.changeset/neat-days-sip.md new file mode 100644 index 0000000000..b919c784d0 --- /dev/null +++ b/.changeset/neat-days-sip.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Compensate for error formatting mismatch between Webpack 5 and react-dev-utils diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index 082b1247ab..2f23d5cae6 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -117,9 +117,22 @@ async function build(compiler: webpack.Compiler, isCi: boolean) { if (!stats) { throw new Error('No stats provided'); } - const { errors, warnings } = formatWebpackMessages( - stats.toJson({ all: false, warnings: true, errors: true }), - ); + + const serializedStats = stats.toJson({ + all: false, + warnings: true, + errors: true, + }); + // NOTE(freben): The code below that extracts the message part of the errors, + // is due to react-dev-utils not yet being compatible with webpack 5. This + // may be possible to remove (just passing the serialized stats object + // directly into the format function) after a new release of react-dev-utils + // has been made available. + // See https://github.com/facebook/create-react-app/issues/9880 + const { errors, warnings } = formatWebpackMessages({ + errors: serializedStats.errors?.map(e => (e.message ? e.message : e)), + warnings: serializedStats.warnings?.map(e => (e.message ? e.message : e)), + }); if (errors.length) { // Only keep the first error. Others are often indicative From b63a38a9d1f668c4951abc6a31f88f070db6877e Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 13 Aug 2021 11:28:56 +0200 Subject: [PATCH 015/102] chore: Fix vale error Signed-off-by: Johan Haals --- .changeset/thirty-gorillas-pull.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/thirty-gorillas-pull.md b/.changeset/thirty-gorillas-pull.md index 3d105d4e8e..fbb30ddcde 100644 --- a/.changeset/thirty-gorillas-pull.md +++ b/.changeset/thirty-gorillas-pull.md @@ -2,4 +2,4 @@ '@backstage/cli': patch --- -Remove webpack deprecation message when running build. +Remove Webpack deprecation message when running build. From 57d71f3a2ce9a1ea9cd873de2874364670b616f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 13 Aug 2021 11:42:38 +0200 Subject: [PATCH 016/102] Fix windows test errors due to newline differences MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- packages/backend-common/src/reading/AwsS3UrlReader.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index fe45931d53..084f7e3e13 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -160,7 +160,7 @@ describe('AwsS3UrlReader', () => { const response = await awsS3UrlReader.read( 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', ); - expect(response.toString()).toBe('site_name: Test\n'); + expect(response.toString().trim()).toBe('site_name: Test'); }); it('rejects unknown targets', async () => { @@ -214,7 +214,7 @@ describe('AwsS3UrlReader', () => { 'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml', ); const buffer = await response.buffer(); - expect(buffer.toString()).toBe('site_name: Test\n'); + expect(buffer.toString().trim()).toBe('site_name: Test'); }); it('rejects unknown targets', async () => { From 0e7595d191d492a768d9af7ef506991618cec046 Mon Sep 17 00:00:00 2001 From: Miguel Alexandre Date: Fri, 13 Aug 2021 12:13:00 +0200 Subject: [PATCH 017/102] Fix Sentry widget showing [No Type] Signed-off-by: Miguel Alexandre --- .changeset/purple-tigers-tell.md | 5 +++++ .../components/ErrorCell/ErrorCell.test.tsx | 20 +++++++++++++++++++ .../src/components/ErrorCell/ErrorCell.tsx | 11 +++++++--- 3 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 .changeset/purple-tigers-tell.md diff --git a/.changeset/purple-tigers-tell.md b/.changeset/purple-tigers-tell.md new file mode 100644 index 0000000000..9c3d61bcff --- /dev/null +++ b/.changeset/purple-tigers-tell.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-sentry': patch +--- + +Do not show [No Type] when issue metadata includes title instead of type diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx index 98833aaeba..ff1960aa4f 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.test.tsx @@ -44,4 +44,24 @@ describe('Sentry error cell component', () => { 'http://example.com', ); }); + it('should render the title if type is not present', async () => { + const testIssue = { + ...mockIssue, + title: 'Exception: Could not load credentials from any providers', + count: '1', + metadata: {}, + userCount: 2, + permalink: 'http://example.com', + }; + const cell = render( + + + , + ); + const errorType = await cell.findByText('Exception: Could not load cr...'); + expect(errorType.closest('a')).toHaveAttribute( + 'href', + 'http://example.com', + ); + }); }); diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx index 57f9cddc05..d2da63f01f 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx @@ -44,13 +44,18 @@ const useStyles = makeStyles(theme => ({ export const ErrorCell = ({ sentryIssue }: { sentryIssue: SentryIssue }) => { const classes = useStyles(); + let issueType = '[No Type]'; + if (sentryIssue.metadata.type) { + issueType = sentryIssue.metadata.type; + } else if (sentryIssue.title) { + issueType = sentryIssue.title; + } + return (
- {sentryIssue.metadata.type - ? stripText(sentryIssue.metadata.type, 28) - : '[No type]'} + {stripText(issueType, 28)} Date: Fri, 13 Aug 2021 13:22:00 +0200 Subject: [PATCH 018/102] add changest Signed-off-by: Samira Mokaram --- .changeset/fifty-insects-love.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/fifty-insects-love.md diff --git a/.changeset/fifty-insects-love.md b/.changeset/fifty-insects-love.md new file mode 100644 index 0000000000..6df7897f05 --- /dev/null +++ b/.changeset/fifty-insects-love.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-app-api': minor +'@backstage/plugin-auth-backend': minor +--- + +Add support for additional app origins From d373bae7aae26b30af620447a5e9c49301c3ed3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 13 Aug 2021 16:35:23 +0200 Subject: [PATCH 019/102] add the hash column MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/catalog-backend/knexfile.js | 26 ++++++++++++ .../20210813143113_add_refresh_state_hash.js | 40 +++++++++++++++++++ 2 files changed, 66 insertions(+) create mode 100644 plugins/catalog-backend/knexfile.js create mode 100644 plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js diff --git a/plugins/catalog-backend/knexfile.js b/plugins/catalog-backend/knexfile.js new file mode 100644 index 0000000000..4c8be42673 --- /dev/null +++ b/plugins/catalog-backend/knexfile.js @@ -0,0 +1,26 @@ +/* + * 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. + */ + +// This file makes it possible to run "yarn knex migrate:make some_file_name" +// to assist in making new migrations +module.exports = { + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + migrations: { + directory: './migrations', + }, +}; diff --git a/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js b/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js new file mode 100644 index 0000000000..5ac9a95689 --- /dev/null +++ b/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js @@ -0,0 +1,40 @@ +/* + * 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. + */ + +// @ts-check + +/** + * @param {import('knex').Knex} knex + */ +exports.up = async function up(knex) { + await knex.schema.alterTable('refresh_state', table => { + table + .text('hash') + .nullable() + .comment( + 'A hash of the processed contents, used to avoid duplicate work', + ); + }); +}; + +/** + * @param {import('knex').Knex} knex + */ +exports.down = async function down(knex) { + await knex.schema.alterTable('refresh_state', table => { + table.dropColumn('hash'); + }); +}; From e671ac95920875c414817d6e709ace9b47734d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 13 Aug 2021 19:02:15 +0200 Subject: [PATCH 020/102] compute hash and update on each round MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../next/DefaultCatalogProcessingEngine.ts | 29 ++++++++++++++++++- .../DefaultProcessingDatabase.test.ts | 11 ++++++- .../database/DefaultProcessingDatabase.ts | 3 ++ .../src/next/database/tables.ts | 1 + .../src/next/database/types.ts | 3 ++ 5 files changed, 45 insertions(+), 2 deletions(-) diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts index 0ae20c94b1..89bf3616ca 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts @@ -20,6 +20,8 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { serializeError } from '@backstage/errors'; +import { createHash } from 'crypto'; +import stableStringify from 'fast-json-stable-stringify'; import { Logger } from 'winston'; import { ProcessingDatabase, RefreshStateItem } from './database/types'; import { CatalogProcessingOrchestrator } from './processing/types'; @@ -125,7 +127,14 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { }, processTask: async item => { try { - const { id, state, unprocessedEntity, entityRef, locationKey } = item; + const { + id, + state, + unprocessedEntity, + entityRef, + locationKey, + hash: previousHash, + } = item; const result = await this.orchestrator.process({ entity: unprocessedEntity, state, @@ -142,6 +151,22 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { result.errors.map(e => serializeError(e)), ); + let hashBuilder = createHash('sha1').update(errorsString); + if (result.ok) { + hashBuilder = hashBuilder + .update(stableStringify({ ...result.completedEntity })) + .update(stableStringify([...result.deferredEntities])) + .update(stableStringify([...result.relations])) + .update(stableStringify({ ...result.state })); + } + + const hash = hashBuilder.digest('hex'); + if (hash === previousHash) { + console.log('skipping ', entityRef); + return; + } + console.log('going ahead with ', entityRef); + // If the result was marked as not OK, it signals that some part of the // processing pipeline threw an exception. This can happen both as part of // non-catastrophic things such as due to validation errors, as well as if @@ -154,6 +179,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { await this.processingDatabase.updateProcessedEntityErrors(tx, { id, errors: errorsString, + hash, }); }); await this.stitcher.stitch( @@ -167,6 +193,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { await this.processingDatabase.updateProcessedEntity(tx, { id, processedEntity: result.completedEntity, + hash, state: result.state, errors: errorsString, relations: result.relations, diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts index 6a0626afc4..8b59e5b5da 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts @@ -206,6 +206,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, + hash: '', state: new Map(), relations: [], deferredEntities: [], @@ -224,6 +225,7 @@ describe('Default Processing Database', () => { const options = { id, processedEntity, + hash: '', state: new Map(), relations: [], deferredEntities: [], @@ -250,7 +252,11 @@ describe('Default Processing Database', () => { await db.transaction(tx => expect( - db.updateProcessedEntity(tx, { ...options, locationKey: 'fail' }), + db.updateProcessedEntity(tx, { + ...options, + hash: '', + locationKey: 'fail', + }), ).rejects.toThrow( `Conflicting write of processing result for ${id} with location key 'fail'`, ), @@ -280,6 +286,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, + hash: '', state, relations: [], deferredEntities: [], @@ -336,6 +343,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, + hash: '', state: new Map(), relations: relations, deferredEntities: [], @@ -387,6 +395,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, + hash: '', state: new Map(), relations: [], deferredEntities, diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts index 1f1ea4bf84..1511854a03 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts @@ -60,6 +60,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const { id, processedEntity, + hash, state, errors, relations, @@ -69,6 +70,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const refreshResult = await tx('refresh_state') .update({ processed_entity: JSON.stringify(processedEntity), + hash, cache: JSON.stringify(state), errors, location_key: locationKey, @@ -492,6 +494,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { processedEntity: i.processed_entity ? (JSON.parse(i.processed_entity) as Entity) : undefined, + hash: i.hash, nextUpdateAt: i.next_update_at, lastDiscoveryAt: i.last_discovery_at, state: i.cache diff --git a/plugins/catalog-backend/src/next/database/tables.ts b/plugins/catalog-backend/src/next/database/tables.ts index 395694131d..594b7a3ade 100644 --- a/plugins/catalog-backend/src/next/database/tables.ts +++ b/plugins/catalog-backend/src/next/database/tables.ts @@ -25,6 +25,7 @@ export type DbRefreshStateRow = { entity_ref: string; unprocessed_entity: string; processed_entity?: string; + hash?: string; cache?: string; next_update_at: string; last_discovery_at: string; // remove? diff --git a/plugins/catalog-backend/src/next/database/types.ts b/plugins/catalog-backend/src/next/database/types.ts index 78199900e4..c559895d27 100644 --- a/plugins/catalog-backend/src/next/database/types.ts +++ b/plugins/catalog-backend/src/next/database/types.ts @@ -34,6 +34,7 @@ export type AddUnprocessedEntitiesResult = {}; export type UpdateProcessedEntityOptions = { id: string; processedEntity: Entity; + hash: string; state?: Map; errors?: string; relations: EntityRelationSpec[]; @@ -44,6 +45,7 @@ export type UpdateProcessedEntityOptions = { export type UpdateProcessedEntityErrorsOptions = { id: string; errors?: string; + hash: string; }; export type RefreshStateItem = { @@ -51,6 +53,7 @@ export type RefreshStateItem = { entityRef: string; unprocessedEntity: Entity; processedEntity?: Entity; + hash: string; nextUpdateAt: string; lastDiscoveryAt: string; // remove? state: Map; From 61aa6526f70c67266189d90fa45310cc88336cda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Fri, 13 Aug 2021 19:03:13 +0200 Subject: [PATCH 021/102] add changeset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/cuddly-flowers-film.md | 5 +++++ .../src/next/DefaultCatalogProcessingEngine.test.ts | 2 ++ .../src/next/DefaultCatalogProcessingEngine.ts | 5 +++-- .../src/next/database/DefaultProcessingDatabase.ts | 5 +++-- 4 files changed, 13 insertions(+), 4 deletions(-) create mode 100644 .changeset/cuddly-flowers-film.md diff --git a/.changeset/cuddly-flowers-film.md b/.changeset/cuddly-flowers-film.md new file mode 100644 index 0000000000..290bb74db6 --- /dev/null +++ b/.changeset/cuddly-flowers-film.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Avoid duplicate work by comparing previous processing rounds with the next diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts index 5f3bab63e7..794f2e6d0b 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts @@ -76,6 +76,7 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, + hash: '', state: new Map(), nextUpdateAt: '', lastDiscoveryAt: '', @@ -137,6 +138,7 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, + hash: '', state: new Map(), nextUpdateAt: '', lastDiscoveryAt: '', diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts index 89bf3616ca..8e4a4ddc0c 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts @@ -162,10 +162,11 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { const hash = hashBuilder.digest('hex'); if (hash === previousHash) { - console.log('skipping ', entityRef); + // If nothing changed in our produced outputs, we cannot have any + // significant effect on our surroundings; therefore, we just abort + // without any updates / stitching. return; } - console.log('going ahead with ', entityRef); // If the result was marked as not OK, it signals that some part of the // processing pipeline threw an exception. This can happen both as part of diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts index 1511854a03..bb5f50c7a0 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts @@ -124,11 +124,12 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { options: UpdateProcessedEntityOptions, ): Promise { const tx = txOpaque as Knex.Transaction; - const { id, errors } = options; + const { id, errors, hash } = options; await tx('refresh_state') .update({ errors, + hash, }) .where('entity_id', id); } @@ -494,7 +495,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { processedEntity: i.processed_entity ? (JSON.parse(i.processed_entity) as Entity) : undefined, - hash: i.hash, + hash: i.hash || '', nextUpdateAt: i.next_update_at, lastDiscoveryAt: i.last_discovery_at, state: i.cache From fa1e003e0adbc35c46991d2c7eb9659c729d9257 Mon Sep 17 00:00:00 2001 From: Phil Kuang Date: Fri, 13 Aug 2021 12:55:07 -0400 Subject: [PATCH 022/102] feat(EntityLayout): show not found message for missing entity Signed-off-by: Phil Kuang --- .changeset/gorgeous-ties-doubt.md | 5 ++++ .../EntityLayout/EntityLayout.test.tsx | 24 +++++++++++++++++++ .../components/EntityLayout/EntityLayout.tsx | 15 ++++++++++++ 3 files changed, 44 insertions(+) create mode 100644 .changeset/gorgeous-ties-doubt.md diff --git a/.changeset/gorgeous-ties-doubt.md b/.changeset/gorgeous-ties-doubt.md new file mode 100644 index 0000000000..c046e71572 --- /dev/null +++ b/.changeset/gorgeous-ties-doubt.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog': patch +--- + +Show a Not Found message when navigating to a nonexistent entity diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx index e040c6a67f..f9faa3f90f 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.test.tsx @@ -61,6 +61,30 @@ describe('EntityLayout', () => { expect(rendered.getByText('tabbed-test-content')).toBeInTheDocument(); }); + it('renders error message when entity is not found', async () => { + const noEntityData = { + ...mockEntityData, + entity: undefined, + }; + + const rendered = await renderInTestApp( + + + + +
tabbed-test-content
+
+
+
+
, + ); + + expect(rendered.getByText('Warning: Entity not found')).toBeInTheDocument(); + expect(rendered.queryByText('my-entity')).not.toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-title')).not.toBeInTheDocument(); + expect(rendered.queryByText('tabbed-test-content')).not.toBeInTheDocument(); + }); + it('navigates when user clicks different tab', async () => { const rendered = await renderInTestApp( diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index a139fbaa1e..eabcbe4993 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -23,9 +23,11 @@ import { Content, Header, HeaderLabel, + Link, Page, Progress, RoutedTabs, + WarningPanel, } from '@backstage/core-components'; import { attachComponentData, @@ -245,6 +247,19 @@ export const EntityLayout = ({ {error.toString()} )} + + {!loading && !error && !entity && ( + + + There is no {kind} with the requested{' '} + + kind, namespace, and name + + . + + + )} + Date: Fri, 13 Aug 2021 21:53:23 +0200 Subject: [PATCH 023/102] add test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .../20210813143113_add_refresh_state_hash.js | 4 +- .../DefaultCatalogProcessingEngine.test.ts | 78 ++++++++++++++++++- .../next/DefaultCatalogProcessingEngine.ts | 17 ++-- .../src/next/NextCatalogBuilder.ts | 2 + .../DefaultProcessingDatabase.test.ts | 18 +++-- .../database/DefaultProcessingDatabase.ts | 12 +-- .../src/next/database/tables.ts | 2 +- .../src/next/database/types.ts | 6 +- 8 files changed, 109 insertions(+), 30 deletions(-) diff --git a/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js b/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js index 5ac9a95689..29a102e876 100644 --- a/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js +++ b/plugins/catalog-backend/migrations/20210813143113_add_refresh_state_hash.js @@ -22,7 +22,7 @@ exports.up = async function up(knex) { await knex.schema.alterTable('refresh_state', table => { table - .text('hash') + .text('result_hash') .nullable() .comment( 'A hash of the processed contents, used to avoid duplicate work', @@ -35,6 +35,6 @@ exports.up = async function up(knex) { */ exports.down = async function down(knex) { await knex.schema.alterTable('refresh_state', table => { - table.dropColumn('hash'); + table.dropColumn('result_hash'); }); }; diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts index 794f2e6d0b..898ef08f6c 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.test.ts @@ -15,6 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import { Hash } from 'crypto'; import waitForExpect from 'wait-for-expect'; import { DefaultProcessingDatabase } from './database/DefaultProcessingDatabase'; import { DefaultCatalogProcessingEngine } from './DefaultCatalogProcessingEngine'; @@ -33,6 +34,10 @@ describe('DefaultCatalogProcessingEngine', () => { const stitcher = { stitch: jest.fn(), } as unknown as jest.Mocked; + const hash = { + update: () => hash, + digest: jest.fn(), + } as unknown as jest.Mocked; beforeEach(() => { jest.resetAllMocks(); @@ -57,6 +62,7 @@ describe('DefaultCatalogProcessingEngine', () => { db, orchestrator, stitcher, + () => hash, ); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -76,7 +82,7 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, - hash: '', + resultHash: '', state: new Map(), nextUpdateAt: '', lastDiscoveryAt: '', @@ -118,6 +124,7 @@ describe('DefaultCatalogProcessingEngine', () => { db, orchestrator, stitcher, + () => hash, ); db.transaction.mockImplementation(cb => cb((() => {}) as any)); @@ -138,7 +145,7 @@ describe('DefaultCatalogProcessingEngine', () => { kind: 'Location', metadata: { name: 'test' }, }, - hash: '', + resultHash: '', state: new Map(), nextUpdateAt: '', lastDiscoveryAt: '', @@ -160,4 +167,71 @@ describe('DefaultCatalogProcessingEngine', () => { }); await engine.stop(); }); + + it('runs fully when hash mismatches, early-outs when hash matches', async () => { + const entity = { + apiVersion: '1', + kind: 'Location', + metadata: { name: 'test' }, + }; + + const refreshState = { + id: '', + entityRef: '', + unprocessedEntity: entity, + resultHash: 'the matching hash', + state: new Map(), + nextUpdateAt: '', + lastDiscoveryAt: '', + }; + + hash.digest.mockReturnValue('the matching hash'); + + orchestrator.process.mockResolvedValue({ + ok: true, + completedEntity: entity, + relations: [], + errors: [], + deferredEntities: [], + state: new Map(), + }); + + const engine = new DefaultCatalogProcessingEngine( + getVoidLogger(), + [], + db, + orchestrator, + stitcher, + () => hash, + ); + + db.transaction.mockImplementation(cb => cb((() => {}) as any)); + + db.getProcessableEntities + .mockResolvedValueOnce({ + items: [{ ...refreshState, resultHash: 'NOT RIGHT' }], + }) + .mockResolvedValue({ items: [] }); + + await engine.start(); + + await waitForExpect(() => { + expect(orchestrator.process).toBeCalledTimes(1); + expect(hash.digest).toBeCalledTimes(1); + expect(db.updateProcessedEntity).toBeCalledTimes(1); + }); + + db.getProcessableEntities + .mockReset() + .mockResolvedValueOnce({ items: [refreshState] }) + .mockResolvedValue({ items: [] }); + + await waitForExpect(() => { + expect(orchestrator.process).toBeCalledTimes(2); + expect(hash.digest).toBeCalledTimes(2); + expect(db.updateProcessedEntity).toBeCalledTimes(1); + }); + + await engine.stop(); + }); }); diff --git a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts index 8e4a4ddc0c..6468fa5187 100644 --- a/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts +++ b/plugins/catalog-backend/src/next/DefaultCatalogProcessingEngine.ts @@ -20,7 +20,7 @@ import { stringifyEntityRef, } from '@backstage/catalog-model'; import { serializeError } from '@backstage/errors'; -import { createHash } from 'crypto'; +import { Hash } from 'crypto'; import stableStringify from 'fast-json-stable-stringify'; import { Logger } from 'winston'; import { ProcessingDatabase, RefreshStateItem } from './database/types'; @@ -91,6 +91,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { private readonly processingDatabase: ProcessingDatabase, private readonly orchestrator: CatalogProcessingOrchestrator, private readonly stitcher: Stitcher, + private readonly createHash: () => Hash, ) {} async start() { @@ -133,7 +134,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { unprocessedEntity, entityRef, locationKey, - hash: previousHash, + resultHash: previousResultHash, } = item; const result = await this.orchestrator.process({ entity: unprocessedEntity, @@ -151,17 +152,17 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { result.errors.map(e => serializeError(e)), ); - let hashBuilder = createHash('sha1').update(errorsString); + let hashBuilder = this.createHash().update(errorsString); if (result.ok) { hashBuilder = hashBuilder .update(stableStringify({ ...result.completedEntity })) .update(stableStringify([...result.deferredEntities])) .update(stableStringify([...result.relations])) - .update(stableStringify({ ...result.state })); + .update(stableStringify(Object.fromEntries(result.state))); } - const hash = hashBuilder.digest('hex'); - if (hash === previousHash) { + const resultHash = hashBuilder.digest('hex'); + if (resultHash === previousResultHash) { // If nothing changed in our produced outputs, we cannot have any // significant effect on our surroundings; therefore, we just abort // without any updates / stitching. @@ -180,7 +181,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { await this.processingDatabase.updateProcessedEntityErrors(tx, { id, errors: errorsString, - hash, + resultHash, }); }); await this.stitcher.stitch( @@ -194,7 +195,7 @@ export class DefaultCatalogProcessingEngine implements CatalogProcessingEngine { await this.processingDatabase.updateProcessedEntity(tx, { id, processedEntity: result.completedEntity, - hash, + resultHash, state: result.state, errors: errorsString, relations: result.relations, diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index a525dacd51..fb12077508 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -31,6 +31,7 @@ import { } from '@backstage/catalog-model'; import { Config } from '@backstage/config'; import { ScmIntegrations } from '@backstage/integration'; +import { createHash } from 'crypto'; import lodash from 'lodash'; import { Logger } from 'winston'; import { @@ -310,6 +311,7 @@ export class NextCatalogBuilder { processingDatabase, orchestrator, stitcher, + () => createHash('sha1'), ); const locationsCatalog = new DatabaseLocationsCatalog(db); diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts index 8b59e5b5da..5103c25dbc 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts @@ -19,8 +19,8 @@ import { TestDatabaseId, TestDatabases } from '@backstage/backend-test-utils'; import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/config'; import { Knex } from 'knex'; -import { Logger } from 'winston'; import * as uuid from 'uuid'; +import { Logger } from 'winston'; import { DatabaseManager } from './DatabaseManager'; import { DefaultProcessingDatabase } from './DefaultProcessingDatabase'; import { @@ -206,7 +206,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, - hash: '', + resultHash: '', state: new Map(), relations: [], deferredEntities: [], @@ -225,7 +225,7 @@ describe('Default Processing Database', () => { const options = { id, processedEntity, - hash: '', + resultHash: '', state: new Map(), relations: [], deferredEntities: [], @@ -254,7 +254,7 @@ describe('Default Processing Database', () => { expect( db.updateProcessedEntity(tx, { ...options, - hash: '', + resultHash: '', locationKey: 'fail', }), ).rejects.toThrow( @@ -286,7 +286,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, - hash: '', + resultHash: '', state, relations: [], deferredEntities: [], @@ -302,7 +302,9 @@ describe('Default Processing Database', () => { expect(entities[0].processed_entity).toEqual( JSON.stringify(processedEntity), ); - expect(entities[0].cache).toEqual(JSON.stringify(state)); + expect(entities[0].cache).toEqual( + JSON.stringify(Object.fromEntries(state)), + ); expect(entities[0].errors).toEqual("['something broke']"); expect(entities[0].location_key).toEqual('key'); }, @@ -343,7 +345,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, - hash: '', + resultHash: '', state: new Map(), relations: relations, deferredEntities: [], @@ -395,7 +397,7 @@ describe('Default Processing Database', () => { db.updateProcessedEntity(tx, { id, processedEntity, - hash: '', + resultHash: '', state: new Map(), relations: [], deferredEntities, diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts index bb5f50c7a0..177bf979ab 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts @@ -60,7 +60,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const { id, processedEntity, - hash, + resultHash, state, errors, relations, @@ -70,8 +70,8 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { const refreshResult = await tx('refresh_state') .update({ processed_entity: JSON.stringify(processedEntity), - hash, - cache: JSON.stringify(state), + result_hash: resultHash, + cache: JSON.stringify(Object.fromEntries(state || [])), errors, location_key: locationKey, }) @@ -124,12 +124,12 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { options: UpdateProcessedEntityOptions, ): Promise { const tx = txOpaque as Knex.Transaction; - const { id, errors, hash } = options; + const { id, errors, resultHash } = options; await tx('refresh_state') .update({ errors, - hash, + result_hash: resultHash, }) .where('entity_id', id); } @@ -495,7 +495,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { processedEntity: i.processed_entity ? (JSON.parse(i.processed_entity) as Entity) : undefined, - hash: i.hash || '', + resultHash: i.result_hash || '', nextUpdateAt: i.next_update_at, lastDiscoveryAt: i.last_discovery_at, state: i.cache diff --git a/plugins/catalog-backend/src/next/database/tables.ts b/plugins/catalog-backend/src/next/database/tables.ts index 594b7a3ade..d735607554 100644 --- a/plugins/catalog-backend/src/next/database/tables.ts +++ b/plugins/catalog-backend/src/next/database/tables.ts @@ -25,7 +25,7 @@ export type DbRefreshStateRow = { entity_ref: string; unprocessed_entity: string; processed_entity?: string; - hash?: string; + result_hash?: string; cache?: string; next_update_at: string; last_discovery_at: string; // remove? diff --git a/plugins/catalog-backend/src/next/database/types.ts b/plugins/catalog-backend/src/next/database/types.ts index c559895d27..c93b90084d 100644 --- a/plugins/catalog-backend/src/next/database/types.ts +++ b/plugins/catalog-backend/src/next/database/types.ts @@ -34,7 +34,7 @@ export type AddUnprocessedEntitiesResult = {}; export type UpdateProcessedEntityOptions = { id: string; processedEntity: Entity; - hash: string; + resultHash: string; state?: Map; errors?: string; relations: EntityRelationSpec[]; @@ -45,7 +45,7 @@ export type UpdateProcessedEntityOptions = { export type UpdateProcessedEntityErrorsOptions = { id: string; errors?: string; - hash: string; + resultHash: string; }; export type RefreshStateItem = { @@ -53,7 +53,7 @@ export type RefreshStateItem = { entityRef: string; unprocessedEntity: Entity; processedEntity?: Entity; - hash: string; + resultHash: string; nextUpdateAt: string; lastDiscoveryAt: string; // remove? state: Map; From 517133c0acf355e0dee55b661905bd7d772858de Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 04:46:48 +0000 Subject: [PATCH 024/102] chore(deps): bump @material-ui/core from 4.12.2 to 4.12.3 Bumps [@material-ui/core](https://github.com/mui-org/material-ui/tree/HEAD/packages/material-ui) from 4.12.2 to 4.12.3. - [Release notes](https://github.com/mui-org/material-ui/releases) - [Changelog](https://github.com/mui-org/material-ui/blob/v4.12.3/CHANGELOG.md) - [Commits](https://github.com/mui-org/material-ui/commits/v4.12.3/packages/material-ui) --- updated-dependencies: - dependency-name: "@material-ui/core" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/yarn.lock b/yarn.lock index d14fac46fa..d90f6bda6c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2193,14 +2193,7 @@ core-js-pure "^3.0.0" regenerator-runtime "^0.13.4" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": - version "7.14.6" - resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz#535203bc0892efc7dec60bdc27b2ecf6e409062d" - integrity sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg== - dependencies: - regenerator-runtime "^0.13.4" - -"@babel/runtime@^7.14.8": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.0", "@babel/runtime@^7.10.1", "@babel/runtime@^7.10.2", "@babel/runtime@^7.10.4", "@babel/runtime@^7.10.5", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.5", "@babel/runtime@^7.14.0", "@babel/runtime@^7.14.8", "@babel/runtime@^7.3.1", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.0", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.0", "@babel/runtime@^7.6.3", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.3", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.14.8" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.8.tgz#7119a56f421018852694290b9f9148097391b446" integrity sha512-twj3L8Og5SaCRCErB4x4ajbvBIVV77CGeFglHpeg5WC5FF8TZzBWXtTJ4MqaD9QszLYTtr+IsaAL2rEUevb+eg== @@ -4514,9 +4507,9 @@ react-double-scrollbar "0.0.15" "@material-ui/core@^4.11.0", "@material-ui/core@^4.11.3", "@material-ui/core@^4.12.2": - version "4.12.2" - resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.2.tgz#59a8b19f16b8c218d912f37f5ae70473c3c82c73" - integrity sha512-Q1npB8V73IC+eV2X6as+g71MpEGQwqKHUI2iujY62npk35V8nMx/bUXAHjv5kKG1BZ8s8XUWoG6s/VkjYPjjQA== + version "4.12.3" + resolved "https://registry.npmjs.org/@material-ui/core/-/core-4.12.3.tgz#80d665caf0f1f034e52355c5450c0e38b099d3ca" + integrity sha512-sdpgI/PL56QVsEJldwEe4FFaFTLUqN+rd7sSZiRCdx2E/C7z5yK0y/khAWVBH24tXwto7I1hCzNWfJGZIYJKnw== dependencies: "@babel/runtime" "^7.4.4" "@material-ui/styles" "^4.11.4" @@ -22691,12 +22684,7 @@ react-is@^16.7.0, react-is@^16.8.0, react-is@^16.8.1, react-is@^16.8.4, react-is resolved "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== -"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1: - version "17.0.1" - resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" - integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== - -react-is@^17.0.2: +"react-is@^16.8.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== From 451f4058103839da4b0f064b24bf9951c9d8c1b3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 16 Aug 2021 04:48:05 +0000 Subject: [PATCH 025/102] chore(deps): bump @roadiehq/backstage-plugin-github-pull-requests Bumps [@roadiehq/backstage-plugin-github-pull-requests](https://github.com/RoadieHQ/backstage-plugin-github-pull-requests) from 1.0.10 to 1.0.13. - [Release notes](https://github.com/RoadieHQ/backstage-plugin-github-pull-requests/releases) - [Commits](https://github.com/RoadieHQ/backstage-plugin-github-pull-requests/commits) --- updated-dependencies: - dependency-name: "@roadiehq/backstage-plugin-github-pull-requests" dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index d14fac46fa..d92653e078 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5208,15 +5208,15 @@ react-use "^17.2.4" "@roadiehq/backstage-plugin-github-pull-requests@^1.0.10": - version "1.0.10" - resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.0.10.tgz#8c921393d25a4d498c196fcab7d6138c5e2998ba" - integrity sha512-8mIhWpxIDZBUrfK72+Pc6g8e/nNo+IA0wbDqWbGO53gw8J2dWSFIU06IRpatN672pzkqrrfzt1qhQujzQslRkg== + version "1.0.13" + resolved "https://registry.npmjs.org/@roadiehq/backstage-plugin-github-pull-requests/-/backstage-plugin-github-pull-requests-1.0.13.tgz#18673d5b906f03fe9b34e388f85e699f51ce89f0" + integrity sha512-k9z2uas1SiCLC93A6kUu8smeKnBA6GbSdY+VhhMgojHmGkhS669HxIbCyt9xyf1S6/G7o6/4MibxHBvHfszI4Q== dependencies: "@backstage/catalog-model" "^0.9.0" "@backstage/core-app-api" "^0.1.3" - "@backstage/core-components" "^0.1.3" + "@backstage/core-components" "^0.3.0" "@backstage/core-plugin-api" "^0.1.3" - "@backstage/plugin-catalog-react" "^0.3.0" + "@backstage/plugin-catalog-react" "^0.4.0" "@material-ui/core" "^4.11.0" "@material-ui/icons" "^4.9.1" "@octokit/rest" "^18.5.3" From 485438a5690b92a682ab2486d66b78e6b34228fc Mon Sep 17 00:00:00 2001 From: Joel Low Date: Mon, 16 Aug 2021 13:36:19 +0800 Subject: [PATCH 026/102] Fix passing arguments from `backstage-cli backend:dev` to package The old Webpack version would invoke the backend package in the same process, hence `process.argv` would contain the arguments passed to `backstage-cli`. This no longer seems to be true in Webpack 5, so arguments must be explicitly passed. Signed-off-by: Joel Low --- .changeset/two-suits-act.md | 5 +++++ packages/cli/src/lib/bundler/config.ts | 1 + 2 files changed, 6 insertions(+) create mode 100644 .changeset/two-suits-act.md diff --git a/.changeset/two-suits-act.md b/.changeset/two-suits-act.md new file mode 100644 index 0000000000..935460a93e --- /dev/null +++ b/.changeset/two-suits-act.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fix `backstage-cli backend:dev` argument passing diff --git a/packages/cli/src/lib/bundler/config.ts b/packages/cli/src/lib/bundler/config.ts index c9e858a31c..d6a4a1534f 100644 --- a/packages/cli/src/lib/bundler/config.ts +++ b/packages/cli/src/lib/bundler/config.ts @@ -318,6 +318,7 @@ export async function createBackendConfig( new RunScriptWebpackPlugin({ name: 'main.js', nodeArgs: options.inspectEnabled ? ['--inspect'] : undefined, + args: process.argv.slice(3), // drop `node backstage-cli backend:dev` }), new webpack.HotModuleReplacementPlugin(), ...(checksEnabled From 22f775d19faf816461bf591e9a75d21db55f6b71 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 13 Aug 2021 14:40:33 +0200 Subject: [PATCH 027/102] Catalog: Add refresh spread functionality Signed-off-by: Johan Haals --- plugins/catalog-backend/package.json | 3 +- .../src/next/NextCatalogBuilder.ts | 11 +++++ .../DefaultProcessingDatabase.test.ts | 44 +++++++++++++++++++ .../database/DefaultProcessingDatabase.ts | 26 ++++++----- 4 files changed, 73 insertions(+), 11 deletions(-) diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 0bf5a08dfd..f095f39785 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -73,7 +73,8 @@ "msw": "^0.29.0", "sqlite3": "^5.0.1", "supertest": "^6.1.3", - "wait-for-expect": "^3.0.2" + "wait-for-expect": "^3.0.2", + "luxon": "^2.0.2" }, "files": [ "dist", diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index fb12077508..fe7df180ee 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -113,6 +113,7 @@ export class NextCatalogBuilder { private processorsReplace: boolean; private parser: CatalogProcessorParser | undefined; private refreshIntervalSeconds = 100; + private refreshSpreadSeconds = { min: 10, max: 60 }; constructor(env: CatalogEnvironment) { this.env = env; @@ -152,6 +153,15 @@ export class NextCatalogBuilder { return this; } + /** + * Refresh spread configures configures the minimum and maximum number of seconds + * to wait between refreshes in addition to the configured refresh interval. + */ + setRefreshSpreadSeconds(min: number, max: number): NextCatalogBuilder { + this.refreshSpreadSeconds = { min, max }; + return this; + } + /** * Sets what policies to use for validation of entities between the pre- * processing and post-processing stages. All such policies must pass for the @@ -286,6 +296,7 @@ export class NextCatalogBuilder { database: dbClient, logger, refreshIntervalSeconds: this.refreshIntervalSeconds, + refreshSpreadSeconds: this.refreshSpreadSeconds, }); const integrations = ScmIntegrations.fromConfig(config); const orchestrator = new DefaultCatalogProcessingOrchestrator({ diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts index 5103c25dbc..690dcc1b7b 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts @@ -20,7 +20,11 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/config'; import { Knex } from 'knex'; import * as uuid from 'uuid'; +<<<<<<< HEAD import { Logger } from 'winston'; +======= +import { DateTime } from 'luxon'; +>>>>>>> d31a82869 (Catalog: Add refresh spread functionality) import { DatabaseManager } from './DatabaseManager'; import { DefaultProcessingDatabase } from './DefaultProcessingDatabase'; import { @@ -47,6 +51,7 @@ describe('Default Processing Database', () => { database: knex, logger, refreshIntervalSeconds: 100, + refreshSpreadSeconds: { min: 10, max: 60 }, }), }; } @@ -960,5 +965,44 @@ describe('Default Processing Database', () => { }, 60_000, ); + + it.each(databases.eachSupportedId())( + 'should update the next_refresh interval with a timestamp that includes refresh spread, %p', + async databaseId => { + const { knex, db } = await createDatabase(databaseId); + const entity = JSON.stringify({ + kind: 'Location', + apiVersion: '1.0.0', + metadata: { + name: 'xyz', + }, + } as Entity); + await knex('refresh_state').insert({ + entity_id: '2', + entity_ref: 'location:default/new-root', + unprocessed_entity: entity, + errors: '[]', + next_update_at: '2019-01-01 23:00:00', + last_discovery_at: '2021-04-01 13:37:00', + }); + await db.transaction(async tx => { + // Result does not include the updated timestamp + await db.getProcessableEntities(tx, { + processBatchSize: 1, + }); + }); + const now = DateTime.local(); + const result = await knex('refresh_state') + .where('entity_ref', 'location:default/new-root') + .select(); + const nextUpdate = DateTime.fromSQL(result[0].next_update_at, { + zone: 'utc', + }); + expect(nextUpdate.diff(now, 'seconds').seconds).toBeGreaterThanOrEqual( + 110, + ); + }, + 60_000, + ); }); }); diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts index 177bf979ab..926423c848 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts @@ -49,6 +49,10 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { database: Knex; logger: Logger; refreshIntervalSeconds: number; + refreshSpreadSeconds: { + min: number; + max: number; + }; }, ) {} @@ -447,6 +451,17 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { } } + // Returns the next update timestamp by combining refresh interval and refresh spread + private getNextUpdateAt(tx: Knex.Transaction): Knex.MaybeRawColumn { + const { min, max } = this.options.refreshSpreadSeconds; + const refreshSpread = Math.floor(Math.random() * (max - min + 1)) + min; + const nextUpdate = this.options.refreshIntervalSeconds + refreshSpread; + + return tx.client.config.client === 'sqlite3' + ? tx.raw(`datetime('now', ?)`, [`${nextUpdate} seconds`]) + : tx.raw(`now() + interval '${Number(nextUpdate)} seconds'`); + } + async getProcessableEntities( txOpaque: Transaction, request: { processBatchSize: number }, @@ -473,16 +488,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { items.map(i => i.entity_ref), ) .update({ - next_update_at: - tx.client.config.client === 'sqlite3' - ? tx.raw(`datetime('now', ?)`, [ - `${this.options.refreshIntervalSeconds} seconds`, - ]) - : tx.raw( - `now() + interval '${Number( - this.options.refreshIntervalSeconds, - )} seconds'`, - ), + next_update_at: this.getNextUpdateAt(tx), }); return { From fe960ad0fd60929a2997771d73c049ef404a0e0b Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Fri, 13 Aug 2021 15:31:07 +0200 Subject: [PATCH 028/102] catalog-backend: Add refresh interval function MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fredrik Adelöw Signed-off-by: Johan Haals --- .changeset/early-ducks-stare.md | 8 +++++ .../src/next/NextCatalogBuilder.ts | 30 +++++++++++----- .../DefaultProcessingDatabase.test.ts | 9 +++-- .../database/DefaultProcessingDatabase.ts | 27 ++++++-------- plugins/catalog-backend/src/next/index.ts | 2 ++ plugins/catalog-backend/src/next/refresh.ts | 36 +++++++++++++++++++ 6 files changed, 83 insertions(+), 29 deletions(-) create mode 100644 .changeset/early-ducks-stare.md create mode 100644 plugins/catalog-backend/src/next/refresh.ts diff --git a/.changeset/early-ducks-stare.md b/.changeset/early-ducks-stare.md new file mode 100644 index 0000000000..58b1525e25 --- /dev/null +++ b/.changeset/early-ducks-stare.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-catalog-backend': minor +--- + +Updates the `DefaultProcessingDatabase` to accept a refresh interval function instead of a fixed refresh interval in seconds which used to default to 100s. The catalog now ships with a default refresh interval function that schedules entities for refresh every 100-150 seconds, this should +help to smooth out bursts that occur when a lot of entities are scheduled for refresh at the same second. + +Custom `RefreshIntervalFunction` can be implemented and passed to the CatalogBuilder using `.setInterval(fn)` diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index fe7df180ee..b277f0b16a 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -75,6 +75,10 @@ import { DefaultLocationStore } from './DefaultLocationStore'; import { NextEntitiesCatalog } from './NextEntitiesCatalog'; import { DefaultCatalogProcessingOrchestrator } from './processing/DefaultCatalogProcessingOrchestrator'; import { Stitcher } from './stitching/Stitcher'; +import { + createRandomRefreshInterval, + RefreshIntervalFunction, +} from './refresh'; export type CatalogEnvironment = { logger: Logger; @@ -112,8 +116,11 @@ export class NextCatalogBuilder { private processors: CatalogProcessor[]; private processorsReplace: boolean; private parser: CatalogProcessorParser | undefined; - private refreshIntervalSeconds = 100; - private refreshSpreadSeconds = { min: 10, max: 60 }; + private refreshInterval: RefreshIntervalFunction = + createRandomRefreshInterval({ + minSeconds: 100, + maxSeconds: 150, + }); constructor(env: CatalogEnvironment) { this.env = env; @@ -145,11 +152,15 @@ export class NextCatalogBuilder { /** * Refresh interval determines how often entities should be refreshed. - * The default refresh duration is 100, setting this too low will potentially - * deplete request quotas to upstream services. + * Seconds provided will be multiplied by 1.5 + * The default refresh duration is 100-150 seconds. + * setting this too low will potentially deplete request quotas to upstream services. */ setRefreshIntervalSeconds(seconds: number): NextCatalogBuilder { - this.refreshIntervalSeconds = seconds; + this.refreshInterval = createRandomRefreshInterval({ + minSeconds: seconds, + maxSeconds: seconds * 1.5, + }); return this; } @@ -157,8 +168,10 @@ export class NextCatalogBuilder { * Refresh spread configures configures the minimum and maximum number of seconds * to wait between refreshes in addition to the configured refresh interval. */ - setRefreshSpreadSeconds(min: number, max: number): NextCatalogBuilder { - this.refreshSpreadSeconds = { min, max }; + setRefreshInterval( + refreshInterval: RefreshIntervalFunction, + ): NextCatalogBuilder { + this.refreshInterval = refreshInterval; return this; } @@ -295,8 +308,7 @@ export class NextCatalogBuilder { const processingDatabase = new DefaultProcessingDatabase({ database: dbClient, logger, - refreshIntervalSeconds: this.refreshIntervalSeconds, - refreshSpreadSeconds: this.refreshSpreadSeconds, + refreshInterval: this.refreshInterval, }); const integrations = ScmIntegrations.fromConfig(config); const orchestrator = new DefaultCatalogProcessingOrchestrator({ diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts index 690dcc1b7b..5b717ee36e 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts @@ -32,6 +32,7 @@ import { DbRefreshStateRow, DbRelationsRow, } from './tables'; +import { createRandomRefreshInterval } from '../refresh'; describe('Default Processing Database', () => { const defaultLogger = getVoidLogger(); @@ -50,8 +51,10 @@ describe('Default Processing Database', () => { db: new DefaultProcessingDatabase({ database: knex, logger, - refreshIntervalSeconds: 100, - refreshSpreadSeconds: { min: 10, max: 60 }, + refreshInterval: createRandomRefreshInterval({ + minSeconds: 100, + maxSeconds: 150, + }), }), }; } @@ -999,7 +1002,7 @@ describe('Default Processing Database', () => { zone: 'utc', }); expect(nextUpdate.diff(now, 'seconds').seconds).toBeGreaterThanOrEqual( - 110, + 100, ); }, 60_000, diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts index 926423c848..203ca3efbf 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.ts @@ -23,6 +23,7 @@ import { v4 as uuid } from 'uuid'; import type { Logger } from 'winston'; import { Transaction } from '../../database'; import { DeferredEntity } from '../processing/types'; +import { RefreshIntervalFunction } from '../refresh'; import { DbRefreshStateReferencesRow, DbRefreshStateRow, @@ -48,11 +49,7 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { private readonly options: { database: Knex; logger: Logger; - refreshIntervalSeconds: number; - refreshSpreadSeconds: { - min: number; - max: number; - }; + refreshInterval: RefreshIntervalFunction; }, ) {} @@ -451,17 +448,6 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { } } - // Returns the next update timestamp by combining refresh interval and refresh spread - private getNextUpdateAt(tx: Knex.Transaction): Knex.MaybeRawColumn { - const { min, max } = this.options.refreshSpreadSeconds; - const refreshSpread = Math.floor(Math.random() * (max - min + 1)) + min; - const nextUpdate = this.options.refreshIntervalSeconds + refreshSpread; - - return tx.client.config.client === 'sqlite3' - ? tx.raw(`datetime('now', ?)`, [`${nextUpdate} seconds`]) - : tx.raw(`now() + interval '${Number(nextUpdate)} seconds'`); - } - async getProcessableEntities( txOpaque: Transaction, request: { processBatchSize: number }, @@ -488,7 +474,14 @@ export class DefaultProcessingDatabase implements ProcessingDatabase { items.map(i => i.entity_ref), ) .update({ - next_update_at: this.getNextUpdateAt(tx), + next_update_at: + tx.client.config.client === 'sqlite3' + ? tx.raw(`datetime('now', ?)`, [ + `${this.options.refreshInterval()} seconds`, + ]) + : tx.raw( + `now() + interval '${this.options.refreshInterval()} seconds'`, + ), }); return { diff --git a/plugins/catalog-backend/src/next/index.ts b/plugins/catalog-backend/src/next/index.ts index 8c1921ae8b..a27174c415 100644 --- a/plugins/catalog-backend/src/next/index.ts +++ b/plugins/catalog-backend/src/next/index.ts @@ -18,3 +18,5 @@ export { NextCatalogBuilder } from './NextCatalogBuilder'; export { createNextRouter } from './NextRouter'; export * from './processing'; export * from './stitching'; +export type { RefreshIntervalFunction } from './refresh'; +export { createRandomRefreshInterval } from './refresh'; diff --git a/plugins/catalog-backend/src/next/refresh.ts b/plugins/catalog-backend/src/next/refresh.ts new file mode 100644 index 0000000000..d9f56c3254 --- /dev/null +++ b/plugins/catalog-backend/src/next/refresh.ts @@ -0,0 +1,36 @@ +/* + * 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. + */ + +/** + * Function that returns the catalog refresh interval in seconds. + */ +export type RefreshIntervalFunction = () => number; + +/** + * @param {number} options.minSeconds The minimum number of seconds between refreshes + * @param {number} options.maxSeconds The maximum number of seconds between refreshes + * @returns {RefreshIntervalFunction} that provides the next refresh interval + * + */ +export function createRandomRefreshInterval(options: { + minSeconds: number; + maxSeconds: number; +}): RefreshIntervalFunction { + const { minSeconds, maxSeconds } = options; + return () => { + return Math.random() * (maxSeconds - minSeconds) + minSeconds; + }; +} From 8fbf7b74522b880971574e90b1b7eaf0e95e13c9 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 16 Aug 2021 09:43:53 +0200 Subject: [PATCH 029/102] API reports Signed-off-by: Johan Haals --- packages/catalog-client/api-report.md | 53 +++++++++++++++------------ plugins/catalog-backend/api-report.md | 30 ++++++++++++++- 2 files changed, 57 insertions(+), 26 deletions(-) diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 173af9aef3..2e39dfd07f 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -25,11 +25,6 @@ export type AddLocationResponse = { entities: Entity[]; }; -// Warning: (ae-missing-release-tag) "CATALOG_FILTER_EXISTS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export const CATALOG_FILTER_EXISTS: unique symbol; - // Warning: (ae-missing-release-tag) "CatalogApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -76,50 +71,59 @@ export interface CatalogApi { ): Promise; } +// Warning: (ae-forgotten-export) The symbol "CatalogApi" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "CatalogClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class CatalogClient implements CatalogApi { - constructor(options: { discoveryApi: DiscoveryApi }); +export class CatalogClient implements CatalogApi_2 { + constructor(options: { discoveryApi: DiscoveryApi_2 }); + // Warning: (ae-forgotten-export) The symbol "AddLocationRequest" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "AddLocationResponse" needs to be exported by the entry point index.d.ts + // // (undocumented) addLocation( - { type, target, dryRun, presence }: AddLocationRequest, - options?: CatalogRequestOptions, - ): Promise; + { type, target, dryRun, presence }: AddLocationRequest_2, + options?: CatalogRequestOptions_2, + ): Promise; + // Warning: (ae-forgotten-export) The symbol "CatalogEntitiesRequest" needs to be exported by the entry point index.d.ts + // Warning: (ae-forgotten-export) The symbol "CatalogListResponse" needs to be exported by the entry point index.d.ts + // // (undocumented) getEntities( - request?: CatalogEntitiesRequest, - options?: CatalogRequestOptions, - ): Promise>; + request?: CatalogEntitiesRequest_2, + options?: CatalogRequestOptions_2, + ): Promise>; // (undocumented) getEntityByName( compoundName: EntityName, - options?: CatalogRequestOptions, + options?: CatalogRequestOptions_2, ): Promise; // (undocumented) getLocationByEntity( entity: Entity, - options?: CatalogRequestOptions, + options?: CatalogRequestOptions_2, ): Promise; + // Warning: (ae-forgotten-export) The symbol "CatalogRequestOptions" needs to be exported by the entry point index.d.ts + // // (undocumented) getLocationById( id: string, - options?: CatalogRequestOptions, + options?: CatalogRequestOptions_2, ): Promise; // (undocumented) getOriginLocationByEntity( entity: Entity, - options?: CatalogRequestOptions, + options?: CatalogRequestOptions_2, ): Promise; // (undocumented) removeEntityByUid( uid: string, - options?: CatalogRequestOptions, + options?: CatalogRequestOptions_2, ): Promise; // (undocumented) removeLocationById( id: string, - options?: CatalogRequestOptions, + options?: CatalogRequestOptions_2, ): Promise; } @@ -128,8 +132,8 @@ export class CatalogClient implements CatalogApi { // @public (undocumented) export type CatalogEntitiesRequest = { filter?: - | Record[] - | Record + | Record[] + | Record | undefined; fields?: string[] | undefined; }; @@ -148,11 +152,12 @@ export type CatalogRequestOptions = { token?: string; }; -// Warning: (ae-missing-release-tag) "ENTITY_STATUS_CATALOG_PROCESSING_TYPE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "DiscoveryApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = - 'backstage.io/catalog-processing'; +export type DiscoveryApi = { + getBaseUrl(pluginId: string): Promise; +}; // Warnings were encountered during analysis: // diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 7286855d4e..8f0d6c32e9 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -419,6 +419,24 @@ export function createNextRouter( options: RouterOptions_2, ): Promise; +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters +// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters +// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' +// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// Warning: (ae-missing-release-tag) "createRandomRefreshInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createRandomRefreshInterval(options: { + minSeconds: number; + maxSeconds: number; +}): RefreshIntervalFunction; + // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -914,6 +932,9 @@ export class NextCatalogBuilder { key: string, resolver: PlaceholderResolver, ): NextCatalogBuilder; + setRefreshInterval( + refreshInterval: RefreshIntervalFunction, + ): NextCatalogBuilder; setRefreshIntervalSeconds(seconds: number): NextCatalogBuilder; } @@ -987,6 +1008,11 @@ export type RecursivePartial = { : T[P]; }; +// Warning: (ae-missing-release-tag) "RefreshIntervalFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type RefreshIntervalFunction = () => number; + // Warning: (ae-missing-release-tag) "relation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1107,8 +1133,8 @@ export class UrlReaderProcessor implements CatalogProcessor { // src/ingestion/types.d.ts:41:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/ingestion/types.d.ts:49:5 - (ae-forgotten-export) The symbol "AnalyzeLocationExistingEntity" needs to be exported by the entry point index.d.ts // src/ingestion/types.d.ts:50:5 - (ae-forgotten-export) The symbol "AnalyzeLocationGenerateEntity" needs to be exported by the entry point index.d.ts -// src/next/NextCatalogBuilder.d.ts:140:9 - (ae-forgotten-export) The symbol "CatalogProcessingEngine" needs to be exported by the entry point index.d.ts -// src/next/NextCatalogBuilder.d.ts:141:9 - (ae-forgotten-export) The symbol "LocationService" needs to be exported by the entry point index.d.ts +// src/next/NextCatalogBuilder.d.ts:147:9 - (ae-forgotten-export) The symbol "CatalogProcessingEngine" needs to be exported by the entry point index.d.ts +// src/next/NextCatalogBuilder.d.ts:148:9 - (ae-forgotten-export) The symbol "LocationService" needs to be exported by the entry point index.d.ts // src/next/processing/types.d.ts:11:5 - (ae-forgotten-export) The symbol "DeferredEntity" needs to be exported by the entry point index.d.ts // (No @packageDocumentation comment for this package) From 2fc711c675dac56217c96239c6ac7d07d38a1d40 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Mon, 16 Aug 2021 09:58:57 +0200 Subject: [PATCH 030/102] cleanup bad imports Signed-off-by: Johan Haals --- .../src/next/database/DefaultProcessingDatabase.test.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts index 5b717ee36e..09647b980c 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts @@ -20,11 +20,8 @@ import { Entity, stringifyEntityRef } from '@backstage/catalog-model'; import { JsonObject } from '@backstage/config'; import { Knex } from 'knex'; import * as uuid from 'uuid'; -<<<<<<< HEAD import { Logger } from 'winston'; -======= import { DateTime } from 'luxon'; ->>>>>>> d31a82869 (Catalog: Add refresh spread functionality) import { DatabaseManager } from './DatabaseManager'; import { DefaultProcessingDatabase } from './DefaultProcessingDatabase'; import { From 9b8cec063177df147c9031945e713961ef4b63b8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Aug 2021 17:12:37 +0200 Subject: [PATCH 031/102] config-loader: add config file watching support Signed-off-by: Patrik Oldsberg --- .changeset/thin-bugs-compete.md | 5 ++ packages/config-loader/package.json | 1 + packages/config-loader/src/loader.test.ts | 83 ++++++++++++++++++++++- packages/config-loader/src/loader.ts | 64 +++++++++++++++-- 4 files changed, 146 insertions(+), 7 deletions(-) create mode 100644 .changeset/thin-bugs-compete.md diff --git a/.changeset/thin-bugs-compete.md b/.changeset/thin-bugs-compete.md new file mode 100644 index 0000000000..8b9805249c --- /dev/null +++ b/.changeset/thin-bugs-compete.md @@ -0,0 +1,5 @@ +--- +'@backstage/config-loader': patch +--- + +Add support for config file watching through a new group of `watch` options to `loadConfig`. diff --git a/packages/config-loader/package.json b/packages/config-loader/package.json index 33b8acc3b9..d4c737d2c8 100644 --- a/packages/config-loader/package.json +++ b/packages/config-loader/package.json @@ -34,6 +34,7 @@ "@backstage/config": "^0.1.6", "@types/json-schema": "^7.0.6", "ajv": "^7.0.3", + "chokidar": "^3.5.2", "fs-extra": "9.1.0", "json-schema": "^0.3.0", "json-schema-merge-allof": "^0.8.1", diff --git a/packages/config-loader/src/loader.test.ts b/packages/config-loader/src/loader.test.ts index b0e236f9c9..3f51e76936 100644 --- a/packages/config-loader/src/loader.test.ts +++ b/packages/config-loader/src/loader.test.ts @@ -14,11 +14,13 @@ * limitations under the License. */ +import { AppConfig } from '@backstage/config'; import { loadConfig } from './loader'; import mockFs from 'mock-fs'; +import fs from 'fs-extra'; describe('loadConfig', () => { - beforeAll(() => { + beforeEach(() => { process.env.MY_SECRET = 'is-secret'; process.env.SUBSTITUTE_ME = 'substituted'; @@ -63,7 +65,7 @@ describe('loadConfig', () => { }); }); - afterAll(() => { + afterEach(() => { mockFs.restore(); }); @@ -170,4 +172,81 @@ describe('loadConfig', () => { }, ]); }); + + it('watches config files', async () => { + const onChange = defer(); + const stopSignal = defer(); + + await expect( + loadConfig({ + configRoot: '/root', + configPaths: [], + watch: { + onChange: onChange.resolve, + stopSignal: stopSignal.promise, + }, + }), + ).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'Example App', + sessionKey: 'abc123', + escaped: '${Escaped}', + }, + }, + }, + ]); + + await fs.writeJson('/root/app-config.yaml', { + app: { + title: 'New Title', + }, + }); + await expect(onChange.promise).resolves.toEqual([ + { + context: 'app-config.yaml', + data: { + app: { + title: 'New Title', + }, + }, + }, + ]); + + stopSignal.resolve(); + }); + + it('stops watching config files', async () => { + const stopSignal = defer(); + + await loadConfig({ + configRoot: '/root', + configPaths: [], + watch: { + onChange: () => { + expect('not').toBe('called'); + }, + stopSignal: stopSignal.promise, + }, + }); + + stopSignal.resolve(); + + await fs.writeJson('/root/app-config.yaml', { + app: { + title: 'New Title', + }, + }); + await new Promise(resolve => setTimeout(resolve, 1000)); + }); + + function defer() { + let resolve: (value: T) => void; + const promise = new Promise(_resolve => { + resolve = _resolve; + }); + return { promise, resolve: resolve! }; + } }); diff --git a/packages/config-loader/src/loader.ts b/packages/config-loader/src/loader.ts index 3846d6474b..4dbfc007a7 100644 --- a/packages/config-loader/src/loader.ts +++ b/packages/config-loader/src/loader.ts @@ -16,6 +16,7 @@ import fs from 'fs-extra'; import yaml from 'yaml'; +import chokidar from 'chokidar'; import { resolve as resolvePath, dirname, isAbsolute, basename } from 'path'; import { AppConfig } from '@backstage/config'; import { @@ -42,13 +43,27 @@ export type LoadConfigOptions = { * @experimental This API is not stable and may change at any point */ experimentalEnvFunc?: EnvFunc; + + /** + * An optional configuration that enables watching of config files. + */ + watch?: { + /** + * A listener that is called when a config file is changed. + */ + onChange: (configs: AppConfig[]) => void; + + /** + * An optional signal that stops the watcher once the promise resolves. + */ + stopSignal?: Promise; + }; }; export async function loadConfig( options: LoadConfigOptions, ): Promise { - const configs = []; - const { configRoot, experimentalEnvFunc: envFunc } = options; + const { configRoot, experimentalEnvFunc: envFunc, watch } = options; const configPaths = options.configPaths.slice(); // If no paths are provided, we default to reading @@ -64,7 +79,9 @@ export async function loadConfig( const env = envFunc ?? (async (name: string) => process.env[name]); - try { + const loadConfigFiles = async () => { + const configs = []; + for (const configPath of configPaths) { if (!isAbsolute(configPath)) { throw new Error(`Config load path is not absolute: '${configPath}'`); @@ -83,13 +100,50 @@ export async function loadConfig( configs.push({ data, context: basename(configPath) }); } + + return configs; + }; + + let fileConfigs; + try { + fileConfigs = await loadConfigFiles(); } catch (error) { throw new Error( `Failed to read static configuration file, ${error.message}`, ); } - configs.push(...readEnvConfig(process.env)); + const envConfigs = await readEnvConfig(process.env); - return configs; + // Set up config file watching if requested by the caller + if (watch) { + let currentSerializedConfig = JSON.stringify(fileConfigs); + + const watcher = chokidar.watch(configPaths, { + usePolling: process.env.NODE_ENV === 'test', + }); + watcher.on('change', async () => { + try { + const newConfigs = await loadConfigFiles(); + const newSerializedConfig = JSON.stringify(newConfigs); + + if (currentSerializedConfig === newSerializedConfig) { + return; + } + currentSerializedConfig = newSerializedConfig; + + watch.onChange([...newConfigs, ...envConfigs]); + } catch (error) { + console.error(`Failed to reload configuration files, ${error}`); + } + }); + + if (watch.stopSignal) { + watch.stopSignal.then(() => { + watcher.close(); + }); + } + } + + return [...fileConfigs, ...envConfigs]; } From 9b4604b38a4ddf3e48f70d38626ddd921439b3db Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Aug 2021 17:21:25 +0200 Subject: [PATCH 032/102] backend-common: add ObservableConfig and config file watching Signed-off-by: Patrik Oldsberg --- .changeset/tricky-starfishes-cheer.md | 5 + packages/backend-common/src/config.ts | 128 ++++++++++++++++++++++++-- 2 files changed, 127 insertions(+), 6 deletions(-) create mode 100644 .changeset/tricky-starfishes-cheer.md diff --git a/.changeset/tricky-starfishes-cheer.md b/.changeset/tricky-starfishes-cheer.md new file mode 100644 index 0000000000..455c8f42eb --- /dev/null +++ b/.changeset/tricky-starfishes-cheer.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add support for watching configuration through a new `ObservableConfig` type that is now returned by `loadBackendConfig`. diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index 1316642ae1..d955aae71e 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -18,32 +18,148 @@ import { resolve as resolvePath } from 'path'; import parseArgs from 'minimist'; import { Logger } from 'winston'; import { findPaths } from '@backstage/cli-common'; -import { Config, ConfigReader } from '@backstage/config'; +import { Config, ConfigReader, JsonValue } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; +export interface ObservableConfig extends Config { + subscribe(onChange: () => void): { unsubscribe: () => void }; +} + +class ObservableConfigProxy implements ObservableConfig { + private config: Config = new ConfigReader({}); + + private readonly subscribers: (() => void)[] = []; + + constructor(private readonly logger: Logger) {} + + setConfig(config: Config) { + this.config = config; + for (const subscriber of this.subscribers) { + try { + subscriber(); + } catch (error) { + this.logger.error(`ObservableConfig subscriber threw error, ${error}`); + } + } + } + + subscribe(onChange: () => void): { unsubscribe: () => void } { + this.subscribers.push(onChange); + return { + unsubscribe: () => { + const index = this.subscribers.indexOf(onChange); + if (index >= 0) { + this.subscribers.splice(index, 1); + } + }, + }; + } + + has(key: string): boolean { + return this.config.has(key); + } + keys(): string[] { + return this.config.keys(); + } + get(key?: string): T { + return this.config.get(key); + } + getOptional(key?: string): T | undefined { + return this.config.getOptional(key); + } + getConfig(key: string): Config { + return this.config.getConfig(key); + } + getOptionalConfig(key: string): Config | undefined { + return this.config.getOptionalConfig(key); + } + getConfigArray(key: string): Config[] { + return this.config.getConfigArray(key); + } + getOptionalConfigArray(key: string): Config[] | undefined { + return this.config.getOptionalConfigArray(key); + } + getNumber(key: string): number { + return this.config.getNumber(key); + } + getOptionalNumber(key: string): number | undefined { + return this.config.getOptionalNumber(key); + } + getBoolean(key: string): boolean { + return this.config.getBoolean(key); + } + getOptionalBoolean(key: string): boolean | undefined { + return this.config.getOptionalBoolean(key); + } + getString(key: string): string { + return this.config.getString(key); + } + getOptionalString(key: string): string | undefined { + return this.config.getOptionalString(key); + } + getStringArray(key: string): string[] { + return this.config.getStringArray(key); + } + getOptionalStringArray(key: string): string[] | undefined { + return this.config.getOptionalStringArray(key); + } +} + type Options = { logger: Logger; // process.argv or any other overrides argv: string[]; }; +// A global used to ensure that only a single file watcher is active at a time. +let currentCancelFunc: () => void; + /** - * Load configuration for a Backend + * Load configuration for a Backend. + * + * This function should only be called once, during the initialization of the backend. */ -export async function loadBackendConfig(options: Options): Promise { +export async function loadBackendConfig( + options: Options, +): Promise { const args = parseArgs(options.argv); - const configOpts: string[] = [args.config ?? []].flat(); + const configPaths: string[] = [args.config ?? []].flat(); + + const config = new ObservableConfigProxy(options.logger); /* eslint-disable-next-line no-restricted-syntax */ const paths = findPaths(__dirname); + const configs = await loadConfig({ configRoot: paths.targetRoot, - configPaths: configOpts.map(opt => resolvePath(opt)), + configPaths: configPaths.map(opt => resolvePath(opt)), + watch: { + onChange(newConfigs) { + options.logger.info( + `Reloaded config from ${newConfigs.map(c => c.context).join(', ')}`, + ); + + config.setConfig(ConfigReader.fromConfigs(newConfigs)); + }, + stopSignal: new Promise(resolve => { + if (currentCancelFunc) { + currentCancelFunc(); + } + currentCancelFunc = resolve; + + // For reloads of this module we need to use a dispose handler rather than the global. + if (module.hot) { + module.hot.addDisposeHandler(resolve); + } + }), + }, }); options.logger.info( `Loaded config from ${configs.map(c => c.context).join(', ')}`, ); - return ConfigReader.fromConfigs(configs); + config.setConfig(ConfigReader.fromConfigs(configs)); + + return config; } From 03bb05af6d6a26611764adc7fadd39deb29fe6c9 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Aug 2021 17:35:43 +0200 Subject: [PATCH 033/102] catalog-backend: enable config location live reloads Signed-off-by: Patrik Oldsberg --- .changeset/great-lemons-mix.md | 5 ++ .../next/ConfigLocationEntityProvider.test.ts | 65 ++++++++++++++++++- .../src/next/ConfigLocationEntityProvider.ts | 37 ++++++++--- .../src/next/NextCatalogBuilder.ts | 3 +- 4 files changed, 98 insertions(+), 12 deletions(-) create mode 100644 .changeset/great-lemons-mix.md diff --git a/.changeset/great-lemons-mix.md b/.changeset/great-lemons-mix.md new file mode 100644 index 0000000000..2faeea4317 --- /dev/null +++ b/.changeset/great-lemons-mix.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Enabled live reload of locations configured in `catalog.locations`. diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts index 32ce28dc1b..e97de4eea4 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts @@ -14,7 +14,10 @@ * limitations under the License. */ -import { resolvePackagePath } from '@backstage/backend-common'; +import { + resolvePackagePath, + ObservableConfig, +} from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import path from 'path'; import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider'; @@ -70,4 +73,64 @@ describe('ConfigLocationEntityProvider', () => { ]), }); }); + + it('should be able to observe the config', async () => { + // Grab the subscriber function and use mutable config data to mock a config file change + let subscriber: () => void; + const mutableConfigData = { + catalog: { + locations: [{ type: 'url', target: 'https://github.com/a/a' }], + }, + }; + + const mockConfig: ObservableConfig = Object.assign( + new ConfigReader(mutableConfigData), + { + subscribe: (s: () => void) => { + subscriber = s; + return { unsubscribe: () => {} }; + }, + }, + ); + + const mockConnection = { + applyMutation: jest.fn(), + } as unknown as EntityProviderConnection; + const locationProvider = new ConfigLocationEntityProvider(mockConfig); + + await locationProvider.connect(mockConnection); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + spec: { + target: 'https://github.com/a/a', + type: 'url', + }, + }), + locationKey: 'url:https://github.com/a/a', + }, + ], + }); + + mutableConfigData.catalog.locations[0].target = 'https://github.com/b/b'; + subscriber!(); + + expect(mockConnection.applyMutation).toHaveBeenCalledWith({ + type: 'full', + entities: [ + { + entity: expect.objectContaining({ + spec: { + target: 'https://github.com/b/b', + type: 'url', + }, + }), + locationKey: 'url:https://github.com/b/b', + }, + ], + }); + }); }); diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts index 71ec84dae7..3c148e808d 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts @@ -15,27 +15,49 @@ */ import { Config } from '@backstage/config'; +import { ObservableConfig } from '@backstage/backend-common'; import path from 'path'; import { getEntityLocationRef } from './processing/util'; import { EntityProvider, EntityProviderConnection } from './types'; import { locationSpecToLocationEntity } from './util'; export class ConfigLocationEntityProvider implements EntityProvider { - private connection: EntityProviderConnection | undefined; - - constructor(private readonly config: Config) {} + constructor(private readonly config: Config | ObservableConfig) {} getProviderName(): string { return 'ConfigLocationProvider'; } async connect(connection: EntityProviderConnection): Promise { - this.connection = connection; + const entities = this.getEntitiesFromConfig(); + await connection.applyMutation({ + type: 'full', + entities, + }); + if ('subscribe' in this.config) { + let currentKey = JSON.stringify(entities); + + this.config.subscribe(() => { + const newEntities = this.getEntitiesFromConfig(); + const newKey = JSON.stringify(newEntities); + + if (currentKey !== newKey) { + currentKey = newKey; + connection.applyMutation({ + type: 'full', + entities: newEntities, + }); + } + }); + } + } + + private getEntitiesFromConfig() { const locationConfigs = this.config.getOptionalConfigArray('catalog.locations') ?? []; - const entities = locationConfigs.map(location => { + return locationConfigs.map(location => { const type = location.getString('type'); const target = location.getString('target'); const entity = locationSpecToLocationEntity({ @@ -45,10 +67,5 @@ export class ConfigLocationEntityProvider implements EntityProvider { const locationKey = getEntityLocationRef(entity); return { entity, locationKey }; }); - - await this.connection.applyMutation({ - type: 'full', - entities, - }); } } diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index fb12077508..8d796d442e 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -15,6 +15,7 @@ */ import { + ObservableConfig, PluginDatabaseManager, resolvePackagePath, UrlReader, @@ -79,7 +80,7 @@ import { Stitcher } from './stitching/Stitcher'; export type CatalogEnvironment = { logger: Logger; database: PluginDatabaseManager; - config: Config; + config: Config | ObservableConfig; reader: UrlReader; }; From b89f04d9f7247e3331e09066f318b643a160e99a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 8 Aug 2021 18:23:18 +0200 Subject: [PATCH 034/102] config-loader,backend-common,catalog-backend: update API reports Signed-off-by: Patrik Oldsberg --- packages/backend-common/api-report.md | 14 +++++++++++++- packages/config-loader/api-report.md | 4 ++++ plugins/catalog-backend/api-report.md | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 2652dad122..a97d50c10f 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -386,13 +386,25 @@ export { isChildPath }; // Warning: (ae-missing-release-tag) "loadBackendConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function loadBackendConfig(options: Options): Promise; +export function loadBackendConfig(options: Options): Promise; // Warning: (ae-missing-release-tag) "notFoundHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function notFoundHandler(): RequestHandler; +// Warning: (ae-missing-release-tag) "ObservableConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface ObservableConfig extends Config { + // (undocumented) + subscribe( + onChange: () => void, + ): { + unsubscribe: () => void; + }; +} + // Warning: (ae-missing-release-tag) "PluginCacheManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/packages/config-loader/api-report.md b/packages/config-loader/api-report.md index 47bc22c60f..38aa0b1ebb 100644 --- a/packages/config-loader/api-report.md +++ b/packages/config-loader/api-report.md @@ -37,6 +37,10 @@ export type LoadConfigOptions = { configPaths: string[]; env?: string; experimentalEnvFunc?: EnvFunc; + watch?: { + onChange: (configs: AppConfig[]) => void; + stopSignal?: Promise; + }; }; // Warning: (ae-forgotten-export) The symbol "Options" needs to be exported by the entry point index.d.ts diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 7286855d4e..688780039b 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -21,6 +21,7 @@ import { Knex } from 'knex'; import { Location as Location_2 } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; +import { ObservableConfig } from '@backstage/backend-common'; import { Organizations } from 'aws-sdk'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; From 90f25476ac68f20b2ac151ee5d167da0c3e11c08 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 12 Aug 2021 12:49:45 +0200 Subject: [PATCH 035/102] config,backend-common: move config subscribe method to the base config interface Signed-off-by: Patrik Oldsberg --- .changeset/modern-ladybugs-promise.md | 5 +++++ .changeset/tricky-starfishes-cheer.md | 2 +- packages/backend-common/api-report.md | 14 +------------- packages/backend-common/src/config.ts | 12 +++--------- packages/config/api-report.md | 3 +++ packages/config/src/types.ts | 11 +++++++++++ plugins/catalog-backend/api-report.md | 1 - .../next/ConfigLocationEntityProvider.test.ts | 18 ++++++------------ .../src/next/ConfigLocationEntityProvider.ts | 5 ++--- .../src/next/NextCatalogBuilder.ts | 3 +-- 10 files changed, 33 insertions(+), 41 deletions(-) create mode 100644 .changeset/modern-ladybugs-promise.md diff --git a/.changeset/modern-ladybugs-promise.md b/.changeset/modern-ladybugs-promise.md new file mode 100644 index 0000000000..8ad48d42f4 --- /dev/null +++ b/.changeset/modern-ladybugs-promise.md @@ -0,0 +1,5 @@ +--- +'@backstage/config': patch +--- + +Extended the `Config` interface to have an optional `subscribe` method that can be used be notified of updates to the configuration. diff --git a/.changeset/tricky-starfishes-cheer.md b/.changeset/tricky-starfishes-cheer.md index 455c8f42eb..bc2fb6a91b 100644 --- a/.changeset/tricky-starfishes-cheer.md +++ b/.changeset/tricky-starfishes-cheer.md @@ -2,4 +2,4 @@ '@backstage/backend-common': patch --- -Add support for watching configuration through a new `ObservableConfig` type that is now returned by `loadBackendConfig`. +Add support for watching configuration by implementing the `subscribe` method in the configuration returned by `loadBackendConfig`. diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index a97d50c10f..2652dad122 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -386,25 +386,13 @@ export { isChildPath }; // Warning: (ae-missing-release-tag) "loadBackendConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function loadBackendConfig(options: Options): Promise; +export function loadBackendConfig(options: Options): Promise; // Warning: (ae-missing-release-tag) "notFoundHandler" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public export function notFoundHandler(): RequestHandler; -// Warning: (ae-missing-release-tag) "ObservableConfig" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -export interface ObservableConfig extends Config { - // (undocumented) - subscribe( - onChange: () => void, - ): { - unsubscribe: () => void; - }; -} - // Warning: (ae-missing-release-tag) "PluginCacheManager" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/packages/backend-common/src/config.ts b/packages/backend-common/src/config.ts index d955aae71e..7c660ee4e6 100644 --- a/packages/backend-common/src/config.ts +++ b/packages/backend-common/src/config.ts @@ -21,11 +21,7 @@ import { findPaths } from '@backstage/cli-common'; import { Config, ConfigReader, JsonValue } from '@backstage/config'; import { loadConfig } from '@backstage/config-loader'; -export interface ObservableConfig extends Config { - subscribe(onChange: () => void): { unsubscribe: () => void }; -} - -class ObservableConfigProxy implements ObservableConfig { +class ObservableConfigProxy implements Config { private config: Config = new ConfigReader({}); private readonly subscribers: (() => void)[] = []; @@ -38,7 +34,7 @@ class ObservableConfigProxy implements ObservableConfig { try { subscriber(); } catch (error) { - this.logger.error(`ObservableConfig subscriber threw error, ${error}`); + this.logger.error(`Config subscriber threw error, ${error}`); } } } @@ -119,9 +115,7 @@ let currentCancelFunc: () => void; * * This function should only be called once, during the initialization of the backend. */ -export async function loadBackendConfig( - options: Options, -): Promise { +export async function loadBackendConfig(options: Options): Promise { const args = parseArgs(options.argv); const configPaths: string[] = [args.config ?? []].flat(); diff --git a/packages/config/api-report.md b/packages/config/api-report.md index eff4236026..91f37894c0 100644 --- a/packages/config/api-report.md +++ b/packages/config/api-report.md @@ -16,6 +16,9 @@ export type AppConfig = { // // @public (undocumented) export type Config = { + subscribe?(onChange: () => void): { + unsubscribe: () => void; + }; has(key: string): boolean; keys(): string[]; get(key?: string): T; diff --git a/packages/config/src/types.ts b/packages/config/src/types.ts index 1135e7d554..84de5dae70 100644 --- a/packages/config/src/types.ts +++ b/packages/config/src/types.ts @@ -26,6 +26,17 @@ export type AppConfig = { }; export type Config = { + /** + * Subscribes to the configuration object in order to receive a notification + * whenever any value within the configuration has changed. + * + * This method is optional to implement, and consumers need to check if it is + * implemented before invoking it. + */ + subscribe?(onChange: () => void): { + unsubscribe: () => void; + }; + has(key: string): boolean; keys(): string[]; diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 688780039b..7286855d4e 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -21,7 +21,6 @@ import { Knex } from 'knex'; import { Location as Location_2 } from '@backstage/catalog-model'; import { LocationSpec } from '@backstage/catalog-model'; import { Logger as Logger_2 } from 'winston'; -import { ObservableConfig } from '@backstage/backend-common'; import { Organizations } from 'aws-sdk'; import { PluginDatabaseManager } from '@backstage/backend-common'; import { PluginEndpointDiscovery } from '@backstage/backend-common'; diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts index e97de4eea4..ef3e47eb8b 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.test.ts @@ -14,10 +14,7 @@ * limitations under the License. */ -import { - resolvePackagePath, - ObservableConfig, -} from '@backstage/backend-common'; +import { resolvePackagePath } from '@backstage/backend-common'; import { ConfigReader } from '@backstage/config'; import path from 'path'; import { ConfigLocationEntityProvider } from './ConfigLocationEntityProvider'; @@ -83,15 +80,12 @@ describe('ConfigLocationEntityProvider', () => { }, }; - const mockConfig: ObservableConfig = Object.assign( - new ConfigReader(mutableConfigData), - { - subscribe: (s: () => void) => { - subscriber = s; - return { unsubscribe: () => {} }; - }, + const mockConfig = Object.assign(new ConfigReader(mutableConfigData), { + subscribe: (s: () => void) => { + subscriber = s; + return { unsubscribe: () => {} }; }, - ); + }); const mockConnection = { applyMutation: jest.fn(), diff --git a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts index 3c148e808d..5deca09944 100644 --- a/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts +++ b/plugins/catalog-backend/src/next/ConfigLocationEntityProvider.ts @@ -15,14 +15,13 @@ */ import { Config } from '@backstage/config'; -import { ObservableConfig } from '@backstage/backend-common'; import path from 'path'; import { getEntityLocationRef } from './processing/util'; import { EntityProvider, EntityProviderConnection } from './types'; import { locationSpecToLocationEntity } from './util'; export class ConfigLocationEntityProvider implements EntityProvider { - constructor(private readonly config: Config | ObservableConfig) {} + constructor(private readonly config: Config) {} getProviderName(): string { return 'ConfigLocationProvider'; @@ -35,7 +34,7 @@ export class ConfigLocationEntityProvider implements EntityProvider { entities, }); - if ('subscribe' in this.config) { + if (this.config.subscribe) { let currentKey = JSON.stringify(entities); this.config.subscribe(() => { diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index 8d796d442e..fb12077508 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -15,7 +15,6 @@ */ import { - ObservableConfig, PluginDatabaseManager, resolvePackagePath, UrlReader, @@ -80,7 +79,7 @@ import { Stitcher } from './stitching/Stitcher'; export type CatalogEnvironment = { logger: Logger; database: PluginDatabaseManager; - config: Config | ObservableConfig; + config: Config; reader: UrlReader; }; From 7f9676b242eeba6cd521c5b13139130b8746914e Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 16 Aug 2021 11:39:39 +0200 Subject: [PATCH 036/102] Remove added color from theme types to prevent version bump Signed-off-by: Philipp Hugenroth --- .../core-components/src/layout/Sidebar/Bar.tsx | 2 +- .../core-components/src/layout/Sidebar/Items.tsx | 16 +++++++--------- packages/theme/src/themes.ts | 2 -- packages/theme/src/types.ts | 1 - 4 files changed, 8 insertions(+), 13 deletions(-) diff --git a/packages/core-components/src/layout/Sidebar/Bar.tsx b/packages/core-components/src/layout/Sidebar/Bar.tsx index 05bb751cc8..f928c4b7c4 100644 --- a/packages/core-components/src/layout/Sidebar/Bar.tsx +++ b/packages/core-components/src/layout/Sidebar/Bar.tsx @@ -42,7 +42,7 @@ const useStyles = makeStyles(theme => ({ msOverflowStyle: 'none', scrollbarWidth: 'none', width: sidebarConfig.drawerWidthClosed, - borderRight: `1px solid ${theme.palette.navigation.divider}`, + borderRight: `1px solid #383838`, transition: theme.transitions.create('width', { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.shortest, diff --git a/packages/core-components/src/layout/Sidebar/Items.tsx b/packages/core-components/src/layout/Sidebar/Items.tsx index 00771c837b..1068a877bd 100644 --- a/packages/core-components/src/layout/Sidebar/Items.tsx +++ b/packages/core-components/src/layout/Sidebar/Items.tsx @@ -344,15 +344,13 @@ export const SidebarSpacer = styled('div')({ height: 8, }); -export const SidebarDivider = styled('hr')( - ({ theme }: { theme: BackstageTheme }) => ({ - height: 1, - width: '100%', - background: theme.palette.navigation.divider, - border: 'none', - margin: '12px 0px', - }), -); +export const SidebarDivider = styled('hr')({ + height: 1, + width: '100%', + background: '#383838', + border: 'none', + margin: '12px 0px', +}); const styledScrollbar = (theme: Theme): CreateCSSProperties => ({ overflowY: 'auto', diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index 03a8fad2a2..b31786aae9 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -66,7 +66,6 @@ export const lightTheme = createTheme({ background: '#171717', indicator: '#9BF0E1', color: '#b5b5b5', - divider: '#383838', selectedColor: '#FFF', }, pinSidebarButton: { @@ -133,7 +132,6 @@ export const darkTheme = createTheme({ background: '#424242', indicator: '#9BF0E1', color: '#b5b5b5', - divider: '#383838', selectedColor: '#FFF', }, pinSidebarButton: { diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index 2c4d198a4f..c2758c29e7 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -47,7 +47,6 @@ type PaletteAdditions = { background: string; indicator: string; color: string; - divider: string; selectedColor: string; }; tabbar: { From 65344071e464131e1f6a2aa00b8cebd866fc9515 Mon Sep 17 00:00:00 2001 From: Philipp Hugenroth Date: Mon, 16 Aug 2021 12:03:51 +0200 Subject: [PATCH 037/102] Adjust api report & changeset Signed-off-by: Philipp Hugenroth --- .changeset/tender-dingos-burn.md | 1 - packages/core-components/api-report.md | 8 -------- 2 files changed, 9 deletions(-) diff --git a/.changeset/tender-dingos-burn.md b/.changeset/tender-dingos-burn.md index fa52bfcead..0801d9d971 100644 --- a/.changeset/tender-dingos-burn.md +++ b/.changeset/tender-dingos-burn.md @@ -1,6 +1,5 @@ --- '@backstage/core-components': patch -'@backstage/theme': patch --- Change the default hover experience for the sidebar to be not jumpy & add visual separation between sidebar & Entity Page tabs for dark mode. diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 14ed06d37c..42404fb0c0 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -7,7 +7,6 @@ import { ApiRef } from '@backstage/core-plugin-api'; import { BackstageIdentityApi } from '@backstage/core-plugin-api'; -import { BackstageTheme } from '@backstage/theme'; import { Breadcrumbs as Breadcrumbs_2 } from '@material-ui/core'; import { ButtonProps } from '@material-ui/core'; import { ButtonTypeMap } from '@material-ui/core'; @@ -1605,13 +1604,6 @@ export const SidebarDivider: React_2.ComponentType< > & StyledComponentProps<'root'> & { className?: string | undefined; - } & Pick< - { - theme: BackstageTheme; - }, - never - > & { - theme?: BackstageTheme | undefined; } >; From b88e270b7f8a51f2708157e06ff1a4835e3b595f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mert=20Can=20Bilgi=C3=A7?= Date: Mon, 16 Aug 2021 14:37:41 +0300 Subject: [PATCH 038/102] adopter contact information has changed for Trendyol MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Mert Can Bilgiç --- ADOPTERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ADOPTERS.md b/ADOPTERS.md index 72b6cce072..67b519c375 100644 --- a/ADOPTERS.md +++ b/ADOPTERS.md @@ -21,7 +21,7 @@ | [Paddle.com](https://paddle.com) | [Ioannis Georgoulas](https://github.com/geototti21) | Developer portal (Tech Docs, Service Catalog, Internal Tooling), we use vanilla Backstage FE and custom BE implementation in Go | | [Acast.com](https://acast.com) | [Olle Lundberg](https://github.com/lndbrg) | Developer portal with tech docs, service catalog and a bunch of other internal tooling | | [Lunar](https://lunar.app) | [Jacob Valdemar](https://github.com/JacobValdemar) | Internal developer portal for service overview and insights, API documentation, technical guides, onboarding guides and RFC's. | -| [Trendyol](https://trendyol.com) | [Erdogan Oksuz](https://github.com/erdoganoksuz) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | +| [Trendyol](https://trendyol.com) | [Gamze Senturk](https://github.com/gmzsenturk), [Mert Can Bilgic](https://github.com/mertcb) | The Developer Portal has been called `Pandora`. Provides an overview of Trendyol tech ecosystem. TechDocs, Catalog, Custom Plugins and Theme. | | [Peloton](https://www.onepeloton.com/) | [Jim Haughwout](https://github.com/JimHaughwout) | Creating our first developer portal and tech-docs. Exploring Service Catalog, Tech Insights and Cost Insights as well. | | [TELUS](https://telus.com) | [Seb Barre](https://github.com/sbarre) | The Go-to place to find answers about development and delivery at TELUS. | | [Brex](https://www.brex.com/) | [Vamsi Chitters](https://github.com/vamsikc) | A centralized UI to understand how a service fits in the whole Brex architecture and manage a team’s engineering dependencies. | From efca1a4ab1e305f41c9783ea6e7546649d0433a5 Mon Sep 17 00:00:00 2001 From: Aaron Nickovich Date: Mon, 16 Aug 2021 11:59:54 -0700 Subject: [PATCH 039/102] Update writing custom actions documentation Signed-off-by: Aaron Nickovich --- docs/features/software-templates/writing-custom-actions.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 53a90e2954..4131af59c8 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -132,6 +132,8 @@ want to have those as well as your new one, you'll need to do the following: ```ts import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend'; +const integrations = ScmIntegrations.fromConfig(config); + const builtInActions = createBuiltinActions({ containerRunner, integrations, From 757a058aed7e0760f7308653e94c5601251c6a13 Mon Sep 17 00:00:00 2001 From: aaronnickovich Date: Mon, 16 Aug 2021 16:09:09 -0700 Subject: [PATCH 040/102] Update writing-custom-actions.md Adding missing import Signed-off-by: Aaron Nickovich --- docs/features/software-templates/writing-custom-actions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/features/software-templates/writing-custom-actions.md b/docs/features/software-templates/writing-custom-actions.md index 4131af59c8..a5773a6aff 100644 --- a/docs/features/software-templates/writing-custom-actions.md +++ b/docs/features/software-templates/writing-custom-actions.md @@ -131,6 +131,7 @@ want to have those as well as your new one, you'll need to do the following: ```ts import { createBuiltinActions } from '@backstage/plugin-scaffolder-backend'; +import { ScmIntegrations } from '@backstage/integration'; const integrations = ScmIntegrations.fromConfig(config); From 360a89830b09d57345d5863acf68b7cf5c16b939 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Aug 2021 04:06:33 +0000 Subject: [PATCH 041/102] chore(deps-dev): bump @types/d3-shape from 3.0.1 to 3.0.2 Bumps [@types/d3-shape](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/d3-shape) from 3.0.1 to 3.0.2. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/d3-shape) --- updated-dependencies: - dependency-name: "@types/d3-shape" dependency-type: direct:development update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1b34c094fb..99eed1cd40 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6555,9 +6555,9 @@ "@types/d3-path" "^1" "@types/d3-shape@^3.0.1": - version "3.0.1" - resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.1.tgz#8cebf5f3d56dd81674e1822279db58a38848a250" - integrity sha512-HnpwE2zl45cOaXLLo0zR4OfRQM9u9sI/ESb0w41PrrsQfkibj8pVIS8VYMSem2Wf5RuWxcgy3/8Kw2/FcIqAEQ== + version "3.0.2" + resolved "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.0.2.tgz#4b1ca4ddaac294e76b712429726d40365cd1e8ca" + integrity sha512-5+ButCmIfNX8id5seZ7jKj3igdcxx+S9IDBiT35fQGTLZUfkFgTv+oBH34xgeoWDKpWcMITSzBILWQtBoN5Piw== dependencies: "@types/d3-path" "*" From 928c9ba27bf24fa852821e166d0c9434b08cb308 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 17 Aug 2021 09:28:36 +0200 Subject: [PATCH 042/102] API reports Signed-off-by: Johan Haals --- packages/catalog-client/api-report.md | 53 ++++++++++++--------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/packages/catalog-client/api-report.md b/packages/catalog-client/api-report.md index 2e39dfd07f..173af9aef3 100644 --- a/packages/catalog-client/api-report.md +++ b/packages/catalog-client/api-report.md @@ -25,6 +25,11 @@ export type AddLocationResponse = { entities: Entity[]; }; +// Warning: (ae-missing-release-tag) "CATALOG_FILTER_EXISTS" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const CATALOG_FILTER_EXISTS: unique symbol; + // Warning: (ae-missing-release-tag) "CatalogApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -71,59 +76,50 @@ export interface CatalogApi { ): Promise; } -// Warning: (ae-forgotten-export) The symbol "CatalogApi" needs to be exported by the entry point index.d.ts // Warning: (ae-missing-release-tag) "CatalogClient" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export class CatalogClient implements CatalogApi_2 { - constructor(options: { discoveryApi: DiscoveryApi_2 }); - // Warning: (ae-forgotten-export) The symbol "AddLocationRequest" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "AddLocationResponse" needs to be exported by the entry point index.d.ts - // +export class CatalogClient implements CatalogApi { + constructor(options: { discoveryApi: DiscoveryApi }); // (undocumented) addLocation( - { type, target, dryRun, presence }: AddLocationRequest_2, - options?: CatalogRequestOptions_2, - ): Promise; - // Warning: (ae-forgotten-export) The symbol "CatalogEntitiesRequest" needs to be exported by the entry point index.d.ts - // Warning: (ae-forgotten-export) The symbol "CatalogListResponse" needs to be exported by the entry point index.d.ts - // + { type, target, dryRun, presence }: AddLocationRequest, + options?: CatalogRequestOptions, + ): Promise; // (undocumented) getEntities( - request?: CatalogEntitiesRequest_2, - options?: CatalogRequestOptions_2, - ): Promise>; + request?: CatalogEntitiesRequest, + options?: CatalogRequestOptions, + ): Promise>; // (undocumented) getEntityByName( compoundName: EntityName, - options?: CatalogRequestOptions_2, + options?: CatalogRequestOptions, ): Promise; // (undocumented) getLocationByEntity( entity: Entity, - options?: CatalogRequestOptions_2, + options?: CatalogRequestOptions, ): Promise; - // Warning: (ae-forgotten-export) The symbol "CatalogRequestOptions" needs to be exported by the entry point index.d.ts - // // (undocumented) getLocationById( id: string, - options?: CatalogRequestOptions_2, + options?: CatalogRequestOptions, ): Promise; // (undocumented) getOriginLocationByEntity( entity: Entity, - options?: CatalogRequestOptions_2, + options?: CatalogRequestOptions, ): Promise; // (undocumented) removeEntityByUid( uid: string, - options?: CatalogRequestOptions_2, + options?: CatalogRequestOptions, ): Promise; // (undocumented) removeLocationById( id: string, - options?: CatalogRequestOptions_2, + options?: CatalogRequestOptions, ): Promise; } @@ -132,8 +128,8 @@ export class CatalogClient implements CatalogApi_2 { // @public (undocumented) export type CatalogEntitiesRequest = { filter?: - | Record[] - | Record + | Record[] + | Record | undefined; fields?: string[] | undefined; }; @@ -152,12 +148,11 @@ export type CatalogRequestOptions = { token?: string; }; -// Warning: (ae-missing-release-tag) "DiscoveryApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "ENTITY_STATUS_CATALOG_PROCESSING_TYPE" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type DiscoveryApi = { - getBaseUrl(pluginId: string): Promise; -}; +export const ENTITY_STATUS_CATALOG_PROCESSING_TYPE = + 'backstage.io/catalog-processing'; // Warnings were encountered during analysis: // From f3bba3d2bd9b5cc6e75c9fcba056fc9acfc77d37 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 17 Aug 2021 09:32:25 +0200 Subject: [PATCH 043/102] cli: Remove useless debug logging Signed-off-by: Johan Haals --- .changeset/cuddly-insects-hunt.md | 5 +++++ packages/cli/src/lib/bundler/backend.ts | 1 - 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 .changeset/cuddly-insects-hunt.md diff --git a/.changeset/cuddly-insects-hunt.md b/.changeset/cuddly-insects-hunt.md new file mode 100644 index 0000000000..63e65e5b55 --- /dev/null +++ b/.changeset/cuddly-insects-hunt.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Remove debug logging diff --git a/packages/cli/src/lib/bundler/backend.ts b/packages/cli/src/lib/bundler/backend.ts index 567fc3476a..bd4ab42098 100644 --- a/packages/cli/src/lib/bundler/backend.ts +++ b/packages/cli/src/lib/bundler/backend.ts @@ -28,7 +28,6 @@ export async function serveBackend(options: BackendServeOptions) { const compiler = webpack(config, (err: Error | undefined) => { if (err) { - console.log('here'); console.error(err); } else console.log('Build succeeded'); }); From 5132ae873d1f5c5b57b1016d57236645a5cd2db7 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 17 Aug 2021 11:45:03 +0200 Subject: [PATCH 044/102] fix test Signed-off-by: Samira Mokaram --- plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts index 0b35ea7bc8..27b629cc07 100644 --- a/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts +++ b/plugins/auth-backend/src/lib/oauth/OAuthAdapter.test.ts @@ -64,6 +64,7 @@ describe('OAuthAdapter', () => { issueToken: async () => 'my-id-token', listPublicKeys: async () => ({ keys: [] }), }, + isOriginAllowed: () => false, }; it('sets the correct headers in start', async () => { @@ -105,6 +106,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, + isOriginAllowed: () => false, }); const state = { nonce: 'nonce', env: 'development' }; @@ -139,6 +141,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, + isOriginAllowed: () => false, }); const mockRequest = { @@ -164,6 +167,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, + isOriginAllowed: () => false, }); const mockRequest = { @@ -190,6 +194,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: false, + isOriginAllowed: () => false, }); const mockRequest = { @@ -220,6 +225,7 @@ describe('OAuthAdapter', () => { const oauthProvider = new OAuthAdapter(providerInstance, { ...oAuthProviderOptions, disableRefresh: true, + isOriginAllowed: () => false, }); const mockRequest = { From 5ab35b84ec92ef40930277de73110b2fc0d1ec87 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 17 Aug 2021 12:00:05 +0200 Subject: [PATCH 045/102] fix check api report Signed-off-by: Samira Mokaram --- plugins/auth-backend/api-report.md | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/auth-backend/api-report.md b/plugins/auth-backend/api-report.md index af4375f5d7..11396cdc7c 100644 --- a/plugins/auth-backend/api-report.md +++ b/plugins/auth-backend/api-report.md @@ -105,6 +105,11 @@ export const createOktaProvider: ( _options?: OktaProviderOptions | undefined, ) => AuthProviderFactory; +// Warning: (ae-missing-release-tag) "createOriginFilter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createOriginFilter(config: Config): (origin: string) => boolean; + // Warning: (ae-missing-release-tag) "createRouter" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -308,6 +313,7 @@ export type OAuthStartRequest = express.Request<{}> & { export type OAuthState = { nonce: string; env: string; + origin?: string; }; // Warning: (ae-missing-release-tag) "oktaEmailSignInResolver" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -399,9 +405,9 @@ export type WebMessageResponse = // src/identity/types.d.ts:25:5 - (ae-forgotten-export) The symbol "TokenParams" needs to be exported by the entry point index.d.ts // src/identity/types.d.ts:31:9 - (ae-forgotten-export) The symbol "AnyJWK" needs to be exported by the entry point index.d.ts // src/providers/google/provider.d.ts:36:5 - (ae-forgotten-export) The symbol "AuthHandler" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:105:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:111:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts -// src/providers/types.d.ts:128:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative +// src/providers/types.d.ts:109:5 - (ae-forgotten-export) The symbol "AuthProviderConfig" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:115:5 - (ae-forgotten-export) The symbol "ExperimentalIdentityResolver" needs to be exported by the entry point index.d.ts +// src/providers/types.d.ts:132:8 - (tsdoc-missing-deprecation-message) The @deprecated block must include a deprecation message, e.g. describing the recommended alternative // (No @packageDocumentation comment for this package) ``` From 4f685aac962f2ad2db65adb15125c2093775056d Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 17 Aug 2021 13:17:55 +0200 Subject: [PATCH 046/102] Update comments Signed-off-by: Johan Haals --- plugins/catalog-backend/api-report.md | 10 +--------- plugins/catalog-backend/src/next/NextCatalogBuilder.ts | 4 ++-- plugins/catalog-backend/src/next/refresh.ts | 4 +--- 3 files changed, 4 insertions(+), 14 deletions(-) diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index 8f0d6c32e9..a6e67215c9 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -419,19 +419,11 @@ export function createNextRouter( options: RouterOptions_2, ): Promise; -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' -// Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// Warning: (tsdoc-param-tag-with-invalid-name) The @param block should be followed by a valid parameter name: The identifier cannot non-word characters -// Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // Warning: (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // Warning: (ae-missing-release-tag) "createRandomRefreshInterval" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public (undocumented) +// @public export function createRandomRefreshInterval(options: { minSeconds: number; maxSeconds: number; diff --git a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts index b277f0b16a..675e170727 100644 --- a/plugins/catalog-backend/src/next/NextCatalogBuilder.ts +++ b/plugins/catalog-backend/src/next/NextCatalogBuilder.ts @@ -165,8 +165,8 @@ export class NextCatalogBuilder { } /** - * Refresh spread configures configures the minimum and maximum number of seconds - * to wait between refreshes in addition to the configured refresh interval. + * Overwrites the default refresh interval function used to spread + * entity updates in the catalog. */ setRefreshInterval( refreshInterval: RefreshIntervalFunction, diff --git a/plugins/catalog-backend/src/next/refresh.ts b/plugins/catalog-backend/src/next/refresh.ts index d9f56c3254..aae992f391 100644 --- a/plugins/catalog-backend/src/next/refresh.ts +++ b/plugins/catalog-backend/src/next/refresh.ts @@ -20,10 +20,8 @@ export type RefreshIntervalFunction = () => number; /** - * @param {number} options.minSeconds The minimum number of seconds between refreshes - * @param {number} options.maxSeconds The maximum number of seconds between refreshes + * Creates a function that returns a random refresh interval between minSeconds and maxSeconds. * @returns {RefreshIntervalFunction} that provides the next refresh interval - * */ export function createRandomRefreshInterval(options: { minSeconds: number; From d155fedfc4e9ac8e38d792b3161c714fbc282f21 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 17 Aug 2021 13:46:37 +0200 Subject: [PATCH 047/102] Floor random number Signed-off-by: Johan Haals --- plugins/catalog-backend/src/next/refresh.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/next/refresh.ts b/plugins/catalog-backend/src/next/refresh.ts index aae992f391..caec7e9548 100644 --- a/plugins/catalog-backend/src/next/refresh.ts +++ b/plugins/catalog-backend/src/next/refresh.ts @@ -29,6 +29,6 @@ export function createRandomRefreshInterval(options: { }): RefreshIntervalFunction { const { minSeconds, maxSeconds } = options; return () => { - return Math.random() * (maxSeconds - minSeconds) + minSeconds; + return Math.floor(Math.random() * (maxSeconds - minSeconds) + minSeconds); }; } From 6fb2d041b7ffff86ba6900b8266f1a03356e60f4 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 17 Aug 2021 13:48:14 +0200 Subject: [PATCH 048/102] fix test Signed-off-by: Samira Mokaram --- .../src/lib/AuthConnector/DefaultAuthConnector.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 ce5df9fc91..28125f6fa8 100644 --- a/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-app-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -126,7 +126,7 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toBeCalledTimes(1); expect(popupSpy.mock.calls[0][0]).toMatchObject({ - url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&env=production', + url: 'http://my-host/api/auth/my-provider/start?scope=a%20b&origin=http%3A%2F%2Flocalhost&env=production', }); await expect(sessionPromise).resolves.toEqual({ @@ -174,7 +174,7 @@ describe('DefaultAuthConnector', () => { expect(popupSpy).toBeCalledTimes(1); expect(popupSpy.mock.calls[0][0]).toMatchObject({ - url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&env=production', + url: 'http://my-host/api/auth/my-provider/start?scope=-ab-&origin=http%3A%2F%2Flocalhost&env=production', }); }); }); From 3ecd59147541c185f0820b99e712cd085b439b48 Mon Sep 17 00:00:00 2001 From: Djamaile Rahamat Date: Fri, 18 Jun 2021 15:24:17 +0200 Subject: [PATCH 049/102] fix: ignore images and files that are bigger than/equal 200k Signed-off-by: Djamaile Rahamat --- .../src/reading/tree/TarArchiveResponse.ts | 5 +++++ .../src/reading/tree/ZipArchiveResponse.ts | 5 +++++ .../src/lib/TodoReader/TodoScmReader.ts | 20 +++++++++++++++++-- 3 files changed, 28 insertions(+), 2 deletions(-) diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index 173f6a886a..adcf420fc0 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -98,6 +98,11 @@ export class TarArchiveResponse implements ReadTreeResponse { } } + if (entry.size && entry.size >= 20000) { + entry.resume(); + return; + } + const content = new Promise(async resolve => { await pipeline(entry, concatStream(resolve)); }); diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 45c6880a55..62219ec27d 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -68,6 +68,11 @@ export class ZipArchiveResponse implements ReadTreeResponse { private shouldBeIncluded(entry: Entry): boolean { const strippedPath = stripFirstDirectoryFromPath(entry.path); + const size = entry.vars.compressedSize; + + if (size >= 20000) { + return false; + } if (this.subPath) { if (!strippedPath.startsWith(this.subPath)) { diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 256279596f..1e9cb68bd9 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -27,6 +27,7 @@ import { } from './types'; import { Config } from '@backstage/config'; import { createTodoParser } from './createTodoParser'; +import path from 'path'; type Options = { logger: Logger; @@ -80,10 +81,25 @@ export class TodoScmReader implements TodoReader { { url }: ReadTodosOptions, etag?: string, ): Promise { + const shouldNotInclude = [ + '.png', + '.svg', + '.jpg', + '.jpeg', + '.gif', + '.raw', + '.lock', + '.ico', + ]; const tree = await this.reader.readTree(url, { etag, - filter(path) { - return !path.startsWith('.') && !path.includes('/.'); + filter(filePath) { + const extname = path.extname(filePath); + return ( + !filePath.startsWith('.') && + !filePath.includes('/.') && + !shouldNotInclude.includes(extname) + ); }, }); From 0b34dc7a328fea9c00cc1d18d4ceea2a29ed9a19 Mon Sep 17 00:00:00 2001 From: Djamaile Rahamat Date: Fri, 18 Jun 2021 15:29:13 +0200 Subject: [PATCH 050/102] chore: add changeset Signed-off-by: Djamaile Rahamat --- .changeset/mean-spiders-run.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/mean-spiders-run.md diff --git a/.changeset/mean-spiders-run.md b/.changeset/mean-spiders-run.md new file mode 100644 index 0000000000..10a1208971 --- /dev/null +++ b/.changeset/mean-spiders-run.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-todo-backend': minor +--- + +images will be ignored and files bigger than 200Kb will also be ignored so that the todo plugin doesn't stay hanging From 62eb025cab2486e70f2c4f8426caa2533ef3d40a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Aug 2021 13:30:43 +0200 Subject: [PATCH 051/102] changesets: tweak todo-backend changeset Signed-off-by: Patrik Oldsberg --- .changeset/mean-spiders-run.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/mean-spiders-run.md b/.changeset/mean-spiders-run.md index 10a1208971..b9d8e5efc4 100644 --- a/.changeset/mean-spiders-run.md +++ b/.changeset/mean-spiders-run.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-todo-backend': minor +'@backstage/plugin-todo-backend': patch --- -images will be ignored and files bigger than 200Kb will also be ignored so that the todo plugin doesn't stay hanging +Ignore images and files that are larger than 200KB. From 7b438e6f3579872cb316ccc3aade5e3dff6149b4 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Aug 2021 13:31:52 +0200 Subject: [PATCH 052/102] backend-common: remove hardcoded size check and replace with filter param Signed-off-by: Patrik Oldsberg --- .../src/reading/tree/TarArchiveResponse.ts | 13 ++++--------- .../src/reading/tree/ZipArchiveResponse.ts | 13 ++++++------- packages/backend-common/src/reading/types.ts | 4 ++-- 3 files changed, 12 insertions(+), 18 deletions(-) diff --git a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts index adcf420fc0..f7ab7f5349 100644 --- a/packages/backend-common/src/reading/tree/TarArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/TarArchiveResponse.ts @@ -43,7 +43,7 @@ export class TarArchiveResponse implements ReadTreeResponse { private readonly subPath: string, private readonly workDir: string, public readonly etag: string, - private readonly filter?: (path: string) => boolean, + private readonly filter?: (path: string, info: { size: number }) => boolean, ) { if (subPath) { if (!subPath.endsWith('/')) { @@ -92,17 +92,12 @@ export class TarArchiveResponse implements ReadTreeResponse { const path = relativePath.slice(this.subPath.length); if (this.filter) { - if (!this.filter(path)) { + if (!this.filter(path, { size: entry.remain })) { entry.resume(); return; } } - if (entry.size && entry.size >= 20000) { - entry.resume(); - return; - } - const content = new Promise(async resolve => { await pipeline(entry, concatStream(resolve)); }); @@ -160,7 +155,7 @@ export class TarArchiveResponse implements ReadTreeResponse { tar.extract({ strip, cwd: dir, - filter: path => { + filter: (path, stat) => { // File path relative to the root extracted directory. Will remove the // top level dir name from the path since its name is hard to predetermine. const relativePath = stripFirstDirectoryFromPath(path); @@ -169,7 +164,7 @@ export class TarArchiveResponse implements ReadTreeResponse { } if (this.filter) { const innerPath = path.split('/').slice(strip).join('/'); - return this.filter(innerPath); + return this.filter(innerPath, { size: stat.size }); } return true; }, diff --git a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts index 62219ec27d..5c12a383ee 100644 --- a/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts +++ b/packages/backend-common/src/reading/tree/ZipArchiveResponse.ts @@ -37,7 +37,7 @@ export class ZipArchiveResponse implements ReadTreeResponse { private readonly subPath: string, private readonly workDir: string, public readonly etag: string, - private readonly filter?: (path: string) => boolean, + private readonly filter?: (path: string, info: { size: number }) => boolean, ) { if (subPath) { if (!subPath.endsWith('/')) { @@ -68,11 +68,6 @@ export class ZipArchiveResponse implements ReadTreeResponse { private shouldBeIncluded(entry: Entry): boolean { const strippedPath = stripFirstDirectoryFromPath(entry.path); - const size = entry.vars.compressedSize; - - if (size >= 20000) { - return false; - } if (this.subPath) { if (!strippedPath.startsWith(this.subPath)) { @@ -80,7 +75,11 @@ export class ZipArchiveResponse implements ReadTreeResponse { } } if (this.filter) { - return this.filter(this.getInnerPath(entry.path)); + return this.filter(this.getInnerPath(entry.path), { + size: + (entry.vars as { uncompressedSize?: number }).uncompressedSize ?? + entry.vars.compressedSize, + }); } return true; } diff --git a/packages/backend-common/src/reading/types.ts b/packages/backend-common/src/reading/types.ts index 8efc833ead..93f287d4fe 100644 --- a/packages/backend-common/src/reading/types.ts +++ b/packages/backend-common/src/reading/types.ts @@ -104,7 +104,7 @@ export type ReadTreeOptions = { * * If no filter is provided all files are extracted. */ - filter?(path: string): boolean; + filter?(path: string, info?: { size: number }): boolean; /** * An etag can be provided to check whether readTree's response has changed from a previous execution. @@ -164,7 +164,7 @@ export type FromArchiveOptions = { // etag of the blob etag: string; // Filter passed on from the ReadTreeOptions - filter?: (path: string) => boolean; + filter?: (path: string, info?: { size: number }) => boolean; }; export interface ReadTreeResponseFactory { From 89966c0c37b01e2fe08f95bea2cf818a84d766f5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Aug 2021 13:32:39 +0200 Subject: [PATCH 053/102] todo-backend: filter out large or binary files + deduplicate in-flight requests Signed-off-by: Patrik Oldsberg --- .../src/lib/TodoReader/TodoScmReader.ts | 38 ++++++++++++------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 1e9cb68bd9..2bd0f0b038 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -29,6 +29,18 @@ import { Config } from '@backstage/config'; import { createTodoParser } from './createTodoParser'; import path from 'path'; +const excludedExtensions = [ + '.png', + '.svg', + '.jpg', + '.jpeg', + '.gif', + '.raw', + '.lock', + '.ico', +]; +const MAX_FILE_SIZE = 200000; + type Options = { logger: Logger; reader: UrlReader; @@ -48,6 +60,7 @@ export class TodoScmReader implements TodoReader { private readonly integrations: ScmIntegrations; private readonly cache = new Map(); + private readonly inFlightReads = new Map>(); static fromConfig(config: Config, options: Omit) { return new TodoScmReader({ @@ -66,7 +79,13 @@ export class TodoScmReader implements TodoReader { async readTodos({ url }: ReadTodosOptions): Promise { const cacheItem = this.cache.get(url); try { - const newCacheItem = await this.doReadTodos({ url }, cacheItem?.etag); + const inFlightRead = this.inFlightReads.get(url); + if (inFlightRead) { + return (await inFlightRead).result; + } + const newRead = this.doReadTodos({ url }, cacheItem?.etag); + this.inFlightReads.set(url, newRead); + const newCacheItem = await newRead; this.cache.set(url, newCacheItem); return newCacheItem.result; } catch (error) { @@ -81,24 +100,17 @@ export class TodoScmReader implements TodoReader { { url }: ReadTodosOptions, etag?: string, ): Promise { - const shouldNotInclude = [ - '.png', - '.svg', - '.jpg', - '.jpeg', - '.gif', - '.raw', - '.lock', - '.ico', - ]; const tree = await this.reader.readTree(url, { etag, - filter(filePath) { + filter(filePath, info) { const extname = path.extname(filePath); + if (info && info.size > MAX_FILE_SIZE) { + return false; + } return ( !filePath.startsWith('.') && !filePath.includes('/.') && - !shouldNotInclude.includes(extname) + !excludedExtensions.includes(extname) ); }, }); From 8543d989068241e739894f6b81b2220e1abe8a71 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 16 Aug 2021 13:34:34 +0200 Subject: [PATCH 054/102] changesets: add changeset for readTree filter addition Signed-off-by: Patrik Oldsberg --- .changeset/fair-otters-applaud.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fair-otters-applaud.md diff --git a/.changeset/fair-otters-applaud.md b/.changeset/fair-otters-applaud.md new file mode 100644 index 0000000000..8e35c6fe6d --- /dev/null +++ b/.changeset/fair-otters-applaud.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-common': patch +--- + +Add an optional `info` parameter to the `readTree` filter option with a `size` property. From c8fd2fc190a3b93b441950aa1d394c3be2a572fb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Aug 2021 11:43:04 +0200 Subject: [PATCH 055/102] backend-common: update API report Signed-off-by: Patrik Oldsberg --- packages/backend-common/api-report.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend-common/api-report.md b/packages/backend-common/api-report.md index 2652dad122..736899d1ed 100644 --- a/packages/backend-common/api-report.md +++ b/packages/backend-common/api-report.md @@ -593,7 +593,7 @@ export function useHotMemoize(_module: NodeModule, valueFactory: () => T): T; // src/cache/types.d.ts:34:5 - (ae-forgotten-export) The symbol "ClientOptions" needs to be exported by the entry point index.d.ts // src/middleware/errorHandler.d.ts:17:26 - (tsdoc-malformed-html-name) Invalid HTML element: A space is not allowed here // src/reading/AzureUrlReader.d.ts:9:9 - (ae-forgotten-export) The symbol "ReadTreeResponseFactory" needs to be exported by the entry point index.d.ts -// src/reading/types.d.ts:106:5 - (ae-forgotten-export) The symbol "ReadTreeResponseDirOptions" needs to be exported by the entry point index.d.ts +// src/reading/types.d.ts:108:5 - (ae-forgotten-export) The symbol "ReadTreeResponseDirOptions" needs to be exported by the entry point index.d.ts // src/service/types.d.ts:12:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/service/types.d.ts:22:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/service/types.d.ts:30:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen From 8f2c880b80441d9cc9554dee15a7e4d1dc458933 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Aug 2021 13:55:13 +0200 Subject: [PATCH 056/102] todo-backend: fix TodoScmReader caching logic Signed-off-by: Patrik Oldsberg --- .../src/lib/TodoReader/TodoScmReader.test.ts | 8 +++++- .../src/lib/TodoReader/TodoScmReader.ts | 26 +++++++++++-------- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts index a3fc607404..9675a285e0 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.test.ts @@ -74,7 +74,13 @@ describe('TodoScmReader', () => { ], }; - await expect(todoReader.readTodos({ url })).resolves.toEqual(expected); + // These two reads should only result in a single call to readTree + await expect( + Promise.all([ + todoReader.readTodos({ url }), + todoReader.readTodos({ url }), + ]), + ).resolves.toEqual([expected, expected]); expect(reader.readTree).toHaveBeenCalledTimes(1); expect(reader.readTree).toHaveBeenCalledWith( diff --git a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts index 2bd0f0b038..8442983637 100644 --- a/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts +++ b/plugins/todo-backend/src/lib/TodoReader/TodoScmReader.ts @@ -77,22 +77,26 @@ export class TodoScmReader implements TodoReader { } async readTodos({ url }: ReadTodosOptions): Promise { + const inFlightRead = this.inFlightReads.get(url); + if (inFlightRead) { + return inFlightRead.then(read => read.result); + } + const cacheItem = this.cache.get(url); - try { - const inFlightRead = this.inFlightReads.get(url); - if (inFlightRead) { - return (await inFlightRead).result; + const newRead = this.doReadTodos({ url }, cacheItem?.etag).catch(error => { + if (cacheItem && error.name === 'NotModifiedError') { + return cacheItem; } - const newRead = this.doReadTodos({ url }, cacheItem?.etag); - this.inFlightReads.set(url, newRead); + throw error; + }); + + this.inFlightReads.set(url, newRead); + try { const newCacheItem = await newRead; this.cache.set(url, newCacheItem); return newCacheItem.result; - } catch (error) { - if (cacheItem && error.name === 'NotModifiedError') { - return cacheItem.result; - } - throw error; + } finally { + this.inFlightReads.delete(url); } } From 8ea1e96b3231e2b4b565818d2845401fa279b08b Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Aug 2021 14:31:35 +0200 Subject: [PATCH 057/102] cli: fix diff command filepath handling on windows Signed-off-by: Patrik Oldsberg --- .changeset/friendly-eyes-explain.md | 5 +++++ packages/cli/src/lib/diff/handlers.ts | 3 ++- 2 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 .changeset/friendly-eyes-explain.md diff --git a/.changeset/friendly-eyes-explain.md b/.changeset/friendly-eyes-explain.md new file mode 100644 index 0000000000..0bee61a82c --- /dev/null +++ b/.changeset/friendly-eyes-explain.md @@ -0,0 +1,5 @@ +--- +'@backstage/cli': patch +--- + +Fix file path handling in diff commands on Windows. diff --git a/packages/cli/src/lib/diff/handlers.ts b/packages/cli/src/lib/diff/handlers.ts index 5cbddde0ec..4a17125e5a 100644 --- a/packages/cli/src/lib/diff/handlers.ts +++ b/packages/cli/src/lib/diff/handlers.ts @@ -16,6 +16,7 @@ import chalk from 'chalk'; import { diffLines } from 'diff'; +import { sep, posix } from 'path'; import { FileDiff, PromptFunc, FileHandler, WriteFileFunc } from './types'; function sortObjectKeys(obj: Record) { @@ -281,7 +282,7 @@ export async function handleAllFiles( promptFunc: PromptFunc, ) { for (const file of files) { - const { path } = file; + const path = file.path.split(sep).join(posix.sep); const fileHandler = fileHandlers.find(handler => handler.patterns.some(pattern => typeof pattern === 'string' ? pattern === path : pattern.test(path), From aed3e16dd901c3435aa25d4809bb0fd3f6e9faa2 Mon Sep 17 00:00:00 2001 From: Samira Mokaram Date: Tue, 17 Aug 2021 14:34:46 +0200 Subject: [PATCH 058/102] update changeset Signed-off-by: Samira Mokaram --- .changeset/fifty-insects-love.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changeset/fifty-insects-love.md b/.changeset/fifty-insects-love.md index 6df7897f05..1e992cb72f 100644 --- a/.changeset/fifty-insects-love.md +++ b/.changeset/fifty-insects-love.md @@ -1,6 +1,6 @@ --- -'@backstage/core-app-api': minor -'@backstage/plugin-auth-backend': minor +'@backstage/core-app-api': patch +'@backstage/plugin-auth-backend': patch --- Add support for additional app origins From 2b7d3455b54c54f63145d7478576f650ebc1bab3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ruben=20Lindstr=C3=B6m?= Date: Tue, 17 Aug 2021 14:44:30 +0200 Subject: [PATCH 059/102] Composable Homepage (#6486) feat: implement homepage plugin --- .changeset/blue-tables-sparkle.md | 9 ++ .github/CODEOWNERS | 1 + packages/app/package.json | 1 + packages/app/src/App.tsx | 7 + packages/app/src/components/home/HomePage.tsx | 72 ++++++++++ plugins/home/.eslintrc.js | 3 + plugins/home/README.md | 95 +++++++++++++ plugins/home/api-report.md | 130 ++++++++++++++++++ plugins/home/dev/index.tsx | 27 ++++ plugins/home/package.json | 51 +++++++ .../componentRenderers/ComponentAccordion.tsx | 102 ++++++++++++++ .../ComponentTabs/ComponentTab.tsx | 36 +++++ .../ComponentTabs/ComponentTabs.tsx | 53 +++++++ .../componentRenderers/ComponentTabs/index.ts | 18 +++ plugins/home/src/componentRenderers/index.ts | 18 +++ .../components/HomepageCompositionRoot.tsx | 33 +++++ plugins/home/src/components/SettingsModal.tsx | 48 +++++++ plugins/home/src/components/index.ts | 18 +++ plugins/home/src/extensions.tsx | 123 +++++++++++++++++ .../homePageComponents/RandomJoke/Actions.tsx | 29 ++++ .../homePageComponents/RandomJoke/Content.tsx | 31 +++++ .../homePageComponents/RandomJoke/Context.tsx | 99 +++++++++++++ .../RandomJoke/Settings.tsx | 48 +++++++ .../homePageComponents/RandomJoke/index.ts | 20 +++ plugins/home/src/index.ts | 26 ++++ plugins/home/src/plugin.test.ts | 22 +++ plugins/home/src/plugin.ts | 68 +++++++++ plugins/home/src/routes.ts | 20 +++ plugins/home/src/setupTests.ts | 17 +++ 29 files changed, 1225 insertions(+) create mode 100644 .changeset/blue-tables-sparkle.md create mode 100644 packages/app/src/components/home/HomePage.tsx create mode 100644 plugins/home/.eslintrc.js create mode 100644 plugins/home/README.md create mode 100644 plugins/home/api-report.md create mode 100644 plugins/home/dev/index.tsx create mode 100644 plugins/home/package.json create mode 100644 plugins/home/src/componentRenderers/ComponentAccordion.tsx create mode 100644 plugins/home/src/componentRenderers/ComponentTabs/ComponentTab.tsx create mode 100644 plugins/home/src/componentRenderers/ComponentTabs/ComponentTabs.tsx create mode 100644 plugins/home/src/componentRenderers/ComponentTabs/index.ts create mode 100644 plugins/home/src/componentRenderers/index.ts create mode 100644 plugins/home/src/components/HomepageCompositionRoot.tsx create mode 100644 plugins/home/src/components/SettingsModal.tsx create mode 100644 plugins/home/src/components/index.ts create mode 100644 plugins/home/src/extensions.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/Actions.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/Content.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/Context.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/Settings.tsx create mode 100644 plugins/home/src/homePageComponents/RandomJoke/index.ts create mode 100644 plugins/home/src/index.ts create mode 100644 plugins/home/src/plugin.test.ts create mode 100644 plugins/home/src/plugin.ts create mode 100644 plugins/home/src/routes.ts create mode 100644 plugins/home/src/setupTests.ts diff --git a/.changeset/blue-tables-sparkle.md b/.changeset/blue-tables-sparkle.md new file mode 100644 index 0000000000..1656375267 --- /dev/null +++ b/.changeset/blue-tables-sparkle.md @@ -0,0 +1,9 @@ +--- +'@backstage/plugin-home': minor +--- + +Create the `Home` plugin which exports some basic functionality that's used to compose a homepage. An example of a composed homepage is added to the example app. + +This change also introduces the `createCardExtension` which creates a lazy loaded card that is intended to be used for homepage components. + +Adoption of this homepage requires setup similar to what can be found in the example app. diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index fda500ed02..781feb1f04 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -17,6 +17,7 @@ /plugins/techdocs @backstage/techdocs-core /plugins/techdocs-backend @backstage/techdocs-core /plugins/ilert @yacut +/plugins/home @backstage/techdocs-core /packages/search-common @backstage/techdocs-core /packages/techdocs-common @backstage/techdocs-core /.changeset/cost-insights-* @backstage/silver-lining diff --git a/packages/app/package.json b/packages/app/package.json index dcf14c88c1..1778857cfb 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -23,6 +23,7 @@ "@backstage/plugin-gcp-projects": "^0.3.2", "@backstage/plugin-github-actions": "^0.4.16", "@backstage/plugin-graphiql": "^0.2.14", + "@backstage/plugin-home": "^0.1.1", "@backstage/plugin-jenkins": "^0.5.3", "@backstage/plugin-kafka": "^0.2.13", "@backstage/plugin-kubernetes": "^0.4.11", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index f0ee099924..a73039fed2 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -20,6 +20,7 @@ import { OAuthRequestDialog, SignInPage, } from '@backstage/core-components'; +import { HomepageCompositionRoot } from '@backstage/plugin-home'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { CatalogEntityPage, @@ -63,6 +64,8 @@ import { Root } from './components/Root'; import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; import { LowerCaseValuePickerFieldExtension } from './components/scaffolder/customScaffolderExtensions'; +import { HomePage } from './components/home/HomePage'; + import { providers } from './identityProviders'; import * as plugins from './plugins'; @@ -108,6 +111,10 @@ const AppRouter = app.getRouter(); const routes = ( + {/* TODO(rubenl): Move this to / once its more mature and components exist */} + }> + + } /> ( + + + + + + + + + + + ( + + ), + }, + { + label: 'Any', + Component: () => ( + + ), + }, + ]} + /> + + +); diff --git a/plugins/home/.eslintrc.js b/plugins/home/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/home/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/home/README.md b/plugins/home/README.md new file mode 100644 index 0000000000..a09c85775b --- /dev/null +++ b/plugins/home/README.md @@ -0,0 +1,95 @@ +# Home + +Development is ongoing. You can follow the progress and contribute at the Backstage [Home Project Board](https://github.com/backstage/backstage/projects/7) or reach out to us in the [`#support` Discord channel](https://discord.com/channels/687207715902193673/687235481154617364). + +## Overview + +The Home plugin introduces a system for composing a Home Page for Backstage in order to surface relevant info and provide convenient shortcuts for common tasks. It's designed with composability in mind with an open ecosystem that allows anyone to contribute with any component, to be included in any Home Page. + +For App Integrators, the system is designed to be composable to give total freedom in designing a Home Page that suits the needs of the organization. From the perspective of a Component Developer who wishes to contribute with building blocks to be included in Home Pages, there's a convenient interface for bundling the different parts and exporting them with both error boundary and lazy loading handled under the surface. + +## Getting started + +If you have a standalone app (you didn't clone this repo), then do + +```bash +# From the Backstage repository root +cd packages/app +yarn add @backstage/plugin-home +``` + +### Setting up the Home Page + +1. Create a Home Page Component that will be used for composition. + +`packages/app/src/components/home/HomePage.tsx` + +```tsx +import React from 'react'; + +export const HomePage = () => { + return { + /* TODO: Compose a Home Page here */ + }; +}; +``` + +2. Add a route where the homepage will live, presumably `/`. + +`packages/app/src/App.tsx` + +```tsx +import { HomepageCompositionRoot } from '@backstage/plugin-home'; +import { HomePage } from './components/home/HomePage'; + +// ... +}> + +; +// ... +``` + +### Creating Components + +The Home Page can be composed with regular React components, so there's no magic in creating components to be used for composition 🪄 🎩 . However, in order to assure that your component fits into a diverse set of Home Pages, there's an extension creator for this purpose, that creates a Card-based layout, for consistency between components (read more about extensions [here](https://backstage.io/docs/plugins/composability#extensions)). The extension creator requires two fields: `title` and `components`. The `components` field is expected to be an asynchronous import that should at least contain a `Content` field. Additionally, you can optionally provide `settings`, `actions` and `contextProvider` as well. These parts will be combined to create a card, where the `content`, `actions` and `settings` will be wrapped within the `contextProvider` in order to be able to access to context and effectively communicate with one another. + +Finally, the `createCardExtension` also accepts a generic, such that Component Developers can indicate to App Integrators what custom props their component will accept, such as the example below where the default category of the random jokes can be set. + +```tsx +export const RandomJokeHomePageComponent = homePlugin.provide( + createCardExtension<{ defaultCategory?: 'programming' | 'any' }>({ + title: 'Random Joke', + components: () => import('./homePageComponents/RandomJoke'), + }), +); +``` + +In summary: it is not necessary to use the `createCardExtension` extension creator to register a home page component, although it is convenient since it provides error boundary and lazy loading, and it also may hook into other functionality in the future. + +### Composing a Home Page + +Composing a Home Page is no different from creating a regular React Component, i.e. the App Integrator is free to include whatever content they like. However, there are components developed with the Home Page in mind, as described in the previous section. If created by the `createCardExtension` extension creator, they are rendered like so + +```tsx +import React from 'react' +import Grid from '@material-ui/core/Grid' +import { RandomJokeHomePageComponent } from '@backstage/plugin-home'; + +export const HomePage = () => { + return ( + + + + + + ) +} +``` + +Additionally, the App Integrator is provided an escape hatch in case the way the card is rendered does not fit their requirements. They may optionally pass the `Renderer`-prop, which will receive the `title`, `content` and optionally `actions`, `settings` and `contextProvider`, if they exist for the component. This allows the App Integrator to render the content in any way they want. + +## Contributing + +We believe that people have great ideas for what makes a useful Home Page, and we want to make it easy for every to benefit from the effort you put in to create something cool for the Home Page. Therefore, a great way of contributing is by simply creating more Home Page Components, than can then be used by everyone when composing their own Home Page. If they are tightly coupled to an existing plugin, it is recommended to allow them to live within that plugin, for convenience and to limit complex dependencies. On the other hand, if there's no clear plugin that the component is based on, it's also fine to contribute them into the [home plugin](/plugins/home/src/homePageComponents) + +Additionally, the API is at a very early state, so contributing with additional use cases may expose weaknesses in the current solution that we may iterate on, to provide more flexibility and ease of use for those who wish to develop components for the Home Page. diff --git a/plugins/home/api-report.md b/plugins/home/api-report.md new file mode 100644 index 0000000000..a2350b803b --- /dev/null +++ b/plugins/home/api-report.md @@ -0,0 +1,130 @@ +## API Report File for "@backstage/plugin-home" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts +/// + +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { Extension } from '@backstage/core-plugin-api'; +import { ReactNode } from 'react'; +import { RouteRef } from '@backstage/core-plugin-api'; + +// Warning: (ae-missing-release-tag) "ComponentAccordion" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ComponentAccordion: ({ + title, + Content, + Actions, + Settings, + ContextProvider, + ...childProps +}: { + title: string; + Content: () => JSX.Element; + Actions?: (() => JSX.Element) | undefined; + Settings?: (() => JSX.Element) | undefined; + ContextProvider?: ((props: any) => JSX.Element) | undefined; +}) => JSX.Element; + +// Warning: (ae-missing-release-tag) "ComponentTab" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ComponentTab: ({ + title, + Content, + ContextProvider, + ...childProps +}: { + title: string; + Content: () => JSX.Element; + ContextProvider?: ((props: any) => JSX.Element) | undefined; +}) => JSX.Element; + +// Warning: (ae-missing-release-tag) "ComponentTabs" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const ComponentTabs: ({ + title, + tabs, +}: { + title: string; + tabs: { + label: string; + Component: () => JSX.Element; + }[]; +}) => JSX.Element; + +// Warning: (ae-forgotten-export) The symbol "ComponentRenderer" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "createCardExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export function createCardExtension({ + title, + components, +}: { + title: string; + components: () => Promise; +}): Extension< + ({ + Renderer, + title: overrideTitle, + ...childProps + }: ComponentRenderer & { + title?: string; + } & T) => JSX.Element +>; + +// Warning: (ae-missing-release-tag) "HomepageCompositionRoot" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const HomepageCompositionRoot: (props: { + title?: string | undefined; + children?: ReactNode; +}) => JSX.Element; + +// Warning: (ae-missing-release-tag) "homePlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const homePlugin: BackstagePlugin< + { + root: RouteRef; + }, + {} +>; + +// Warning: (ae-missing-release-tag) "RandomJokeHomePageComponent" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const RandomJokeHomePageComponent: ({ + Renderer, + title: overrideTitle, + ...childProps +}: ComponentRenderer & { + title?: string | undefined; +} & { + defaultCategory?: 'any' | 'programming' | undefined; +}) => JSX.Element; + +// Warning: (ae-missing-release-tag) "SettingsModal" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const SettingsModal: ({ + open, + close, + componentName, + children, +}: { + open: boolean; + close: Function; + componentName: string; + children: JSX.Element; +}) => JSX.Element; + +// Warnings were encountered during analysis: +// +// src/extensions.d.ts:16:5 - (ae-forgotten-export) The symbol "ComponentParts" needs to be exported by the entry point index.d.ts + +// (No @packageDocumentation comment for this package) +``` diff --git a/plugins/home/dev/index.tsx b/plugins/home/dev/index.tsx new file mode 100644 index 0000000000..802203e9f4 --- /dev/null +++ b/plugins/home/dev/index.tsx @@ -0,0 +1,27 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { createDevApp } from '@backstage/dev-utils'; +import { homePlugin, HomepageCompositionRoot } from '../src/plugin'; + +createDevApp() + .registerPlugin(homePlugin) + .addPage({ + element: , + title: 'Root Page', + path: '/', + }) + .render(); diff --git a/plugins/home/package.json b/plugins/home/package.json new file mode 100644 index 0000000000..b2c1741728 --- /dev/null +++ b/plugins/home/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-home", + "version": "0.1.1", + "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" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core-components": "^0.3.1", + "@backstage/core-plugin-api": "^0.1.6", + "@backstage/theme": "^0.2.9", + "@material-ui/core": "^4.12.2", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@types/react": "^16.9", + "react": "^16.13.1", + "react-dom": "^16.13.1", + "react-router": "^6.0.0-beta.0", + "react-use": "^17.2.4" + }, + "devDependencies": { + "@backstage/cli": "^0.7.8", + "@backstage/core-app-api": "^0.1.8", + "@backstage/dev-utils": "^0.2.6", + "@backstage/test-utils": "^0.1.17", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^13.1.8", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "cross-fetch": "^3.0.6", + "msw": "^0.29.0" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/home/src/componentRenderers/ComponentAccordion.tsx b/plugins/home/src/componentRenderers/ComponentAccordion.tsx new file mode 100644 index 0000000000..5cf54d7a0c --- /dev/null +++ b/plugins/home/src/componentRenderers/ComponentAccordion.tsx @@ -0,0 +1,102 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + Accordion, + AccordionDetails, + AccordionSummary, + Typography, + IconButton, + Theme, +} from '@material-ui/core'; +import { makeStyles } from '@material-ui/core/styles'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; +import SettingsIcon from '@material-ui/icons/Settings'; + +import { SettingsModal } from '../components'; + +const useStyles = makeStyles((theme: Theme) => ({ + settingsIconButton: { + padding: theme.spacing(0, 1, 0, 0), + }, +})); + +export const ComponentAccordion = ({ + title, + Content, + Actions, + Settings, + ContextProvider, + ...childProps +}: { + title: string; + Content: () => JSX.Element; + Actions?: () => JSX.Element; + Settings?: () => JSX.Element; + ContextProvider?: (props: any) => JSX.Element; +}) => { + const classes = useStyles(); + const [settingsIsExpanded, setSettingsIsExpanded] = React.useState(false); + const [isExpanded, setIsExpanded] = React.useState(false); + + const handleOpenSettings = (e: any) => { + e.stopPropagation(); + setSettingsIsExpanded(prevState => !prevState); + }; + + const innerContent = ( + <> + {Settings && ( + setSettingsIsExpanded(false)} + componentName={title} + > + + + )} + setIsExpanded(expanded)} + > + }> + {Settings && ( + + + + )} + {title} + + +
+ + {Actions && } +
+
+
+ + ); + + return ContextProvider ? ( + {innerContent} + ) : ( + innerContent + ); +}; diff --git a/plugins/home/src/componentRenderers/ComponentTabs/ComponentTab.tsx b/plugins/home/src/componentRenderers/ComponentTabs/ComponentTab.tsx new file mode 100644 index 0000000000..2b3e4e746c --- /dev/null +++ b/plugins/home/src/componentRenderers/ComponentTabs/ComponentTab.tsx @@ -0,0 +1,36 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +export const ComponentTab = ({ + title, + Content, + ContextProvider, + ...childProps +}: { + title: string; + Content: () => JSX.Element; + ContextProvider?: (props: any) => JSX.Element; +}) => { + return ContextProvider ? ( + + + + ) : ( + + ); +}; diff --git a/plugins/home/src/componentRenderers/ComponentTabs/ComponentTabs.tsx b/plugins/home/src/componentRenderers/ComponentTabs/ComponentTabs.tsx new file mode 100644 index 0000000000..f404a80980 --- /dev/null +++ b/plugins/home/src/componentRenderers/ComponentTabs/ComponentTabs.tsx @@ -0,0 +1,53 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Tabs, Tab } from '@material-ui/core'; +import { InfoCard } from '@backstage/core-components'; + +type TabType = { + label: string; + Component: () => JSX.Element; +}; + +export const ComponentTabs = ({ + title, + tabs, +}: { + title: string; + tabs: TabType[]; +}) => { + const [value, setValue] = React.useState(0); + + const handleChange = (_event: any, newValue: number) => { + setValue(newValue); + }; + + return ( + + + {tabs.map(t => ( + + ))} + + {tabs.map(({ Component }, idx) => ( +
+ +
+ ))} +
+ ); +}; diff --git a/plugins/home/src/componentRenderers/ComponentTabs/index.ts b/plugins/home/src/componentRenderers/ComponentTabs/index.ts new file mode 100644 index 0000000000..672d6e8385 --- /dev/null +++ b/plugins/home/src/componentRenderers/ComponentTabs/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ComponentTabs } from './ComponentTabs'; +export { ComponentTab } from './ComponentTab'; diff --git a/plugins/home/src/componentRenderers/index.ts b/plugins/home/src/componentRenderers/index.ts new file mode 100644 index 0000000000..5c5d3a457a --- /dev/null +++ b/plugins/home/src/componentRenderers/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ComponentAccordion } from './ComponentAccordion'; +export { ComponentTabs, ComponentTab } from './ComponentTabs'; diff --git a/plugins/home/src/components/HomepageCompositionRoot.tsx b/plugins/home/src/components/HomepageCompositionRoot.tsx new file mode 100644 index 0000000000..92232fb387 --- /dev/null +++ b/plugins/home/src/components/HomepageCompositionRoot.tsx @@ -0,0 +1,33 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { ReactNode } from 'react'; +import { useOutlet } from 'react-router'; +import { Content, Header, Page } from '@backstage/core-components'; + +export const HomepageCompositionRoot = (props: { + title?: string; + children?: ReactNode; +}) => { + const outlet = useOutlet(); + const children = props.children ?? outlet; + return ( + +
+ {children} + + ); +}; diff --git a/plugins/home/src/components/SettingsModal.tsx b/plugins/home/src/components/SettingsModal.tsx new file mode 100644 index 0000000000..25566bccdf --- /dev/null +++ b/plugins/home/src/components/SettingsModal.tsx @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@material-ui/core'; + +export const SettingsModal = ({ + open, + close, + componentName, + children, +}: { + open: boolean; + close: Function; + componentName: string; + children: JSX.Element; +}) => { + return ( + close()}> + Settings - {componentName} + {children} + + + + + ); +}; diff --git a/plugins/home/src/components/index.ts b/plugins/home/src/components/index.ts new file mode 100644 index 0000000000..aae0cbe26d --- /dev/null +++ b/plugins/home/src/components/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { HomepageCompositionRoot } from './HomepageCompositionRoot'; +export { SettingsModal } from './SettingsModal'; diff --git a/plugins/home/src/extensions.tsx b/plugins/home/src/extensions.tsx new file mode 100644 index 0000000000..93fe4ceb4f --- /dev/null +++ b/plugins/home/src/extensions.tsx @@ -0,0 +1,123 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { Suspense } from 'react'; +import { IconButton } from '@material-ui/core'; +import SettingsIcon from '@material-ui/icons/Settings'; +import { InfoCard } from '@backstage/core-components'; +import { SettingsModal } from './components'; +import { createReactExtension, useApp } from '@backstage/core-plugin-api'; + +export type ComponentRenderer = { + Renderer?: (props: RendererProps) => JSX.Element; +}; + +type ComponentParts = { + Content: () => JSX.Element; + Actions?: () => JSX.Element; + Settings?: () => JSX.Element; + ContextProvider?: (props: any) => JSX.Element; +}; + +type RendererProps = { title: string } & ComponentParts; + +export function createCardExtension({ + title, + components, +}: { + title: string; + components: () => Promise; +}) { + return createReactExtension({ + component: { + lazy: () => + components().then(({ Content, Actions, Settings, ContextProvider }) => { + const CardExtension = ({ + Renderer, + title: overrideTitle, + ...childProps + }: ComponentRenderer & { title?: string } & T) => { + const app = useApp(); + const { Progress } = app.getComponents(); + const [settingsOpen, setSettingsOpen] = React.useState(false); + + if (Renderer) { + return ( + }> + + + ); + } + + const cardProps = { + title: overrideTitle ?? title, + ...(Settings + ? { + action: ( + setSettingsOpen(true)}> + Settings + + ), + } + : {}), + ...(Actions + ? { + actions: , + } + : {}), + }; + + const innerContent = ( + + {Settings && ( + setSettingsOpen(false)} + > + + + )} + + + ); + + return ( + }> + {ContextProvider ? ( + + {innerContent} + + ) : ( + innerContent + )} + + ); + }; + return CardExtension; + }), + }, + }); +} diff --git a/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx b/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx new file mode 100644 index 0000000000..cebb0400ab --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Actions.tsx @@ -0,0 +1,29 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; + +import { Button } from '@material-ui/core'; +import { useRandomJoke } from './Context'; + +export const Actions = () => { + const { rerollJoke } = useRandomJoke(); + return ( + + ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Content.tsx b/plugins/home/src/homePageComponents/RandomJoke/Content.tsx new file mode 100644 index 0000000000..231e32356a --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Content.tsx @@ -0,0 +1,31 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { useRandomJoke } from './Context'; + +export const Content = () => { + const { joke, loading } = useRandomJoke(); + + if (loading) return

Loading...

; + + return ( +
+

{joke.setup}

+

{joke.punchline}

+
+ ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Context.tsx b/plugins/home/src/homePageComponents/RandomJoke/Context.tsx new file mode 100644 index 0000000000..5c2790d8a5 --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Context.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { createContext } from 'react'; + +export type JokeType = 'any' | 'programming'; + +type Joke = { + setup: string; + punchline: string; +}; + +type RandomJokeContextValue = { + loading: boolean; + joke: Joke; + type: JokeType; + rerollJoke: Function; + handleChangeType: Function; +}; + +const Context = createContext(undefined); + +const getNewJoke = (type: string): Promise => + fetch( + `https://official-joke-api.appspot.com/jokes${ + type !== 'any' ? `/${type}` : '' + }/random`, + ) + .then(res => res.json()) + .then(data => (Array.isArray(data) ? data[0] : data)); + +export const ContextProvider = ({ + children, + defaultCategory, +}: { + children: JSX.Element; + defaultCategory?: JokeType; +}) => { + const [loading, setLoading] = React.useState(true); + const [joke, setJoke] = React.useState({ + setup: '', + punchline: '', + }); + const [type, setType] = React.useState( + defaultCategory || ('programming' as JokeType), + ); + + const rerollJoke = React.useCallback(() => { + setLoading(true); + getNewJoke(type).then(newJoke => setJoke(newJoke)); + }, [type]); + + const handleChangeType = (newType: JokeType) => { + setType(newType); + }; + + React.useEffect(() => { + setLoading(false); + }, [joke]); + + React.useEffect(() => { + rerollJoke(); + }, [rerollJoke]); + + const value: RandomJokeContextValue = { + loading, + joke, + type, + rerollJoke, + handleChangeType, + }; + + return {children}; +}; + +export const useRandomJoke = () => { + const value = React.useContext(Context); + + if (value === undefined) { + throw new Error('useRandomJoke must be used within a RandomJokeProvider'); + } + + return value; +}; + +export default Context; diff --git a/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx b/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx new file mode 100644 index 0000000000..ee5de69dc6 --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/Settings.tsx @@ -0,0 +1,48 @@ +/* + * 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 { + FormControl, + FormLabel, + RadioGroup, + FormControlLabel, + Radio, +} from '@material-ui/core'; +import React from 'react'; +import { useRandomJoke, JokeType } from './Context'; + +export const Settings = () => { + const { type, handleChangeType } = useRandomJoke(); + const JOKE_TYPES: JokeType[] = ['any' as JokeType, 'programming' as JokeType]; + return ( + + Joke Type + handleChangeType(e.target.value)} + > + {JOKE_TYPES.map(t => ( + } + label={`${t.slice(0, 1).toUpperCase()}${t.slice(1)}`} + /> + ))} + + + ); +}; diff --git a/plugins/home/src/homePageComponents/RandomJoke/index.ts b/plugins/home/src/homePageComponents/RandomJoke/index.ts new file mode 100644 index 0000000000..aafbc17a2e --- /dev/null +++ b/plugins/home/src/homePageComponents/RandomJoke/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { Actions } from './Actions'; +export { Content } from './Content'; +export { Settings } from './Settings'; +export { ContextProvider } from './Context'; diff --git a/plugins/home/src/index.ts b/plugins/home/src/index.ts new file mode 100644 index 0000000000..812e1b2495 --- /dev/null +++ b/plugins/home/src/index.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { + homePlugin, + HomepageCompositionRoot, + RandomJokeHomePageComponent, + ComponentAccordion, + ComponentTabs, + ComponentTab, +} from './plugin'; +export { SettingsModal } from './components'; +export { createCardExtension } from './extensions'; diff --git a/plugins/home/src/plugin.test.ts b/plugins/home/src/plugin.test.ts new file mode 100644 index 0000000000..920a9076f4 --- /dev/null +++ b/plugins/home/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { homePlugin } from './plugin'; + +describe('home', () => { + it('should export plugin', () => { + expect(homePlugin).toBeDefined(); + }); +}); diff --git a/plugins/home/src/plugin.ts b/plugins/home/src/plugin.ts new file mode 100644 index 0000000000..5600d2d315 --- /dev/null +++ b/plugins/home/src/plugin.ts @@ -0,0 +1,68 @@ +/* + * 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 { + createComponentExtension, + createPlugin, + createRoutableExtension, +} from '@backstage/core-plugin-api'; +import { createCardExtension } from './extensions'; + +import { rootRouteRef } from './routes'; + +export const homePlugin = createPlugin({ + id: 'home', + routes: { + root: rootRouteRef, + }, +}); + +export const HomepageCompositionRoot = homePlugin.provide( + createRoutableExtension({ + component: () => + import('./components').then(m => m.HomepageCompositionRoot), + mountPoint: rootRouteRef, + }), +); + +export const ComponentAccordion = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => + import('./componentRenderers').then(m => m.ComponentAccordion), + }, + }), +); +export const ComponentTabs = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./componentRenderers').then(m => m.ComponentTabs), + }, + }), +); +export const ComponentTab = homePlugin.provide( + createComponentExtension({ + component: { + lazy: () => import('./componentRenderers').then(m => m.ComponentTab), + }, + }), +); + +export const RandomJokeHomePageComponent = homePlugin.provide( + createCardExtension<{ defaultCategory?: 'any' | 'programming' }>({ + title: 'Random Joke', + components: () => import('./homePageComponents/RandomJoke'), + }), +); diff --git a/plugins/home/src/routes.ts b/plugins/home/src/routes.ts new file mode 100644 index 0000000000..2c641d8433 --- /dev/null +++ b/plugins/home/src/routes.ts @@ -0,0 +1,20 @@ +/* + * 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 { createRouteRef } from '@backstage/core-plugin-api'; + +export const rootRouteRef = createRouteRef({ + title: 'home', +}); diff --git a/plugins/home/src/setupTests.ts b/plugins/home/src/setupTests.ts new file mode 100644 index 0000000000..fc6dbd98f8 --- /dev/null +++ b/plugins/home/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; From ba01a6bcdfc918d53c2494482bb44a70f6cb4aa2 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Tue, 17 Aug 2021 14:59:07 +0200 Subject: [PATCH 060/102] Parse date correctly, drop math.Floor Signed-off-by: Johan Haals --- .../DefaultProcessingDatabase.test.ts | 19 ++++++++++++++++--- plugins/catalog-backend/src/next/refresh.ts | 2 +- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts index 09647b980c..df9c08ad6c 100644 --- a/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts +++ b/plugins/catalog-backend/src/next/database/DefaultProcessingDatabase.test.ts @@ -66,6 +66,21 @@ describe('Default Processing Database', () => { await db('refresh_state').insert(ref); }; + const parseDate = (date: string | Date): DateTime => { + const parsedDate = + typeof date === 'string' + ? DateTime.fromSQL(date, { zone: 'UTC' }) + : DateTime.fromJSDate(date); + + if (!parsedDate.isValid) { + throw new Error( + `Failed to parse date, reason: ${parsedDate.invalidReason}, explanation: ${parsedDate.invalidExplanation}`, + ); + } + + return parsedDate; + }; + describe('addUprocessedEntities', () => { function mockEntity(name: string, type: string): Entity { return { @@ -995,9 +1010,7 @@ describe('Default Processing Database', () => { const result = await knex('refresh_state') .where('entity_ref', 'location:default/new-root') .select(); - const nextUpdate = DateTime.fromSQL(result[0].next_update_at, { - zone: 'utc', - }); + const nextUpdate = parseDate(result[0].next_update_at); expect(nextUpdate.diff(now, 'seconds').seconds).toBeGreaterThanOrEqual( 100, ); diff --git a/plugins/catalog-backend/src/next/refresh.ts b/plugins/catalog-backend/src/next/refresh.ts index caec7e9548..aae992f391 100644 --- a/plugins/catalog-backend/src/next/refresh.ts +++ b/plugins/catalog-backend/src/next/refresh.ts @@ -29,6 +29,6 @@ export function createRandomRefreshInterval(options: { }): RefreshIntervalFunction { const { minSeconds, maxSeconds } = options; return () => { - return Math.floor(Math.random() * (maxSeconds - minSeconds) + minSeconds); + return Math.random() * (maxSeconds - minSeconds) + minSeconds; }; } From 879a98ca3b36970450f61e33d6c49feafb0e6cdb Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 17 Aug 2021 17:22:57 +0200 Subject: [PATCH 061/102] auth-backend: fix origin filter tests on windows Signed-off-by: Patrik Oldsberg --- plugins/auth-backend/src/service/router.test.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/auth-backend/src/service/router.test.ts b/plugins/auth-backend/src/service/router.test.ts index ca8a14f4e6..f0f5134848 100644 --- a/plugins/auth-backend/src/service/router.test.ts +++ b/plugins/auth-backend/src/service/router.test.ts @@ -28,19 +28,19 @@ describe('Auth origin filtering', () => { const config = defaultConfig(); config.getOptionalString = getOptionalString; it('Will explode, invalid origin', () => { - const origin = 'https://test\\.example.net'; + const origin = 'https://test.example.net'; expect(createOriginFilter(config)(origin)).toBeFalsy(); }); it('Will explode, invalid origin domain', () => { - const origin = 'https://test-1234\\.examplee.net'; + const origin = 'https://test-1234.examplee.net'; expect(createOriginFilter(config)(origin)).toBeFalsy(); }); it("Won't explode, valid origin with numbers", () => { - const origin = 'https://test-1234\\.example.net'; + const origin = 'https://test-1234.example.net'; expect(createOriginFilter(config)(origin)).toBeTruthy(); }); it("Won't explode, valid origin with chars and numbers", () => { - const origin = 'https://test-test1234\\.example.net'; + const origin = 'https://test-test1234.example.net'; expect(createOriginFilter(config)(origin)).toBeTruthy(); }); }); From 24d0e1ea1253f8f828741d2405cddc589d9d02f6 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 17 Aug 2021 17:32:21 +0200 Subject: [PATCH 062/102] Set field id in scaffolder Signed-off-by: Oliver Sand --- .changeset/fifty-taxis-deny.md | 5 +++++ .../src/components/fields/EntityPicker/EntityPicker.tsx | 4 +++- .../components/fields/TextValuePicker/TextValuePicker.tsx | 8 ++++++-- 3 files changed, 14 insertions(+), 3 deletions(-) create mode 100644 .changeset/fifty-taxis-deny.md diff --git a/.changeset/fifty-taxis-deny.md b/.changeset/fifty-taxis-deny.md new file mode 100644 index 0000000000..63100635d1 --- /dev/null +++ b/.changeset/fifty-taxis-deny.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Set `id` in ``. diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index 04693a94cd..fca40d822f 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { useApi } from '@backstage/core-plugin-api'; import { catalogApiRef, formatEntityRefTitle, @@ -23,7 +24,6 @@ import Autocomplete from '@material-ui/lab/Autocomplete'; import { FieldProps } from '@rjsf/core'; import React from 'react'; import { useAsync } from 'react-use'; -import { useApi } from '@backstage/core-plugin-api'; export const EntityPicker = ({ onChange, @@ -32,6 +32,7 @@ export const EntityPicker = ({ uiSchema, rawErrors, formData, + idSchema, }: FieldProps) => { const allowedKinds = uiSchema['ui:options']?.allowedKinds as string[]; const defaultKind = uiSchema['ui:options']?.defaultKind as string | undefined; @@ -58,6 +59,7 @@ export const EntityPicker = ({ error={rawErrors?.length > 0 && !formData} > ) => ( Date: Tue, 17 Aug 2021 18:13:33 +0200 Subject: [PATCH 063/102] Update API report Signed-off-by: Oliver Sand --- plugins/scaffolder/api-report.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 4df2a0808c..b005610f04 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -66,6 +66,7 @@ export const EntityPicker: ({ uiSchema, rawErrors, formData, + idSchema, }: FieldProps) => JSX.Element; // Warning: (ae-missing-release-tag) "EntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -229,6 +230,8 @@ export const TextValuePicker: ({ rawErrors, formData, uiSchema: { 'ui:autofocus': autoFocus }, + idSchema, + placeholder, }: FieldProps) => JSX.Element; // (No @packageDocumentation comment for this package) From 77cdc5a842f85cfd661c4d2f6233d6a5e6230acf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 17 Aug 2021 19:40:06 +0200 Subject: [PATCH 064/102] Support passing a UserTransformer through the processor to the reader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/shy-pets-join.md | 5 +++ .../api-report.md | 3 ++ .../src/microsoftGraph/read.test.ts | 42 +++++++++++++++++++ .../src/microsoftGraph/read.ts | 2 + .../MicrosoftGraphOrgReaderProcessor.ts | 11 ++++- 5 files changed, 62 insertions(+), 1 deletion(-) create mode 100644 .changeset/shy-pets-join.md diff --git a/.changeset/shy-pets-join.md b/.changeset/shy-pets-join.md new file mode 100644 index 0000000000..ba39dfd4f4 --- /dev/null +++ b/.changeset/shy-pets-join.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Pass along a `UserTransformer` to the read step diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index 91f8180e94..f614bdca36 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -111,6 +111,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: MicrosoftGraphProviderConfig[]; logger: Logger_2; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; }); // (undocumented) @@ -118,6 +119,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { config: Config, options: { logger: Logger_2; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; }, ): MicrosoftGraphOrgReaderProcessor; @@ -170,6 +172,7 @@ export function readMicrosoftGraphOrg( options: { userFilter?: string; groupFilter?: string; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; logger: Logger_2; }, diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts index 68f7e216ea..82279b76c6 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.test.ts @@ -114,6 +114,48 @@ describe('read microsoft graph', () => { expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); }); + + it('should read users with custom transformer', async () => { + async function* getExampleUsers() { + yield { + id: 'userid', + displayName: 'User Name', + mail: 'user.name@example.com', + }; + } + + client.getUsers.mockImplementation(getExampleUsers); + client.getUserPhotoWithSizeLimit.mockResolvedValue( + 'data:image/jpeg;base64,...', + ); + + const { users } = await readMicrosoftGraphUsers(client, { + userFilter: 'accountEnabled eq true', + transformer: async () => ({ + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'x' }, + spec: { memberOf: [] }, + }), + logger: getVoidLogger(), + }); + + expect(users).toEqual([ + { + apiVersion: 'backstage.io/v1alpha1', + kind: 'User', + metadata: { name: 'x' }, + spec: { memberOf: [] }, + }, + ]); + + expect(client.getUsers).toBeCalledTimes(1); + expect(client.getUsers).toBeCalledWith({ + filter: 'accountEnabled eq true', + }); + expect(client.getUserPhotoWithSizeLimit).toBeCalledTimes(1); + expect(client.getUserPhotoWithSizeLimit).toBeCalledWith('userid', 120); + }); }); describe('readMicrosoftGraphOrganization', () => { diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 62513b0e10..6fbd2cb0de 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -382,12 +382,14 @@ export async function readMicrosoftGraphOrg( options: { userFilter?: string; groupFilter?: string; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; logger: Logger; }, ): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> { const { users } = await readMicrosoftGraphUsers(client, { userFilter: options.userFilter, + transformer: options.userTransformer, logger: options.logger, }); const { groups, rootGroup, groupMember, groupMemberOf } = diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 88240db618..1495bce93c 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -28,6 +28,7 @@ import { MicrosoftGraphProviderConfig, readMicrosoftGraphConfig, readMicrosoftGraphOrg, + UserTransformer, } from '../microsoftGraph'; /** @@ -36,11 +37,16 @@ import { export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { private readonly providers: MicrosoftGraphProviderConfig[]; private readonly logger: Logger; + private readonly userTransformer?: UserTransformer; private readonly groupTransformer?: GroupTransformer; static fromConfig( config: Config, - options: { logger: Logger; groupTransformer?: GroupTransformer }, + options: { + logger: Logger; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + }, ) { const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg'); return new MicrosoftGraphOrgReaderProcessor({ @@ -52,10 +58,12 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { constructor(options: { providers: MicrosoftGraphProviderConfig[]; logger: Logger; + userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; }) { this.providers = options.providers; this.logger = options.logger; + this.userTransformer = options.userTransformer; this.groupTransformer = options.groupTransformer; } @@ -89,6 +97,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { { userFilter: provider.userFilter, groupFilter: provider.groupFilter, + userTransformer: this.userTransformer, groupTransformer: this.groupTransformer, logger: this.logger, }, From 9f9b808f963ecd1ec572ef00515bd58173d1aaf3 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 17 Aug 2021 23:28:24 +0200 Subject: [PATCH 065/102] Review comments Signed-off-by: Oliver Sand --- .../src/components/fields/EntityPicker/EntityPicker.tsx | 2 +- .../src/components/fields/TextValuePicker/TextValuePicker.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx index fca40d822f..a8ef524a13 100644 --- a/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx +++ b/plugins/scaffolder/src/components/fields/EntityPicker/EntityPicker.tsx @@ -59,7 +59,7 @@ export const EntityPicker = ({ error={rawErrors?.length > 0 && !formData} > ) => ( Date: Tue, 17 Aug 2021 17:19:45 -0400 Subject: [PATCH 066/102] chore(UnregisterEntityDialog): migrate to catalog-react Signed-off-by: Phil Kuang --- .changeset/seven-glasses-sit.md | 6 ++++++ plugins/catalog-react/api-report.md | 11 +++++++++++ .../UnregisterEntityDialog.test.tsx | 2 +- .../UnregisterEntityDialog.tsx | 4 ++-- .../components/UnregisterEntityDialog/index.ts | 17 +++++++++++++++++ .../useUnregisterEntityDialogState.test.tsx | 3 ++- .../useUnregisterEntityDialogState.ts | 2 +- plugins/catalog-react/src/components/index.ts | 1 + .../components/EntityLayout/EntityLayout.tsx | 2 +- .../EntityPageLayout/EntityPageLayout.tsx | 2 +- 10 files changed, 43 insertions(+), 7 deletions(-) create mode 100644 .changeset/seven-glasses-sit.md rename plugins/{catalog => catalog-react}/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx (99%) rename plugins/{catalog => catalog-react}/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx (98%) create mode 100644 plugins/catalog-react/src/components/UnregisterEntityDialog/index.ts rename plugins/{catalog => catalog-react}/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx (98%) rename plugins/{catalog => catalog-react}/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts (98%) diff --git a/.changeset/seven-glasses-sit.md b/.changeset/seven-glasses-sit.md new file mode 100644 index 0000000000..349c972641 --- /dev/null +++ b/.changeset/seven-glasses-sit.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog': patch +'@backstage/plugin-catalog-react': patch +--- + +Migrate and export `UnregisterEntityDialog` component from `catalog-react` package diff --git a/plugins/catalog-react/api-report.md b/plugins/catalog-react/api-report.md index 57bb5360c2..c5f3226874 100644 --- a/plugins/catalog-react/api-report.md +++ b/plugins/catalog-react/api-report.md @@ -715,6 +715,17 @@ export function reduceEntityFilters( // @public (undocumented) export const rootRoute: RouteRef; +// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts +// Warning: (ae-missing-release-tag) "UnregisterEntityDialog" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const UnregisterEntityDialog: ({ + open, + onConfirm, + onClose, + entity, +}: Props_3) => JSX.Element; + // Warning: (ae-missing-release-tag) "useEntity" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx similarity index 99% rename from plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx rename to plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx index 55ee42f253..339323c1bf 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.test.tsx @@ -21,7 +21,7 @@ import React from 'react'; import { UnregisterEntityDialog } from './UnregisterEntityDialog'; import { ORIGIN_LOCATION_ANNOTATION } from '@backstage/catalog-model'; import { CatalogClient } from '@backstage/catalog-client'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '../../api'; import { screen, waitFor } from '@testing-library/react'; import { renderInTestApp } from '@backstage/test-utils'; import * as state from './useUnregisterEntityDialogState'; diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx similarity index 98% rename from plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx rename to plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index abec90bad1..1f1f01ff48 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Backstage Authors + * 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. @@ -15,7 +15,7 @@ */ import { Entity } from '@backstage/catalog-model'; -import { EntityRefLink } from '@backstage/plugin-catalog-react'; +import { EntityRefLink } from '../EntityRefLink'; import { Box, Button, diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/index.ts b/plugins/catalog-react/src/components/UnregisterEntityDialog/index.ts new file mode 100644 index 0000000000..8fc750e706 --- /dev/null +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { UnregisterEntityDialog } from './UnregisterEntityDialog'; diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx similarity index 98% rename from plugins/catalog/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx rename to plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx index 6cc2f4b23b..8b4f436aed 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.test.tsx @@ -19,7 +19,8 @@ import { Location, ORIGIN_LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { CatalogApi, catalogApiRef } from '@backstage/plugin-catalog-react'; +import { CatalogApi } from '@backstage/catalog-client'; +import { catalogApiRef } from '../../api'; import { act, renderHook, diff --git a/plugins/catalog/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts similarity index 98% rename from plugins/catalog/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts rename to plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts index 9b2e68cc5d..ae0a323d36 100644 --- a/plugins/catalog/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/useUnregisterEntityDialogState.ts @@ -20,7 +20,7 @@ import { getEntityName, ORIGIN_LOCATION_ANNOTATION, } from '@backstage/catalog-model'; -import { catalogApiRef } from '@backstage/plugin-catalog-react'; +import { catalogApiRef } from '../../api'; import { useCallback } from 'react'; import { useAsync } from 'react-use'; import { useApi } from '@backstage/core-plugin-api'; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index 2664af8ef9..434514c4ed 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -23,4 +23,5 @@ export * from './EntityTable'; export * from './EntityTagPicker'; export * from './EntityTypePicker'; export * from './FavoriteEntity'; +export * from './UnregisterEntityDialog'; export * from './UserListPicker'; diff --git a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx index eabcbe4993..4eec0d5341 100644 --- a/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx +++ b/plugins/catalog/src/components/EntityLayout/EntityLayout.tsx @@ -39,6 +39,7 @@ import { EntityRefLinks, FavoriteEntity, getEntityRelations, + UnregisterEntityDialog, useEntityCompoundName, } from '@backstage/plugin-catalog-react'; import { Box, TabProps } from '@material-ui/core'; @@ -46,7 +47,6 @@ import { Alert } from '@material-ui/lab'; import React, { useContext, useState } from 'react'; import { useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; type SubRoute = { path: string; diff --git a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx index 088a9f2c53..c371d6245e 100644 --- a/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx +++ b/plugins/catalog/src/components/EntityPageLayout/EntityPageLayout.tsx @@ -23,13 +23,13 @@ import { EntityRefLinks, FavoriteEntity, getEntityRelations, + UnregisterEntityDialog, useEntityCompoundName, } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import React, { useContext, useState } from 'react'; import { useNavigate } from 'react-router'; import { EntityContextMenu } from '../EntityContextMenu/EntityContextMenu'; -import { UnregisterEntityDialog } from '../UnregisterEntityDialog/UnregisterEntityDialog'; import { Tabbed } from './Tabbed'; import { From 603094c8369bce8e1943f1d5618a33aaebdd5ebf Mon Sep 17 00:00:00 2001 From: Sathish Kumar Date: Tue, 17 Aug 2021 14:55:47 -0500 Subject: [PATCH 067/102] Adding Backstage Plugin for Gitlab Signed-off-by: Sathish Kumar --- microsite/data/gitlab.yaml | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 microsite/data/gitlab.yaml diff --git a/microsite/data/gitlab.yaml b/microsite/data/gitlab.yaml new file mode 100644 index 0000000000..d88f001c07 --- /dev/null +++ b/microsite/data/gitlab.yaml @@ -0,0 +1,12 @@ +--- +title: Gitlab +author: Loblaw +authorUrl: https://github.com/loblaw-sre/backstage-plugin-gitlab +category: CI/CD +description: View Gitlab pipelines, merge requests, languages and contributors. +documentation: https://github.com/loblaw-sre/backstage-plugin-gitlab +iconUrl: https://about.gitlab.com/images/icons/logos/slp-logo.svg +npmPackageName: '@loblaw/backstage-plugin-gitlab' +tags: + - ci + - cd From 83700022a0654facb622b2a41ec6e52d73368dbd Mon Sep 17 00:00:00 2001 From: Sathish Kumar Date: Tue, 17 Aug 2021 19:27:37 -0500 Subject: [PATCH 068/102] Updated the file path for the plugin Signed-off-by: Sathish Kumar --- microsite/data/{ => plugins}/gitlab.yaml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename microsite/data/{ => plugins}/gitlab.yaml (100%) diff --git a/microsite/data/gitlab.yaml b/microsite/data/plugins/gitlab.yaml similarity index 100% rename from microsite/data/gitlab.yaml rename to microsite/data/plugins/gitlab.yaml From 550101a2849e091c4d302209c0e6301f473e60de Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Tue, 17 Aug 2021 19:03:29 -0600 Subject: [PATCH 069/102] GitLab logo, name tweak Signed-off-by: Tim Hansen --- microsite/data/plugins/gitlab.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/microsite/data/plugins/gitlab.yaml b/microsite/data/plugins/gitlab.yaml index d88f001c07..7a5493c7f4 100644 --- a/microsite/data/plugins/gitlab.yaml +++ b/microsite/data/plugins/gitlab.yaml @@ -1,11 +1,11 @@ --- -title: Gitlab +title: GitLab author: Loblaw authorUrl: https://github.com/loblaw-sre/backstage-plugin-gitlab category: CI/CD -description: View Gitlab pipelines, merge requests, languages and contributors. +description: View GitLab pipelines, merge requests, languages and contributors. documentation: https://github.com/loblaw-sre/backstage-plugin-gitlab -iconUrl: https://about.gitlab.com/images/icons/logos/slp-logo.svg +iconUrl: https://about.gitlab.com/images/press/logo/png/gitlab-icon-rgb.png npmPackageName: '@loblaw/backstage-plugin-gitlab' tags: - ci From cd44f1d602bf58f32771c190a22d22da9445a6c6 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Aug 2021 04:07:43 +0000 Subject: [PATCH 070/102] chore(deps): bump dompurify from 2.3.0 to 2.3.1 Bumps [dompurify](https://github.com/cure53/DOMPurify) from 2.3.0 to 2.3.1. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](https://github.com/cure53/DOMPurify/compare/2.3.0...2.3.1) --- updated-dependencies: - dependency-name: dompurify dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4d15795a5f..a201aab185 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12425,15 +12425,10 @@ domhandler@^4.0.0: dependencies: domelementtype "^2.1.0" -dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.8: - version "2.2.8" - resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.2.8.tgz#ce88e395f6d00b6dc53f80d6b2a6fdf5446873c6" - integrity sha512-9H0UL59EkDLgY3dUFjLV6IEUaHm5qp3mxSqWw7Yyx4Zhk2Jn2cmLe+CNPP3xy13zl8Bqg+0NehQzkdMoVhGRww== - -dompurify@^2.2.9: - version "2.3.0" - resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.0.tgz#07bb39515e491588e5756b1d3e8375b5964814e2" - integrity sha512-VV5C6Kr53YVHGOBKO/F86OYX6/iLTw2yVSI721gKetxpHCK/V5TaLEf9ODjRgl1KLSWRMY6cUhAbv/c+IUnwQw== +dompurify@^2.0.12, dompurify@^2.1.1, dompurify@^2.2.8, dompurify@^2.2.9: + version "2.3.1" + resolved "https://registry.npmjs.org/dompurify/-/dompurify-2.3.1.tgz#a47059ca21fd1212d3c8f71fdea6943b8bfbdf6a" + integrity sha512-xGWt+NHAQS+4tpgbOAI08yxW0Pr256Gu/FNE2frZVTbgrBUn8M7tz7/ktS/LZ2MHeGqz6topj0/xY+y8R5FBFw== domutils@1.5.1: version "1.5.1" From 15b41477bd3c5d511720e44e2c1779214ea45dc3 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 18 Aug 2021 04:12:04 +0000 Subject: [PATCH 071/102] chore(deps): bump graphiql from 1.4.0 to 1.4.2 Bumps [graphiql](https://github.com/graphql/graphiql) from 1.4.0 to 1.4.2. - [Release notes](https://github.com/graphql/graphiql/releases) - [Changelog](https://github.com/graphql/graphiql/blob/main/CHANGELOG.md) - [Commits](https://github.com/graphql/graphiql/commits/graphiql@1.4.2) --- updated-dependencies: - dependency-name: graphiql dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] --- yarn.lock | 44 +++++++++++++++++++++++++------------------- 1 file changed, 25 insertions(+), 19 deletions(-) diff --git a/yarn.lock b/yarn.lock index 4d15795a5f..682a83563c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3073,14 +3073,15 @@ stream-events "^1.0.1" xdg-basedir "^4.0.0" -"@graphiql/toolkit@^0.1.0": - version "0.1.1" - resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.1.1.tgz#a7da3ba460ceae27bcdc8f03831ca4f88f90f3d7" - integrity sha512-cvsuaPkOA6/TZOdqEdvzqr7i+or2STTpSsteyDkrUXrwftRnH9ZfiUwnHPyf0AC2cKMwpP/Dny/UTS6CLC8ZNQ== +"@graphiql/toolkit@^0.2.0": + version "0.2.2" + resolved "https://registry.npmjs.org/@graphiql/toolkit/-/toolkit-0.2.2.tgz#193d570afcf686c9ee61c92054c1782b9f3c1255" + integrity sha512-kDgYhqnS4p4LqSo1KvLd3tbX8Hhdj0ZrgQuGsosjjEnahiPYmmylxUL1p9lj6348OsypcTlCncGpEjeb9S3TiQ== dependencies: - "@n1ru4l/push-pull-async-iterable-iterator" "^2.0.1" - graphql-ws "^4.1.0" - meros "^1.1.2" + "@n1ru4l/push-pull-async-iterable-iterator" "^2.1.4" + graphql-ws "^4.3.2" + meros "^1.1.4" + optionalDependencies: subscriptions-transport-ws "^0.9.18" "@graphql-codegen/cli@^1.21.3": @@ -4740,10 +4741,10 @@ strict-event-emitter "^0.2.0" xmldom "^0.6.0" -"@n1ru4l/push-pull-async-iterable-iterator@^2.0.1": - version "2.1.2" - resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-2.1.2.tgz#e486bf86c4c29e78601694a26f31c2dec0c08d9b" - integrity sha512-KwZGeX2XK7Xj9ksWwei5923QnqIGoEuLlh3O46OW9vc8hQxjzmMTKCgJMVZ5ne5xaWFQYDT2dMpbUhq6hEOhxA== +"@n1ru4l/push-pull-async-iterable-iterator@^2.1.4": + version "2.1.4" + resolved "https://registry.npmjs.org/@n1ru4l/push-pull-async-iterable-iterator/-/push-pull-async-iterable-iterator-2.1.4.tgz#a90225474352f9f159bff979905f707b9c6bcf04" + integrity sha512-qLIvoOUJ+zritv+BlzcBMePKNjKQzH9Rb2i9W98YXxf/M62Lye8qH0peyiU8yJ1tL0kfulWi31BoK10E6BKJeA== "@nodelib/fs.scandir@2.1.3": version "2.1.3" @@ -12528,7 +12529,7 @@ downshift@^6.0.15: prop-types "^15.7.2" react-is "^17.0.2" -dset@^3.0.0: +dset@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/dset/-/dset-3.1.0.tgz#23feb6df93816ea452566308b1374d6e869b0d7b" integrity sha512-7xTQ5DzyE59Nn+7ZgXDXjKAGSGmXZHqttMVVz1r4QNfmGpyj+cm2YtI3II0c/+4zS4a9yq2mBhgdeq2QnpcYlw== @@ -14893,15 +14894,15 @@ grapheme-splitter@^1.0.4: integrity sha512-bzh50DW9kTPM00T8y4o8vQg89Di9oLJVLW/KaOGIXJWP/iqCN6WKYkbNOF04vFLJhwcpYUh9ydh/+5vpOqV4YQ== graphiql@^1.0.0-alpha.10: - version "1.4.0" - resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.4.0.tgz#8b17c988720a9da5ea3ebf7ff9d86939b462655e" - integrity sha512-E/Xzfu3YnifdINrK6dKHD1G0qnWkYYK2gHQ//vApUadMb4I4lZQ399ZKt5nqM5kzrATf5FDUTuSpnkaeSoARkQ== + version "1.4.2" + resolved "https://registry.npmjs.org/graphiql/-/graphiql-1.4.2.tgz#a1dc1a4d8d35f60c90d6d8a9eb62a99756e9fd9b" + integrity sha512-TQDuuU/ZqTWV1yQDpVEiKskg0IYA+Wck37DYrrFzLlpgZWRbWiyab1PyHKiRep7J540CgScBg6C/gGCymKyO3g== dependencies: - "@graphiql/toolkit" "^0.1.0" + "@graphiql/toolkit" "^0.2.0" codemirror "^5.54.0" codemirror-graphql "^1.0.0" copy-to-clipboard "^3.2.0" - dset "^3.0.0" + dset "^3.1.0" entities "^2.0.0" graphql-language-service "^3.1.2" markdown-it "^10.0.0" @@ -15030,7 +15031,12 @@ graphql-type-json@^0.3.2: resolved "https://registry.npmjs.org/graphql-type-json/-/graphql-type-json-0.3.2.tgz#f53a851dbfe07bd1c8157d24150064baab41e115" integrity sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg== -graphql-ws@^4.1.0, graphql-ws@^4.4.1: +graphql-ws@^4.3.2: + version "4.9.0" + resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.9.0.tgz#5cfd8bb490b35e86583d8322f5d5d099c26e365c" + integrity sha512-sHkK9+lUm20/BGawNEWNtVAeJzhZeBg21VmvmLoT5NdGVeZWv5PdIhkcayQIAgjSyyQ17WMKmbDijIPG2On+Ag== + +graphql-ws@^4.4.1: version "4.7.0" resolved "https://registry.npmjs.org/graphql-ws/-/graphql-ws-4.7.0.tgz#b323fbf35a3736eed85dac24c0054d6d10c93e62" integrity sha512-Md8SsmC9ZlsogFPd3Ot8HbIAAqsHh8Xoq7j4AmcIat1Bh6k91tjVyQvA0Au1/BolXSYq+RDvib6rATU2Hcf1Xw== @@ -19183,7 +19189,7 @@ merge@^2.1.0: resolved "https://registry.npmjs.org/merge/-/merge-2.1.1.tgz#59ef4bf7e0b3e879186436e8481c06a6c162ca98" integrity sha512-jz+Cfrg9GWOZbQAnDQ4hlVnQky+341Yk5ru8bZSe6sIDTCIg8n9i/u7hSQGSVOF3C7lH6mGtqjkiT9G4wFLL0w== -meros@1.1.4, meros@^1.1.2: +meros@1.1.4, meros@^1.1.4: version "1.1.4" resolved "https://registry.npmjs.org/meros/-/meros-1.1.4.tgz#c17994d3133db8b23807f62bec7f0cb276cfd948" integrity sha512-E9ZXfK9iQfG9s73ars9qvvvbSIkJZF5yOo9j4tcwM5tN8mUKfj/EKN5PzOr3ZH0y5wL7dLAHw3RVEfpQV9Q7VQ== From be498d22feb04cf97ed2a29c4a6a36eccfcd062d Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 18 Aug 2021 09:05:14 +0200 Subject: [PATCH 072/102] Pass along a `OrganizationTransformer` to the read step Follow up to #6855 Signed-off-by: Oliver Sand --- .changeset/violet-fishes-look.md | 5 +++++ .../api-report.md | 3 +++ .../src/microsoftGraph/read.ts | 18 +++++++++++++----- .../MicrosoftGraphOrgReaderProcessor.ts | 6 ++++++ 4 files changed, 27 insertions(+), 5 deletions(-) create mode 100644 .changeset/violet-fishes-look.md diff --git a/.changeset/violet-fishes-look.md b/.changeset/violet-fishes-look.md new file mode 100644 index 0000000000..f526638a59 --- /dev/null +++ b/.changeset/violet-fishes-look.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-msgraph': patch +--- + +Pass along a `OrganizationTransformer` to the read step diff --git a/plugins/catalog-backend-module-msgraph/api-report.md b/plugins/catalog-backend-module-msgraph/api-report.md index f614bdca36..7a10ad2995 100644 --- a/plugins/catalog-backend-module-msgraph/api-report.md +++ b/plugins/catalog-backend-module-msgraph/api-report.md @@ -113,6 +113,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { logger: Logger_2; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; }); // (undocumented) static fromConfig( @@ -121,6 +122,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { logger: Logger_2; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; }, ): MicrosoftGraphOrgReaderProcessor; // (undocumented) @@ -174,6 +176,7 @@ export function readMicrosoftGraphOrg( groupFilter?: string; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; logger: Logger_2; }, ): Promise<{ diff --git a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts index 6fbd2cb0de..096cf8a479 100644 --- a/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts +++ b/plugins/catalog-backend-module-msgraph/src/microsoftGraph/read.ts @@ -20,6 +20,7 @@ import { } from '@backstage/catalog-model'; import * as MicrosoftGraph from '@microsoft/microsoft-graph-types'; import limiterFactory from 'p-limit'; +import { Logger } from 'winston'; import { MicrosoftGraphClient } from './client'; import { MICROSOFT_GRAPH_GROUP_ID_ANNOTATION, @@ -33,7 +34,6 @@ import { OrganizationTransformer, UserTransformer, } from './types'; -import { Logger } from 'winston'; export async function defaultUserTransformer( user: MicrosoftGraph.User, @@ -212,7 +212,11 @@ export async function defaultGroupTransformer( export async function readMicrosoftGraphGroups( client: MicrosoftGraphClient, tenantId: string, - options?: { groupFilter?: string; transformer?: GroupTransformer }, + options?: { + groupFilter?: string; + groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; + }, ): Promise<{ groups: GroupEntity[]; // With all relations empty rootGroup: GroupEntity | undefined; // With all relations empty @@ -224,13 +228,15 @@ export async function readMicrosoftGraphGroups( const groupMemberOf: Map> = new Map(); const limiter = limiterFactory(10); - const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId); + const { rootGroup } = await readMicrosoftGraphOrganization(client, tenantId, { + transformer: options?.organizationTransformer, + }); if (rootGroup) { groupMember.set(rootGroup.metadata.name, new Set()); groups.push(rootGroup); } - const transformer = options?.transformer ?? defaultGroupTransformer; + const transformer = options?.groupTransformer ?? defaultGroupTransformer; const promises: Promise[] = []; for await (const group of client.getGroups({ @@ -384,6 +390,7 @@ export async function readMicrosoftGraphOrg( groupFilter?: string; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; logger: Logger; }, ): Promise<{ users: UserEntity[]; groups: GroupEntity[] }> { @@ -395,7 +402,8 @@ export async function readMicrosoftGraphOrg( const { groups, rootGroup, groupMember, groupMemberOf } = await readMicrosoftGraphGroups(client, tenantId, { groupFilter: options?.groupFilter, - transformer: options?.groupTransformer, + groupTransformer: options?.groupTransformer, + organizationTransformer: options?.organizationTransformer, }); resolveRelations(rootGroup, groups, users, groupMember, groupMemberOf); diff --git a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts index 1495bce93c..161cc8799f 100644 --- a/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts +++ b/plugins/catalog-backend-module-msgraph/src/processors/MicrosoftGraphOrgReaderProcessor.ts @@ -26,6 +26,7 @@ import { GroupTransformer, MicrosoftGraphClient, MicrosoftGraphProviderConfig, + OrganizationTransformer, readMicrosoftGraphConfig, readMicrosoftGraphOrg, UserTransformer, @@ -39,6 +40,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { private readonly logger: Logger; private readonly userTransformer?: UserTransformer; private readonly groupTransformer?: GroupTransformer; + private readonly organizationTransformer?: OrganizationTransformer; static fromConfig( config: Config, @@ -46,6 +48,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; }, ) { const c = config.getOptionalConfig('catalog.processors.microsoftGraphOrg'); @@ -60,11 +63,13 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { logger: Logger; userTransformer?: UserTransformer; groupTransformer?: GroupTransformer; + organizationTransformer?: OrganizationTransformer; }) { this.providers = options.providers; this.logger = options.logger; this.userTransformer = options.userTransformer; this.groupTransformer = options.groupTransformer; + this.organizationTransformer = options.organizationTransformer; } async readLocation( @@ -99,6 +104,7 @@ export class MicrosoftGraphOrgReaderProcessor implements CatalogProcessor { groupFilter: provider.groupFilter, userTransformer: this.userTransformer, groupTransformer: this.groupTransformer, + organizationTransformer: this.organizationTransformer, logger: this.logger, }, ); From db83840a65cb93a9c6061ef83dfcd4dcdb911798 Mon Sep 17 00:00:00 2001 From: Johan Haals Date: Wed, 18 Aug 2021 11:06:01 +0200 Subject: [PATCH 073/102] Update .changeset/early-ducks-stare.md Signed-off-by: Johan Haals Co-authored-by: Patrik Oldsberg --- .changeset/early-ducks-stare.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/early-ducks-stare.md b/.changeset/early-ducks-stare.md index 58b1525e25..1fd44f2755 100644 --- a/.changeset/early-ducks-stare.md +++ b/.changeset/early-ducks-stare.md @@ -1,5 +1,5 @@ --- -'@backstage/plugin-catalog-backend': minor +'@backstage/plugin-catalog-backend': patch --- Updates the `DefaultProcessingDatabase` to accept a refresh interval function instead of a fixed refresh interval in seconds which used to default to 100s. The catalog now ships with a default refresh interval function that schedules entities for refresh every 100-150 seconds, this should From ee99798da7aa2c257737af1c3c95dceaa5eb3c4b Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 18 Aug 2021 11:19:25 +0200 Subject: [PATCH 074/102] Correct version requirements on postgres from 11 to 12 (#6863) Signed-off-by: Oliver Sand --- .changeset/quiet-hornets-yawn.md | 6 ++++++ docs/features/search/search-engines.md | 2 +- plugins/search-backend-module-pg/README.md | 2 +- .../src/database/DatabaseDocumentStore.ts | 8 ++++---- 4 files changed, 12 insertions(+), 6 deletions(-) create mode 100644 .changeset/quiet-hornets-yawn.md diff --git a/.changeset/quiet-hornets-yawn.md b/.changeset/quiet-hornets-yawn.md new file mode 100644 index 0000000000..35a0a7706d --- /dev/null +++ b/.changeset/quiet-hornets-yawn.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-search-backend-module-pg': patch +--- + +Correct version requirements on postgres from 11 to 12. Postgres 12 is required +due the use of generated columns. diff --git a/docs/features/search/search-engines.md b/docs/features/search/search-engines.md index 69a223b29e..a0302ce279 100644 --- a/docs/features/search/search-engines.md +++ b/docs/features/search/search-engines.md @@ -43,7 +43,7 @@ provides decent results and performs well with ten thousands of indexed documents. The connection to postgres is established via the database manager also used by other plugins. -> **Important**: The search plugin requires at least Postgres 11! +> **Important**: The search plugin requires at least Postgres 12! To use the `PgSearchEngine`, make sure that you have a Postgres database configured and make the following changes to your backend: diff --git a/plugins/search-backend-module-pg/README.md b/plugins/search-backend-module-pg/README.md index fb9dbccc4a..d3f98132aa 100644 --- a/plugins/search-backend-module-pg/README.md +++ b/plugins/search-backend-module-pg/README.md @@ -8,7 +8,7 @@ well with ten thousands of indexed documents. The connection to postgres is established via the database manager also used by other plugins. -> **Important**: The search plugin requires at least Postgres 11! +> **Important**: The search plugin requires at least Postgres 12! ## Getting started diff --git a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts index ecaa3e2f0f..77f15746a6 100644 --- a/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts +++ b/plugins/search-backend-module-pg/src/database/DatabaseDocumentStore.ts @@ -34,18 +34,18 @@ export class DatabaseDocumentStore implements DatabaseStore { try { const majorVersion = await queryPostgresMajorVersion(knex); - if (majorVersion < 11) { + if (majorVersion < 12) { // We are using some features (like generated columns) that aren't // available in older postgres versions. throw new Error( - `The PgSearchEngine requires at least postgres version 11 (but is running on ${majorVersion})`, + `The PgSearchEngine requires at least postgres version 12 (but is running on ${majorVersion})`, ); } } catch { // Actually both mysql and sqlite have a full text search, too. We could // implement them separately or add them here. throw new Error( - 'The PgSearchEngine is only supported when using a postgres database (>=11.x)', + 'The PgSearchEngine is only supported when using a postgres database (>=12.x)', ); } @@ -59,7 +59,7 @@ export class DatabaseDocumentStore implements DatabaseStore { try { const majorVersion = await queryPostgresMajorVersion(knex); - return majorVersion >= 11; + return majorVersion >= 12; } catch { return false; } From 500abf47ebd30b0f7af80a5b18109c5f3ea162ff Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 18 Aug 2021 13:35:52 +0200 Subject: [PATCH 075/102] marketplace: add firehyrant plugin Signed-off-by: Himanshu Mishra --- microsite/data/plugins/firehyrant.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 microsite/data/plugins/firehyrant.yaml diff --git a/microsite/data/plugins/firehyrant.yaml b/microsite/data/plugins/firehyrant.yaml new file mode 100644 index 0000000000..5faad64d37 --- /dev/null +++ b/microsite/data/plugins/firehyrant.yaml @@ -0,0 +1,9 @@ +--- +title: FireHydrant +author: FireHyrant +authorUrl: https://firehydrant.io/ +category: Incident Management +description: The FireHydrant plugin brings incident management to Backstage, and it displays service incidents information such as active incidents and incident analytics. +documentation: https://github.com/backstage/backstage/blob/master/plugins/firehydrant/README.md +iconUrl: https://raw.githubusercontent.com/backstage/backstage/master/plugins/firehydrant/doc/firehydrant_logo.png +npmPackageName: '@backstage/plugin-firehyrant' From 54b441abe34542d502406990804171620f6a89c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Wed, 18 Aug 2021 15:44:02 +0200 Subject: [PATCH 076/102] Introduce LdapOrgEntityProvider MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- .changeset/fifty-bats-itch.md | 5 + .changeset/rich-paws-flow.md | 5 + .../catalog-backend-module-ldap/api-report.md | 34 ++- .../src/ldap/client.ts | 21 +- .../src/ldap/read.ts | 4 +- .../src/processors/LdapOrgEntityProvider.ts | 204 ++++++++++++++++++ .../src/processors/index.ts | 2 + plugins/catalog-backend/api-report.md | 33 ++- plugins/catalog-backend/src/next/index.ts | 9 +- 9 files changed, 306 insertions(+), 11 deletions(-) create mode 100644 .changeset/fifty-bats-itch.md create mode 100644 .changeset/rich-paws-flow.md create mode 100644 plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts diff --git a/.changeset/fifty-bats-itch.md b/.changeset/fifty-bats-itch.md new file mode 100644 index 0000000000..c8793beb06 --- /dev/null +++ b/.changeset/fifty-bats-itch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend': patch +--- + +Export the entity provider related types for external use. diff --git a/.changeset/rich-paws-flow.md b/.changeset/rich-paws-flow.md new file mode 100644 index 0000000000..73ce4fd34a --- /dev/null +++ b/.changeset/rich-paws-flow.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-backend-module-ldap': minor +--- + +Introduce `LdapOrgEntityProvider` as an alternative to `LdapOrgReaderProcessor`. This also changes the `LdapClient` interface to require a logger. diff --git a/plugins/catalog-backend-module-ldap/api-report.md b/plugins/catalog-backend-module-ldap/api-report.md index 5e713eee09..35f647ec70 100644 --- a/plugins/catalog-backend-module-ldap/api-report.md +++ b/plugins/catalog-backend-module-ldap/api-report.md @@ -7,6 +7,8 @@ import { CatalogProcessor } from '@backstage/plugin-catalog-backend'; import { CatalogProcessorEmit } from '@backstage/plugin-catalog-backend'; import { Client } from 'ldapjs'; import { Config } from '@backstage/config'; +import { EntityProvider } from '@backstage/plugin-catalog-backend'; +import { EntityProviderConnection } from '@backstage/plugin-catalog-backend'; import { GroupEntity } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/config'; import { LocationSpec } from '@backstage/catalog-model'; @@ -87,7 +89,7 @@ export const LDAP_UUID_ANNOTATION = 'backstage.io/ldap-uuid'; // // @public export class LdapClient { - constructor(client: Client); + constructor(client: Client, logger: Logger_2); // Warning: (ae-forgotten-export) The symbol "BindConfig" needs to be exported by the entry point index.d.ts // // (undocumented) @@ -103,6 +105,36 @@ export class LdapClient { search(dn: string, options: SearchOptions): Promise; } +// Warning: (ae-missing-release-tag) "LdapOrgEntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export class LdapOrgEntityProvider implements EntityProvider { + constructor(options: { + id: string; + provider: LdapProviderConfig; + logger: Logger_2; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + }); + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + static fromConfig( + configRoot: Config, + options: { + id: string; + target: string; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + logger: Logger_2; + }, + ): LdapOrgEntityProvider; + // (undocumented) + getProviderName(): string; + // (undocumented) + read(): Promise; +} + // Warning: (ae-missing-release-tag) "LdapOrgReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public diff --git a/plugins/catalog-backend-module-ldap/src/ldap/client.ts b/plugins/catalog-backend-module-ldap/src/ldap/client.ts index a99fc3846e..5418577381 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/client.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/client.ts @@ -47,7 +47,7 @@ export class LdapClient { }); if (!bind) { - return new LdapClient(client); + return new LdapClient(client, logger); } return new Promise((resolve, reject) => { @@ -56,13 +56,16 @@ export class LdapClient { if (err) { reject(`LDAP bind failed for ${dn}, ${errorString(err)}`); } else { - resolve(new LdapClient(client)); + resolve(new LdapClient(client, logger)); } }); }); } - constructor(private readonly client: Client) {} + constructor( + private readonly client: Client, + private readonly logger: Logger, + ) {} /** * Performs an LDAP search operation. @@ -72,9 +75,13 @@ export class LdapClient { */ async search(dn: string, options: SearchOptions): Promise { try { - return await new Promise((resolve, reject) => { - const output: SearchEntry[] = []; + const output: SearchEntry[] = []; + const logInterval = setInterval(() => { + this.logger.debug(`Read ${output.length} LDAP entries so far...`); + }, 5000); + + const search = new Promise((resolve, reject) => { this.client.search(dn, options, (err, res) => { if (err) { reject(new Error(errorString(err))); @@ -104,6 +111,10 @@ export class LdapClient { }); }); }); + + return await search.finally(() => { + clearInterval(logInterval); + }); } catch (e) { throw new Error(`LDAP search at DN "${dn}" failed, ${e.message}`); } diff --git a/plugins/catalog-backend-module-ldap/src/ldap/read.ts b/plugins/catalog-backend-module-ldap/src/ldap/read.ts index f3ee09d32d..77ca43a519 100644 --- a/plugins/catalog-backend-module-ldap/src/ldap/read.ts +++ b/plugins/catalog-backend-module-ldap/src/ldap/read.ts @@ -38,7 +38,7 @@ export async function defaultUserTransformer( const { set, map } = config; const entity: UserEntity = { - apiVersion: 'backstage.io/v1alpha1', + apiVersion: 'backstage.io/v1beta1', kind: 'User', metadata: { name: '', @@ -133,7 +133,7 @@ export async function defaultGroupTransformer( ): Promise { const { set, map } = config; const entity: GroupEntity = { - apiVersion: 'backstage.io/v1alpha1', + apiVersion: 'backstage.io/v1beta1', kind: 'Group', metadata: { name: '', diff --git a/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts new file mode 100644 index 0000000000..c0cd663052 --- /dev/null +++ b/plugins/catalog-backend-module-ldap/src/processors/LdapOrgEntityProvider.ts @@ -0,0 +1,204 @@ +/* + * 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 { + Entity, + LOCATION_ANNOTATION, + ORIGIN_LOCATION_ANNOTATION, +} from '@backstage/catalog-model'; +import { Config } from '@backstage/config'; +import { + EntityProvider, + EntityProviderConnection, +} from '@backstage/plugin-catalog-backend'; +import { merge } from 'lodash'; +import { Logger } from 'winston'; +import { + GroupTransformer, + LdapClient, + LdapProviderConfig, + LDAP_DN_ANNOTATION, + readLdapConfig, + readLdapOrg, + UserTransformer, +} from '../ldap'; + +/** + * Reads user and group entries out of an LDAP service, and provides them as + * User and Group entities for the catalog. + */ +export class LdapOrgEntityProvider implements EntityProvider { + private connection?: EntityProviderConnection; + + static fromConfig( + configRoot: Config, + options: { + /** + * A unique, stable identifier for this provider. + * + * @example "production" + */ + id: string; + /** + * The target that this provider should consume. + * + * Should exactly match the "target" field of one of the "ldap.providers" + * configuration entries. + * + * @example "ldaps://ds-read.example.net" + */ + target: string; + /** + * The function that transforms a user entry in LDAP to an entity. + */ + userTransformer?: UserTransformer; + /** + * The function that transforms a group entry in LDAP to an entity. + */ + groupTransformer?: GroupTransformer; + logger: Logger; + }, + ): LdapOrgEntityProvider { + // TODO(freben): Deprecate the old catalog.processors.ldapOrg config + const config = + configRoot.getOptionalConfig('ldap') || + configRoot.getOptionalConfig('catalog.processors.ldapOrg'); + if (!config) { + throw new TypeError( + `There is no LDAP configuration. Please add it as "ldap.providers".`, + ); + } + + const providers = readLdapConfig(config); + const provider = providers.find(p => options.target === p.target); + if (!provider) { + throw new TypeError( + `There is no LDAP configuration that matches ${options.target}. Please add a configuration entry for it under "ldap.providers".`, + ); + } + + const logger = options.logger.child({ + target: options.target, + }); + + return new LdapOrgEntityProvider({ + id: options.id, + provider, + userTransformer: options.userTransformer, + groupTransformer: options.groupTransformer, + logger, + }); + } + + constructor( + private options: { + id: string; + provider: LdapProviderConfig; + logger: Logger; + userTransformer?: UserTransformer; + groupTransformer?: GroupTransformer; + }, + ) {} + + getProviderName() { + return `LdapOrgEntityProvider:${this.options.id}`; + } + + async connect(connection: EntityProviderConnection) { + this.connection = connection; + } + + async read() { + if (!this.connection) { + throw new Error('Not initialized'); + } + + const { markReadComplete } = trackProgress(this.options.logger); + + // Be lazy and create the client each time; even though it's pretty + // inefficient, we usually only do this once per entire refresh loop and + // don't have to worry about timeouts and reconnects etc. + const client = await LdapClient.create( + this.options.logger, + this.options.provider.target, + this.options.provider.bind, + ); + + const { users, groups } = await readLdapOrg( + client, + this.options.provider.users, + this.options.provider.groups, + { + groupTransformer: this.options.groupTransformer, + userTransformer: this.options.userTransformer, + logger: this.options.logger, + }, + ); + + const { markCommitComplete } = markReadComplete({ users, groups }); + + await this.connection.applyMutation({ + type: 'full', + entities: [...users, ...groups].map(entity => ({ + locationKey: `ldap-org-provider:${this.options.id}`, + entity: withLocations(this.options.id, entity), + })), + }); + + markCommitComplete(); + } +} + +// Helps wrap the timing and logging behaviors +function trackProgress(logger: Logger) { + let timestamp = Date.now(); + let summary: string; + + logger.info('Reading LDAP users and groups'); + + function markReadComplete(read: { users: unknown[]; groups: unknown[] }) { + summary = `${read.users.length} LDAP users and ${read.groups.length} LDAP groups`; + const readDuration = ((Date.now() - timestamp) / 1000).toFixed(1); + timestamp = Date.now(); + logger.info(`Read ${summary} in ${readDuration} seconds. Committing...`); + return { markCommitComplete }; + } + + function markCommitComplete() { + const commitDuration = ((Date.now() - timestamp) / 1000).toFixed(1); + logger.info(`Committed ${summary} in ${commitDuration} seconds.`); + } + + return { markReadComplete }; +} + +// Makes sure that emitted entities have a proper location based on their DN +function withLocations(providerId: string, entity: Entity): Entity { + const dn = + entity.metadata.annotations?.[LDAP_DN_ANNOTATION] || entity.metadata.name; + const location = `ldap://${providerId}/${encodeURIComponent(dn)}`; + return merge( + { + metadata: { + annotations: { + [LOCATION_ANNOTATION]: location, + [ORIGIN_LOCATION_ANNOTATION]: location, + }, + }, + }, + entity, + ) as Entity; +} diff --git a/plugins/catalog-backend-module-ldap/src/processors/index.ts b/plugins/catalog-backend-module-ldap/src/processors/index.ts index 6e3f413085..96e1a49cbb 100644 --- a/plugins/catalog-backend-module-ldap/src/processors/index.ts +++ b/plugins/catalog-backend-module-ldap/src/processors/index.ts @@ -13,4 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +export { LdapOrgEntityProvider } from './LdapOrgEntityProvider'; export { LdapOrgReaderProcessor } from './LdapOrgReaderProcessor'; diff --git a/plugins/catalog-backend/api-report.md b/plugins/catalog-backend/api-report.md index a6e67215c9..723ea5c50c 100644 --- a/plugins/catalog-backend/api-report.md +++ b/plugins/catalog-backend/api-report.md @@ -699,6 +699,38 @@ export type EntityProcessingResult = errors: Error[]; }; +// Warning: (ae-missing-release-tag) "EntityProvider" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface EntityProvider { + // (undocumented) + connect(connection: EntityProviderConnection): Promise; + // (undocumented) + getProviderName(): string; +} + +// Warning: (ae-missing-release-tag) "EntityProviderConnection" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export interface EntityProviderConnection { + // (undocumented) + applyMutation(mutation: EntityProviderMutation): Promise; +} + +// Warning: (ae-missing-release-tag) "EntityProviderMutation" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export type EntityProviderMutation = + | { + type: 'full'; + entities: DeferredEntity[]; + } + | { + type: 'delta'; + added: DeferredEntity[]; + removed: DeferredEntity[]; + }; + // Warning: (ae-missing-release-tag) "FileReaderProcessor" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -899,7 +931,6 @@ export class NextCatalogBuilder { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen addEntityPolicy(...policies: EntityPolicy[]): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (ae-forgotten-export) The symbol "EntityProvider" needs to be exported by the entry point index.d.ts addEntityProvider(...providers: EntityProvider[]): NextCatalogBuilder; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen addProcessor(...processors: CatalogProcessor[]): NextCatalogBuilder; diff --git a/plugins/catalog-backend/src/next/index.ts b/plugins/catalog-backend/src/next/index.ts index a27174c415..2afdff63fb 100644 --- a/plugins/catalog-backend/src/next/index.ts +++ b/plugins/catalog-backend/src/next/index.ts @@ -17,6 +17,11 @@ export { NextCatalogBuilder } from './NextCatalogBuilder'; export { createNextRouter } from './NextRouter'; export * from './processing'; -export * from './stitching'; -export type { RefreshIntervalFunction } from './refresh'; export { createRandomRefreshInterval } from './refresh'; +export type { RefreshIntervalFunction } from './refresh'; +export * from './stitching'; +export type { + EntityProvider, + EntityProviderConnection, + EntityProviderMutation, +} from './types'; From 6b1afe8c005141ed089b8b6ee86dbdf4422820fc Mon Sep 17 00:00:00 2001 From: Benjamin Date: Tue, 10 Aug 2021 12:56:25 -0400 Subject: [PATCH 077/102] Add configurable gradient property to backstage theme Signed-off-by: Benjamin --- .changeset/witty-wombats-provide.md | 6 ++++++ packages/core-components/api-report.md | 1 + .../core-components/src/layout/ItemCard/ItemCardHeader.tsx | 6 +++--- packages/theme/src/themes.ts | 6 ++++++ packages/theme/src/types.ts | 3 +++ 5 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 .changeset/witty-wombats-provide.md diff --git a/.changeset/witty-wombats-provide.md b/.changeset/witty-wombats-provide.md new file mode 100644 index 0000000000..1dad4be781 --- /dev/null +++ b/.changeset/witty-wombats-provide.md @@ -0,0 +1,6 @@ +--- +'@backstage/core-components': patch +'@backstage/theme': patch +--- + +Add a configurable `palette.bursts.gradient` property to the Backstage theme, to support customizing the gradients in the `ItemCard` header. diff --git a/packages/core-components/api-report.md b/packages/core-components/api-report.md index 42404fb0c0..b4402f67ac 100644 --- a/packages/core-components/api-report.md +++ b/packages/core-components/api-report.md @@ -28,6 +28,7 @@ import { LinkProps as LinkProps_2 } from '@material-ui/core'; import { LinkProps as LinkProps_3 } from 'react-router-dom'; import { MaterialTableProps } from '@material-table/core'; import { NavLinkProps } from 'react-router-dom'; +import { Palette } from '@material-ui/core/styles/createPalette'; import { ProfileInfoApi } from '@backstage/core-plugin-api'; import { PropsWithChildren } from 'react'; import PropTypes from 'prop-types'; diff --git a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx index 6f8b17701d..58c7c1501d 100644 --- a/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx +++ b/packages/core-components/src/layout/ItemCard/ItemCardHeader.tsx @@ -17,18 +17,18 @@ import { createStyles, makeStyles, - Theme, Typography, WithStyles, } from '@material-ui/core'; import React from 'react'; +import { BackstageTheme } from '../../../../theme/src'; -const styles = (theme: Theme) => +const styles = (theme: BackstageTheme) => createStyles({ root: { color: theme.palette.common.white, padding: theme.spacing(2, 2, 3), - backgroundImage: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', + backgroundImage: theme.palette.bursts.gradient.linear, backgroundPosition: 0, backgroundSize: 'inherit', }, diff --git a/packages/theme/src/themes.ts b/packages/theme/src/themes.ts index b31786aae9..d7a4a6d785 100644 --- a/packages/theme/src/themes.ts +++ b/packages/theme/src/themes.ts @@ -38,6 +38,9 @@ export const lightTheme = createTheme({ backgroundColor: { default: '#7C3699', }, + gradient: { + linear: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', + }, }, primary: { main: '#2E77D0', @@ -100,6 +103,9 @@ export const darkTheme = createTheme({ backgroundColor: { default: '#7C3699', }, + gradient: { + linear: 'linear-gradient(-137deg, #4BB8A5 0%, #187656 100%)', + }, }, primary: { main: '#9CC9FF', diff --git a/packages/theme/src/types.ts b/packages/theme/src/types.ts index c2758c29e7..76a1b7eb5c 100644 --- a/packages/theme/src/types.ts +++ b/packages/theme/src/types.ts @@ -58,6 +58,9 @@ type PaletteAdditions = { backgroundColor: { default: string; }; + gradient: { + linear: string; + }; }; pinSidebarButton: { icon: string; From e8bc9510752c9a34a980adb4dc3059984e23b262 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Wed, 18 Aug 2021 09:09:30 -0600 Subject: [PATCH 078/102] Minor identity-resolver doc correction Signed-off-by: Tim Hansen --- docs/auth/identity-resolver.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/auth/identity-resolver.md b/docs/auth/identity-resolver.md index 7ec6ec7117..2c36ddcd7f 100644 --- a/docs/auth/identity-resolver.md +++ b/docs/auth/identity-resolver.md @@ -128,20 +128,20 @@ export default async function createPlugin({ google: createGoogleProvider({ signIn: { resolver: async ({ profile: { email } }, ctx) => { - const [sub] = email?.split('@') ?? ''; + const [id] = email?.split('@') ?? ''; // Fetch from an external system that returns entity claims like: // ['user:default/breanna.davison', ...] const ent = await externalSystemClient.getUsernames(email); // Resolve group membership from the Backstage catalog const fullEnt = await ctx.catalogIdentityClient.resolveCatalogMembership({ - entityRefs: [sub].concat(ent), + entityRefs: [id].concat(ent), logger: ctx.logger, }); const token = await ctx.tokenIssuer.issueToken({ - claims: { sub, ent: fullEnt }, + claims: { sub: id, ent: fullEnt }, }); - return { sub, token }; + return { id, token }; }, }, }), From d622cfad17cda9e11e2ea173ecc249c89524ac91 Mon Sep 17 00:00:00 2001 From: "@pawelmitka" Date: Wed, 18 Aug 2021 17:02:30 +0200 Subject: [PATCH 079/102] feat(plugin-scaffolder-backend): provide option to require Code Owners in GitHub In order to make creation of a new repository more powerful option to enable 'Require review from Code Owners' has been added. The default behavior is that this option is disabled as with the current behavior. However, adding `requireCodeOwnersReview: true` in context input enables it. Signed-off-by: @pawelmitka --- .changeset/three-eggs-punch.md | 5 ++ .../src/scaffolder/actions/builtin/helpers.ts | 7 ++- .../actions/builtin/publish/github.test.ts | 62 ++++++++++++++++++- .../actions/builtin/publish/github.ts | 8 +++ 4 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 .changeset/three-eggs-punch.md diff --git a/.changeset/three-eggs-punch.md b/.changeset/three-eggs-punch.md new file mode 100644 index 0000000000..28dbd2439d --- /dev/null +++ b/.changeset/three-eggs-punch.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +GitHub branch protection option 'Require review from Code Owners' can be enabled by adding `requireCodeOwnersReview: true` in context input. diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts index 7a10046fd0..c1ad3adf3e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/helpers.ts @@ -124,6 +124,7 @@ type BranchProtectionOptions = { owner: string; repoName: string; logger: Logger; + requireCodeOwnerReviews: boolean; defaultBranch?: string; }; @@ -132,6 +133,7 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ client, owner, logger, + requireCodeOwnerReviews, defaultBranch = 'master', }: BranchProtectionOptions): Promise => { const tryOnce = async () => { @@ -153,7 +155,10 @@ export const enableBranchProtectionOnDefaultRepoBranch = async ({ required_status_checks: { strict: true, contexts: [] }, restrictions: null, enforce_admins: true, - required_pull_request_reviews: { required_approving_review_count: 1 }, + required_pull_request_reviews: { + required_approving_review_count: 1, + require_code_owner_reviews: requireCodeOwnerReviews, + }, }); } catch (e) { if ( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts index 5e948b52b1..4461d227e8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.test.ts @@ -22,7 +22,10 @@ import { ScmIntegrations } from '@backstage/integration'; import { ConfigReader } from '@backstage/config'; import { getVoidLogger } from '@backstage/backend-common'; import { PassThrough } from 'stream'; -import { initRepoAndPush } from '../helpers'; +import { + enableBranchProtectionOnDefaultRepoBranch, + initRepoAndPush, +} from '../helpers'; import { when } from 'jest-when'; describe('publish:github', () => { @@ -583,4 +586,61 @@ describe('publish:github', () => { 'https://github.com/html/url/blob/main', ); }); + + it('should call enableBranchProtectionOnDefaultRepoBranch with the correct values of requireCodeOwnerReviews', async () => { + mockGithubClient.users.getByUsername.mockResolvedValue({ + data: { type: 'User' }, + }); + + mockGithubClient.repos.createForAuthenticatedUser.mockResolvedValue({ + data: { + name: 'repository', + }, + }); + + await action.handler(mockContext); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockGithubClient, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requireCodeOwnerReviews: true, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockGithubClient, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: true, + }); + + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + requireCodeOwnerReviews: false, + }, + }); + + expect(enableBranchProtectionOnDefaultRepoBranch).toHaveBeenCalledWith({ + owner: 'owner', + client: mockGithubClient, + repoName: 'repository', + logger: mockContext.logger, + defaultBranch: 'master', + requireCodeOwnerReviews: false, + }); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts index f767d6d38c..5a358822af 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/github.ts @@ -49,6 +49,7 @@ export function createPublishGithubAction(options: { access?: string; defaultBranch?: string; sourcePath?: string; + requireCodeOwnerReviews?: boolean; repoVisibility: 'private' | 'internal' | 'public'; collaborators: Collaborator[]; topics?: string[]; @@ -75,6 +76,11 @@ export function createPublishGithubAction(options: { description: `Sets an admin collaborator on the repository. Can either be a user reference different from 'owner' in 'repoUrl' or team reference, eg. 'org/team-name'`, type: 'string', }, + requireCodeOwnerReviews: { + title: + 'Require an approved review in PR including files with a designated Code Owner', + type: 'boolean', + }, repoVisibility: { title: 'Repository Visibility', type: 'string', @@ -138,6 +144,7 @@ export function createPublishGithubAction(options: { repoUrl, description, access, + requireCodeOwnerReviews = false, repoVisibility = 'private', defaultBranch = 'master', collaborators, @@ -283,6 +290,7 @@ export function createPublishGithubAction(options: { repoName: newRepo.name, logger: ctx.logger, defaultBranch, + requireCodeOwnerReviews, }); } catch (e) { ctx.logger.warn( From a96793a1fa64961d45f76244d4c17c7641201619 Mon Sep 17 00:00:00 2001 From: Tim Hansen Date: Wed, 18 Aug 2021 09:13:31 -0600 Subject: [PATCH 080/102] Fix incorrect links key Signed-off-by: Tim Hansen --- .../software-templates/migrating-from-v1alpha1-to-v1beta2.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md b/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md index 25ff2a2f3a..28001cce0b 100644 --- a/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md +++ b/docs/features/software-templates/migrating-from-v1alpha1-to-v1beta2.md @@ -325,7 +325,7 @@ spec: output: links: - url: '{{steps.publish.output.remoteUrl}}' - text: 'Go to Repo' + title: 'Go to Repo' ``` ## Questions? From c409a0cbe8beda7bb73a3b41748117cc8f32aa89 Mon Sep 17 00:00:00 2001 From: Himanshu Mishra Date: Wed, 18 Aug 2021 17:20:44 +0200 Subject: [PATCH 081/102] firehyrant -> firehyrant Signed-off-by: Himanshu Mishra --- microsite/data/plugins/{firehyrant.yaml => firehydrant.yaml} | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) rename microsite/data/plugins/{firehyrant.yaml => firehydrant.yaml} (87%) diff --git a/microsite/data/plugins/firehyrant.yaml b/microsite/data/plugins/firehydrant.yaml similarity index 87% rename from microsite/data/plugins/firehyrant.yaml rename to microsite/data/plugins/firehydrant.yaml index 5faad64d37..e1fe95b423 100644 --- a/microsite/data/plugins/firehyrant.yaml +++ b/microsite/data/plugins/firehydrant.yaml @@ -1,9 +1,9 @@ --- title: FireHydrant -author: FireHyrant +author: FireHydrant authorUrl: https://firehydrant.io/ category: Incident Management description: The FireHydrant plugin brings incident management to Backstage, and it displays service incidents information such as active incidents and incident analytics. documentation: https://github.com/backstage/backstage/blob/master/plugins/firehydrant/README.md iconUrl: https://raw.githubusercontent.com/backstage/backstage/master/plugins/firehydrant/doc/firehydrant_logo.png -npmPackageName: '@backstage/plugin-firehyrant' +npmPackageName: '@backstage/plugin-firehydrant' From 3bc009287c1236c2e79513222a9f8a5858098210 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 18 Aug 2021 18:48:46 +0200 Subject: [PATCH 082/102] catalog-plugin-react: clarified messaging around deleting entities from configured locations Signed-off-by: Patrik Oldsberg --- .changeset/grumpy-scissors-refuse.md | 5 +++++ .../UnregisterEntityDialog/UnregisterEntityDialog.tsx | 4 ++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 .changeset/grumpy-scissors-refuse.md diff --git a/.changeset/grumpy-scissors-refuse.md b/.changeset/grumpy-scissors-refuse.md new file mode 100644 index 0000000000..f1a59f4846 --- /dev/null +++ b/.changeset/grumpy-scissors-refuse.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Clarified messaging around configured locations in the `UnregisterEntityDialog`. diff --git a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx index 1f1f01ff48..4dce00e7b7 100644 --- a/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx +++ b/plugins/catalog-react/src/components/UnregisterEntityDialog/UnregisterEntityDialog.tsx @@ -155,8 +155,8 @@ const Contents = ({ return ( <> - This entity does not seem to originate from a location. You therefore - only have the option to delete it outright from the catalog. + This entity does not seem to originate from a registered location. You + therefore only have the option to delete it outright from the catalog.