From 95f86e74825f16699d244dbdbfa4a80be5f77bdb Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Mon, 25 May 2020 16:05:35 +0200 Subject: [PATCH 01/58] Implemented refresh endpoint for google provider --- plugins/auth-backend/package.json | 14 ++++-- .../src/providers/google/provider.ts | 44 ++++++++++++++++++- plugins/auth-backend/src/providers/index.ts | 2 +- plugins/auth-backend/src/service/router.ts | 7 +++ yarn.lock | 40 ++++++++++++++--- 5 files changed, 94 insertions(+), 13 deletions(-) diff --git a/plugins/auth-backend/package.json b/plugins/auth-backend/package.json index 6b85bdfe1c..fe82e544e7 100644 --- a/plugins/auth-backend/package.json +++ b/plugins/auth-backend/package.json @@ -25,10 +25,16 @@ "morgan": "^1.10.0", "winston": "^3.2.1", "yn": "^4.0.0", - "passport": "0.4.1", - "passport-google-oauth20": "2.0.0", - "@types/passport": "1.0.3", - "@types/passport-google-oauth20": "2.0.3" + "passport": "^0.4.1", + "passport-google-oauth20": "^2.0.0", + "passport-oauth2-refresh": "^2.0.0", + "passport-oauth2": "^1.5.0", + "cookie-parser": "^1.4.5", + "@types/passport-oauth2-refresh": "^1.1.1", + "@types/passport": "^1.0.3", + "@types/passport-google-oauth20": "^2.0.3", + "@types/cookie-parser": "^1.4.2", + "@types/passport-oauth2": "^1.4.9" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 0ef7e19d3e..03f6424296 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -15,8 +15,9 @@ */ import passport from 'passport'; -import express from 'express'; +import express, { CookieOptions } from 'express'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; +import refresh from 'passport-oauth2-refresh'; import { AuthProvider, AuthProviderRouteHandlers, @@ -25,6 +26,7 @@ import { import { postMessageResponse } from './../utils'; import { InputError } from '@backstage/backend-common'; +const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export class GoogleAuthProvider implements AuthProvider, AuthProviderRouteHandlers { private readonly providerConfig: AuthProviderConfig; @@ -54,6 +56,18 @@ export class GoogleAuthProvider next: express.NextFunction, ) { return passport.authenticate('google', (_, user) => { + const { refreshToken } = user; + delete user.refreshToken; + + const options: CookieOptions = { + maxAge: THOUSAND_DAYS_MS, + secure: false, + sameSite: 'none', + path: 'localhost:3000/auth/google', + httpOnly: true, + }; + + res.cookie('grtoken', refreshToken, options); postMessageResponse(res, { type: 'auth-result', payload: user, @@ -62,7 +76,33 @@ export class GoogleAuthProvider } async logout(_req: express.Request, res: express.Response) { - res.send('logout!'); + return res.send('logout!'); + } + + async refresh(req: express.Request, res: express.Response) { + const refreshToken = req.cookies['grtoken']; + const scope = req.query.scope?.toString() ?? ''; + const params = scope ? { scope } : {}; + + if (!refreshToken) { + return res.status(401).send('Missing session cookie'); + } + + refresh.requestNewAccessToken( + 'google', + refreshToken, + params, + (err, accessToken) => { + if (err || !accessToken) { + return res.status(401).send('Failed to refresh access token'); + } + + return res.send({ + accessToken, + expiresInSeconds: 36000, + }); + }, + ); } strategy(): passport.Strategy { diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 6658299033..68fe61c7a6 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -24,7 +24,7 @@ export const defaultRouter = (provider: AuthProviderRouteHandlers) => { router.get('/handler/frame', provider.frameHandler); router.get('/logout', provider.logout); if (provider.refresh) { - router.get('/refreshToken', provider.refresh); + router.get('/refresh', provider.refresh); } return router; }; diff --git a/plugins/auth-backend/src/service/router.ts b/plugins/auth-backend/src/service/router.ts index cdde95d9d0..a2e3b3e1db 100644 --- a/plugins/auth-backend/src/service/router.ts +++ b/plugins/auth-backend/src/service/router.ts @@ -17,6 +17,9 @@ import express from 'express'; import Router from 'express-promise-router'; import passport from 'passport'; +import cookieParser from 'cookie-parser'; +import refresh from 'passport-oauth2-refresh'; +import OAuth2Strategy from 'passport-oauth2'; import { Logger } from 'winston'; import { providers } from './../providers/config'; import { makeProvider } from '../providers'; @@ -39,6 +42,9 @@ export async function createRouter( ); logger.info(`Configuring provider: ${providerId}`); passport.use(strategy); + if (strategy instanceof OAuth2Strategy) { + refresh.use(strategy); + } providerRouters[providerId] = providerRouter; } @@ -52,6 +58,7 @@ export async function createRouter( router.use(passport.initialize()); router.use(passport.session()); + router.use(cookieParser()); for (const providerId in providerRouters) { if (providerRouters.hasOwnProperty(providerId)) { diff --git a/yarn.lock b/yarn.lock index 074279db2b..0d49abe417 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3861,6 +3861,13 @@ dependencies: "@types/node" "*" +"@types/cookie-parser@^1.4.2": + version "1.4.2" + resolved "https://registry.npmjs.org/@types/cookie-parser/-/cookie-parser-1.4.2.tgz#e4d5c5ffda82b80672a88a4281aaceefb1bd9df5" + integrity sha512-uwcY8m6SDQqciHsqcKDGbo10GdasYsPCYkH3hVegj9qAah6pX5HivOnOuI3WYmyQMnOATV39zv/Ybs0bC/6iVg== + dependencies: + "@types/express" "*" + "@types/cookiejar@*": version "2.1.1" resolved "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.1.tgz#90b68446364baf9efd8e8349bb36bd3852b75b80" @@ -4181,7 +4188,7 @@ resolved "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz#2f8bb441434d163b35fb8ffdccd7138927ffb8c0" integrity sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA== -"@types/passport-google-oauth20@2.0.3": +"@types/passport-google-oauth20@^2.0.3": version "2.0.3" resolved "https://registry.npmjs.org/@types/passport-google-oauth20/-/passport-google-oauth20-2.0.3.tgz#f554ff6d39f395acff3f1d762e54462194dac8da" integrity sha512-6EUEGzEg4acwowvgR/yVZIj8S2Kkwc6JmlY2/wnM1wJHNz20o7s1TIGrxnah8ymLgJasYDpy95P3TMMqlmetPw== @@ -4190,7 +4197,15 @@ "@types/passport" "*" "@types/passport-oauth2" "*" -"@types/passport-oauth2@*": +"@types/passport-oauth2-refresh@^1.1.1": + version "1.1.1" + resolved "https://registry.npmjs.org/@types/passport-oauth2-refresh/-/passport-oauth2-refresh-1.1.1.tgz#cbe466d4fcac36182fd75bf55279c0b1e953c382" + integrity sha512-Tw0JvfDPv9asgFPACd9oOGCaD/0/Uyi+QF7fmrJC74cJKC6I8N8wwhJJHyfd1N2E/qaLgTh431lhOa9jicpNdg== + dependencies: + "@types/oauth" "*" + "@types/passport-oauth2" "*" + +"@types/passport-oauth2@*", "@types/passport-oauth2@^1.4.9": version "1.4.9" resolved "https://registry.npmjs.org/@types/passport-oauth2/-/passport-oauth2-1.4.9.tgz#134007c4b505a82548c9cb19094c5baeb2205c92" integrity sha512-QP0q+NVQOaIu2r0e10QWkiUA0Ya5mOBHRJN0UrI+LolMLOP1/VN4EVIpJ3xVwFo+xqNFRoFvFwJhBvKnk7kpUA== @@ -4199,7 +4214,7 @@ "@types/oauth" "*" "@types/passport" "*" -"@types/passport@*", "@types/passport@1.0.3": +"@types/passport@*", "@types/passport@^1.0.3": version "1.0.3" resolved "https://registry.npmjs.org/@types/passport/-/passport-1.0.3.tgz#e459ed6c262bf0686684d1b05901be0d0b192a9c" integrity sha512-nyztuxtDPQv9utCzU0qW7Gl8BY2Dn8BKlYAFFyxKipFxjaVd96celbkLCV/tRqqBUZ+JB8If3UfgV8347DTo3Q== @@ -7241,6 +7256,14 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0, convert-source-map@^1.6.0, dependencies: safe-buffer "~5.1.1" +cookie-parser@^1.4.5: + version "1.4.5" + resolved "https://registry.npmjs.org/cookie-parser/-/cookie-parser-1.4.5.tgz#3e572d4b7c0c80f9c61daf604e4336831b5d1d49" + integrity sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw== + dependencies: + cookie "0.4.0" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" @@ -15987,14 +16010,19 @@ pascalcase@^0.1.1: resolved "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz#b363e55e8006ca6fe21784d2db22bd15d7917f14" integrity sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ= -passport-google-oauth20@2.0.0: +passport-google-oauth20@^2.0.0: version "2.0.0" resolved "https://registry.npmjs.org/passport-google-oauth20/-/passport-google-oauth20-2.0.0.tgz#0d241b2d21ebd3dc7f2b60669ec4d587e3a674ef" integrity sha512-KSk6IJ15RoxuGq7D1UKK/8qKhNfzbLeLrG3gkLZ7p4A6DBCcv7xpyQwuXtWdpyR0+E0mwkpjY1VfPOhxQrKzdQ== dependencies: passport-oauth2 "1.x.x" -passport-oauth2@1.x.x: +passport-oauth2-refresh@^2.0.0: + version "2.0.0" + resolved "https://registry.npmjs.org/passport-oauth2-refresh/-/passport-oauth2-refresh-2.0.0.tgz#7b19c77ff3cc000819c69f6ad9e318450f57b85e" + integrity sha512-yXvCB6nem/O+WThhiyI3TlPXpzSGY+9+hy9OTx9QF8e9GInplyRHxHaaOhFylKvnof9UmWHAufQFZk8cO1Fb2g== + +passport-oauth2@1.x.x, passport-oauth2@^1.5.0: version "1.5.0" resolved "https://registry.npmjs.org/passport-oauth2/-/passport-oauth2-1.5.0.tgz#64babbb54ac46a4dcab35e7f266ed5294e3c4108" integrity sha512-kqBt6vR/5VlCK8iCx1/KpY42kQ+NEHZwsSyt4Y6STiNjU+wWICG1i8ucc1FapXDGO15C5O5VZz7+7vRzrDPXXQ== @@ -16010,7 +16038,7 @@ passport-strategy@1.x.x: resolved "https://registry.npmjs.org/passport-strategy/-/passport-strategy-1.0.0.tgz#b5539aa8fc225a3d1ad179476ddf236b440f52e4" integrity sha1-tVOaqPwiWj0a0XlHbd8ja0QPUuQ= -passport@0.4.1: +passport@^0.4.1: version "0.4.1" resolved "https://registry.npmjs.org/passport/-/passport-0.4.1.tgz#941446a21cb92fc688d97a0861c38ce9f738f270" integrity sha512-IxXgZZs8d7uFSt3eqNjM9NQ3g3uQCW5avD8mRNoXV99Yig50vjuaez6dQK2qC0kVWPRTujxY0dWgGfT09adjYg== From 6cd55c3b7b0c554bdbde4b06b9b769514eb02e4c Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Mon, 25 May 2020 16:27:56 +0200 Subject: [PATCH 02/58] Always return scope --- .../auth-backend/src/providers/google/provider.ts | 8 +++++--- plugins/auth-backend/src/providers/types.ts | 15 ++++++++++++--- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 03f6424296..d567e363bc 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -92,14 +92,15 @@ export class GoogleAuthProvider 'google', refreshToken, params, - (err, accessToken) => { + (err, accessToken, _, params) => { if (err || !accessToken) { return res.status(401).send('Failed to refresh access token'); } - return res.send({ accessToken, - expiresInSeconds: 36000, + idToken: params.id_token, + expiresInSeconds: params.expires_in, + scope: params.scope, }); }, ); @@ -121,6 +122,7 @@ export class GoogleAuthProvider idToken: params.id_token, accessToken, refreshToken, + scope: params.scope, }); }, ); diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 4350f36200..36941c6850 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -58,16 +58,25 @@ export type AuthProviderFactory = { new (providerConfig: any): AuthProvider & AuthProviderRouteHandlers; }; -export type AuthInfo = { - profile: passport.Profile; +export type AuthInfoBase = { accessToken: string; + idToken?: string; expiresInSeconds?: number; + scope: string; +}; + +export type AuthInfoWithProfile = AuthInfoBase & { + profile: passport.Profile; +}; + +export type AuthInfoPrivate = AuthInfoWithProfile & { + refreshToken: string; }; export type AuthResponse = | { type: 'auth-result'; - payload: AuthInfo; + payload: AuthInfoBase | AuthInfoWithProfile; } | { type: 'auth-result'; From a739f2da583f5f6d88f58a6b954940699aff45df Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Mon, 25 May 2020 16:51:50 +0200 Subject: [PATCH 03/58] Fixes in response format in frontend and backend apis --- packages/backend/src/index.ts | 6 +++++- .../src/api/apis/implementations/auth/google/GoogleAuth.ts | 4 ++-- .../lib/AuthConnector/DefaultAuthConnector.ts | 2 +- plugins/auth-backend/src/providers/google/provider.ts | 4 +++- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/packages/backend/src/index.ts b/packages/backend/src/index.ts index 7af6e631a8..5b1fc54330 100644 --- a/packages/backend/src/index.ts +++ b/packages/backend/src/index.ts @@ -58,9 +58,13 @@ function createEnv(plugin: string): PluginEnvironment { async function main() { const app = express(); + const corsOptions: cors.CorsOptions = { + origin: 'http://localhost:3000', + credentials: true, + }; app.use(helmet()); - app.use(cors()); + app.use(cors(corsOptions)); app.use(compression()); app.use(express.json()); app.use(requestLoggingHandler()); diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts index d1d5da1a02..9f3bcec762 100644 --- a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts @@ -40,7 +40,7 @@ type CreateOptions = { export type GoogleAuthResponse = { accessToken: string; idToken: string; - scopes: string; + scope: string; expiresInSeconds: number; }; @@ -70,7 +70,7 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi { return { idToken: res.idToken, accessToken: res.accessToken, - scopes: GoogleAuth.normalizeScopes(res.scopes), + scopes: GoogleAuth.normalizeScopes(res.scope), expiresAt: new Date(Date.now() + res.expiresInSeconds * 1000), }; }, diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts index 161015b1a8..ca8a9cd2ae 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts @@ -99,7 +99,7 @@ export class DefaultAuthConnector } async refreshSession(): Promise { - const res = await fetch(this.buildUrl('/token', { optional: true }), { + const res = await fetch(this.buildUrl('/refresh', { optional: true }), { headers: { 'x-requested-with': 'XMLHttpRequest', }, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index d567e363bc..2bfb606495 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -63,7 +63,8 @@ export class GoogleAuthProvider maxAge: THOUSAND_DAYS_MS, secure: false, sameSite: 'none', - path: 'localhost:3000/auth/google', + domain: 'localhost', + path: '/auth/google', httpOnly: true, }; @@ -123,6 +124,7 @@ export class GoogleAuthProvider accessToken, refreshToken, scope: params.scope, + expiresInSeconds: params.expires_in, }); }, ); From 53c1fe0601779139799c71f8ee671b841c26ff0e Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Mon, 25 May 2020 17:46:42 +0200 Subject: [PATCH 04/58] fix lint issues --- .../auth-backend/src/providers/google/provider.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 2bfb606495..4d6183da89 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -81,23 +81,24 @@ export class GoogleAuthProvider } async refresh(req: express.Request, res: express.Response) { - const refreshToken = req.cookies['grtoken']; - const scope = req.query.scope?.toString() ?? ''; - const params = scope ? { scope } : {}; + const refreshToken = req.cookies.grtoken; if (!refreshToken) { return res.status(401).send('Missing session cookie'); } - refresh.requestNewAccessToken( + const scope = req.query.scope?.toString() ?? ''; + const refreshTokenRequestParams = scope ? { scope } : {}; + + return refresh.requestNewAccessToken( 'google', refreshToken, - params, + refreshTokenRequestParams, (err, accessToken, _, params) => { if (err || !accessToken) { return res.status(401).send('Failed to refresh access token'); } - return res.send({ + res.send({ accessToken, idToken: params.id_token, expiresInSeconds: params.expires_in, From a8184250fe3e78c60dcfda55a3eb07da16a0313c Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Mon, 25 May 2020 17:51:27 +0200 Subject: [PATCH 05/58] fix lint issues --- plugins/auth-backend/src/providers/google/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 4d6183da89..88b7345213 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -98,7 +98,7 @@ export class GoogleAuthProvider if (err || !accessToken) { return res.status(401).send('Failed to refresh access token'); } - res.send({ + return res.send({ accessToken, idToken: params.id_token, expiresInSeconds: params.expires_in, From 6d3f745b4b5526895df12c9cb42ad0bdd425b465 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 25 May 2020 18:10:34 +0200 Subject: [PATCH 06/58] packages/app: move AlertDisplay to core --- packages/app/package.json | 1 - packages/app/src/App.tsx | 3 +-- packages/core/package.json | 1 + .../src/components/AlertDisplay/AlertDisplay.tsx | 6 ++---- packages/{app => core}/src/components/AlertDisplay/index.ts | 2 +- packages/core/src/index.ts | 1 + 6 files changed, 6 insertions(+), 8 deletions(-) rename packages/{app => core}/src/components/AlertDisplay/AlertDisplay.tsx (93%) rename packages/{app => core}/src/components/AlertDisplay/index.ts (93%) diff --git a/packages/app/package.json b/packages/app/package.json index 0a3d68603b..a366dcce1a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -18,7 +18,6 @@ "@backstage/plugin-sentry": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "4.0.0-alpha.45", "prop-types": "^15.7.2", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index e29fac8865..327bca48f8 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -14,11 +14,10 @@ * limitations under the License. */ -import { createApp, OAuthRequestDialog } from '@backstage/core'; +import { createApp, AlertDisplay, OAuthRequestDialog } from '@backstage/core'; import React, { FC } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import Root from './components/Root'; -import AlertDisplay from './components/AlertDisplay'; import * as plugins from './plugins'; import apis, { alertApiForwarder } from './apis'; diff --git a/packages/core/package.json b/packages/core/package.json index e4521530b1..b1800f5db3 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -31,6 +31,7 @@ "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", "@types/classnames": "^2.2.9", "@types/google-protobuf": "^3.7.2", "@types/jest": "^25.2.2", diff --git a/packages/app/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx similarity index 93% rename from packages/app/src/components/AlertDisplay/AlertDisplay.tsx rename to packages/core/src/components/AlertDisplay/AlertDisplay.tsx index d9dd0b240b..23d1c7825f 100644 --- a/packages/app/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx @@ -19,14 +19,14 @@ import PropTypes from 'prop-types'; import { Snackbar, IconButton } from '@material-ui/core'; import CloseIcon from '@material-ui/icons/Close'; import { Alert } from '@material-ui/lab'; -import { AlertApiForwarder, AlertMessage } from '@backstage/core'; +import { AlertApiForwarder, AlertMessage } from '../../api'; type Props = { forwarder: AlertApiForwarder; }; // TODO: improve on this and promote to a shared component for use by all apps. -const AlertDisplay: FC = ({ forwarder }) => { +export const AlertDisplay: FC = ({ forwarder }) => { const [messages, setMessages] = useState>([]); useEffect(() => { @@ -73,5 +73,3 @@ const AlertDisplay: FC = ({ forwarder }) => { AlertDisplay.propTypes = { forwarder: PropTypes.instanceOf(AlertApiForwarder).isRequired, }; - -export default AlertDisplay; diff --git a/packages/app/src/components/AlertDisplay/index.ts b/packages/core/src/components/AlertDisplay/index.ts similarity index 93% rename from packages/app/src/components/AlertDisplay/index.ts rename to packages/core/src/components/AlertDisplay/index.ts index b60bc1776b..7bfac0c981 100644 --- a/packages/app/src/components/AlertDisplay/index.ts +++ b/packages/core/src/components/AlertDisplay/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './AlertDisplay'; +export * from './AlertDisplay'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 415f167067..013c80e2fa 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -28,6 +28,7 @@ export { default as InfoCard } from './layout/InfoCard'; export { CardTab, TabbedCard } from './layout/TabbedCard'; export { default as ErrorBoundary } from './layout/ErrorBoundary'; export * from './layout/Sidebar'; +export { AlertDisplay } from './components/AlertDisplay'; export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid'; export { default as ProgressCard } from './components/ProgressBars/ProgressCard'; export { default as CircleProgress } from './components/ProgressBars/CircleProgress'; From 8e4e6d0abaf1ee9a9520790a3ef5ef5c88c2698b Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Mon, 25 May 2020 15:41:52 +0200 Subject: [PATCH 07/58] feature: implement location validation on addLocation --- .../catalog/DatabaseLocationsCatalog.test.ts | 69 +++++++++++++++++++ .../src/catalog/DatabaseLocationsCatalog.ts | 13 ++++ .../ingestion/__mocks__/LocationReaders.ts | 43 ++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts create mode 100644 plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts new file mode 100644 index 0000000000..9692d55150 --- /dev/null +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; +jest.mock('../ingestion/LocationReaders'); + +import knex from 'knex'; +import path from 'path'; + +import { Database } from '../database'; + +describe('DatabaseLocationsCatalog', () => { + const database = knex({ + client: 'sqlite3', + connection: ':memory:', + useNullAsDefault: true, + }); + database.client.pool.on('createSuccess', (_eventId: any, resource: any) => { + resource.run('PRAGMA foreign_keys = ON', () => {}); + }); + let db: Database; + let catalog: DatabaseLocationsCatalog; + + beforeEach(async () => { + await database.migrate.latest({ + directory: path.resolve(__dirname, '../database/migrations'), + loadExtensions: ['.ts'], + }); + db = new Database(database); + catalog = new DatabaseLocationsCatalog(db); + }); + it('resolves to location with id', async () => { + return expect( + catalog.addLocation({ type: 'valid_type', target: 'valid_target' }), + ).resolves.toEqual({ + id: expect.anything(), + type: 'valid_type', + target: 'valid_target', + }); + }); + it('rejects for invalid type', async () => { + const type = 'invalid_type'; + return expect( + catalog.addLocation({ type, target: 'valid_target' }), + ).rejects.toEqual(new Error(`Unknown location type ${type}`)); + }); + it('rejects for unreadable target ', async () => { + const target = 'invalid_target'; + return expect( + catalog.addLocation({ type: 'valid_type', target }), + ).rejects.toEqual( + new Error( + `Can't read location at ${target} with error: Something is broken`, + ), + ); + }); +}); diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index e9f1512074..4b2a3a8006 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -16,11 +16,24 @@ import { Database } from '../database'; import { AddLocation, Location, LocationsCatalog } from './types'; +import { LocationReaders } from '../ingestion'; export class DatabaseLocationsCatalog implements LocationsCatalog { constructor(private readonly database: Database) {} async addLocation(location: AddLocation): Promise { + const outputs = await LocationReaders.create().read( + location.type, + location.target, + ); + outputs.forEach(output => { + if (output.type === 'error') { + throw new Error( + `Can't read location at ${location.target} with error: ${output.error.message}`, + ); + } + }); + const added = await this.database.addLocation(location); return added; } diff --git a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts new file mode 100644 index 0000000000..1bfa815280 --- /dev/null +++ b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts @@ -0,0 +1,43 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { LocationSource } from '../sources/types'; +import { LocationReader, ReaderOutput } from '../types'; + +export class LocationReaders implements LocationReader { + static create(): LocationReader { + return { + read: (type, target) => { + if (type !== 'valid_type') { + throw new Error(`Unknown location type ${type}`); + } + if (target === 'valid_target') { + return Promise.resolve([{ type: 'data', data: {} }]); + } + throw new Error( + `Can't read location at ${target} with error: Something is broken`, + ); + }, + }; + } + + constructor(private readonly sources: Record) {} + + // eslint-disable-next-line + async read(type: string, target: string): Promise { + return []; + } +} From dfcad875179a18ce3caa6e65dfdc373e06b9937c Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Mon, 25 May 2020 18:37:47 +0200 Subject: [PATCH 08/58] fix: tests after merge --- .../src/catalog/DatabaseLocationsCatalog.test.ts | 3 ++- .../catalog-backend/src/ingestion/__mocks__/LocationReaders.ts | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index 9692d55150..af1e1fdf2f 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -20,6 +20,7 @@ import knex from 'knex'; import path from 'path'; import { Database } from '../database'; +import { getVoidLogger } from '../../../../packages/backend-common/src/logging/voidLogger'; describe('DatabaseLocationsCatalog', () => { const database = knex({ @@ -38,7 +39,7 @@ describe('DatabaseLocationsCatalog', () => { directory: path.resolve(__dirname, '../database/migrations'), loadExtensions: ['.ts'], }); - db = new Database(database); + db = new Database(database, getVoidLogger()); catalog = new DatabaseLocationsCatalog(db); }); it('resolves to location with id', async () => { diff --git a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts index 1bfa815280..c6ac92ed0f 100644 --- a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts @@ -14,8 +14,7 @@ * limitations under the License. */ -import { LocationSource } from '../sources/types'; -import { LocationReader, ReaderOutput } from '../types'; +import { LocationSource, LocationReader, ReaderOutput } from '../types'; export class LocationReaders implements LocationReader { static create(): LocationReader { From 9be7870a34459e4af77c4c84ff5e21af98154bf2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 25 May 2020 21:51:06 +0200 Subject: [PATCH 09/58] packages/core: add consumer side to AlertApi and use for AlertDialog + friends --- packages/app/src/App.tsx | 4 +-- .../core/src/api/apis/definitions/AlertApi.ts | 6 +++++ .../apis/implementations/AlertApiForwarder.ts | 17 +++++------- .../components/AlertDisplay/AlertDisplay.tsx | 26 +++++++++---------- 4 files changed, 26 insertions(+), 27 deletions(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 327bca48f8..b4d01e067b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -19,7 +19,7 @@ import React, { FC } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import Root from './components/Root'; import * as plugins from './plugins'; -import apis, { alertApiForwarder } from './apis'; +import apis from './apis'; const app = createApp({ apis, @@ -31,7 +31,7 @@ const AppComponent = app.getRootComponent(); const App: FC<{}> = () => ( - + diff --git a/packages/core/src/api/apis/definitions/AlertApi.ts b/packages/core/src/api/apis/definitions/AlertApi.ts index 9776a96d84..123cd47651 100644 --- a/packages/core/src/api/apis/definitions/AlertApi.ts +++ b/packages/core/src/api/apis/definitions/AlertApi.ts @@ -14,6 +14,7 @@ * limitations under the License. */ import { createApiRef } from '../ApiRef'; +import { Observable } from '../../types'; export type AlertMessage = { message: string; @@ -30,6 +31,11 @@ export type AlertApi = { * Post an alert for handling by the application. */ post(alert: AlertMessage): void; + + /** + * Observe alerts posted by other parts of the application. + */ + alert$(): Observable; }; export const alertApiRef = createApiRef({ diff --git a/packages/core/src/api/apis/implementations/AlertApiForwarder.ts b/packages/core/src/api/apis/implementations/AlertApiForwarder.ts index 349512c0f9..c95408283c 100644 --- a/packages/core/src/api/apis/implementations/AlertApiForwarder.ts +++ b/packages/core/src/api/apis/implementations/AlertApiForwarder.ts @@ -14,22 +14,17 @@ * limitations under the License. */ import { AlertApi, AlertMessage } from '../../../'; - -type SubscriberFunc = (message: AlertMessage) => void; -type Unsubscribe = () => void; +import { PublishSubject } from './lib'; +import { Observable } from '../../types'; export class AlertApiForwarder implements AlertApi { - private readonly subscribers = new Set(); + private readonly subject = new PublishSubject(); post(alert: AlertMessage) { - this.subscribers.forEach(subscriber => subscriber(alert)); + this.subject.next(alert); } - subscribe(func: SubscriberFunc): Unsubscribe { - this.subscribers.add(func); - - return () => { - this.subscribers.delete(func); - }; + alert$(): Observable { + return this.subject; } } diff --git a/packages/core/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx index 23d1c7825f..85b41f2e0a 100644 --- a/packages/core/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx @@ -15,25 +15,27 @@ */ import React, { FC, useEffect, useState } from 'react'; -import PropTypes from 'prop-types'; import { Snackbar, IconButton } from '@material-ui/core'; import CloseIcon from '@material-ui/icons/Close'; import { Alert } from '@material-ui/lab'; -import { AlertApiForwarder, AlertMessage } from '../../api'; +import { AlertMessage, useApi, alertApiRef } from '../../api'; -type Props = { - forwarder: AlertApiForwarder; -}; +type Props = {}; // TODO: improve on this and promote to a shared component for use by all apps. -export const AlertDisplay: FC = ({ forwarder }) => { +export const AlertDisplay: FC = () => { const [messages, setMessages] = useState>([]); + const alertApi = useApi(alertApiRef); useEffect(() => { - return forwarder.subscribe((message: AlertMessage) => - setMessages(msgs => msgs.concat(message)), - ); - }, [forwarder]); + const subscription = alertApi + .alert$() + .subscribe(message => setMessages(msgs => msgs.concat(message))); + + return () => { + subscription.unsubscribe(); + }; + }, [alertApi]); if (messages.length === 0) { return null; @@ -69,7 +71,3 @@ export const AlertDisplay: FC = ({ forwarder }) => { ); }; - -AlertDisplay.propTypes = { - forwarder: PropTypes.instanceOf(AlertApiForwarder).isRequired, -}; From 4e7e0833cc352e852f257cbf14dfcf1630464155 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 25 May 2020 22:01:20 +0200 Subject: [PATCH 10/58] packages/core: add observable consumer to ErrorApi --- .../core/src/api/apis/definitions/ErrorApi.ts | 6 ++++ .../apis/implementations/ErrorApiForwarder.ts | 30 ++++++++----------- .../CopyTextButton/CopyTextButton.test.tsx | 1 + .../src/components/CreateAudit/index.test.tsx | 2 +- 4 files changed, 20 insertions(+), 19 deletions(-) diff --git a/packages/core/src/api/apis/definitions/ErrorApi.ts b/packages/core/src/api/apis/definitions/ErrorApi.ts index c481547459..7f9676ea5a 100644 --- a/packages/core/src/api/apis/definitions/ErrorApi.ts +++ b/packages/core/src/api/apis/definitions/ErrorApi.ts @@ -15,6 +15,7 @@ */ import { createApiRef } from '../ApiRef'; +import { Observable } from '../../types'; /** * Mirrors the javascript Error class, for the purpose of @@ -54,6 +55,11 @@ export type ErrorApi = { * Post an error for handling by the application. */ post(error: Error, context?: ErrorContext): void; + + /** + * Observe errors posted by other parts of the application. + */ + error$(): Observable<{ error: Error; context?: ErrorContext }>; }; export const errorApiRef = createApiRef({ diff --git a/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts b/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts index 8d777ac18f..79397c0a67 100644 --- a/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts +++ b/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts @@ -14,32 +14,26 @@ * limitations under the License. */ import { ErrorApi, ErrorContext, AlertApi } from '../../../'; - -type SubscriberFunc = (error: Error) => void; -type Unsubscribe = () => void; +import { PublishSubject } from './lib'; +import { Observable } from '../../types'; export class ErrorApiForwarder implements ErrorApi { - private readonly subscribers = new Set(); - private alertApi: AlertApi; + private readonly subject = new PublishSubject<{ + error: Error; + context?: ErrorContext; + }>(); - constructor(alertApi: AlertApi) { - this.alertApi = alertApi; - } + constructor(private readonly alertApi: AlertApi) {} post(error: Error, context?: ErrorContext) { - if (context?.hidden) { - return; + if (!context?.hidden) { + this.alertApi.post({ message: error.message, severity: 'error' }); } - this.alertApi.post({ message: error.message, severity: 'error' }); - this.subscribers.forEach(subscriber => subscriber(error)); + this.subject.next({ error, context }); } - subscribe(func: SubscriberFunc): Unsubscribe { - this.subscribers.add(func); - - return () => { - this.subscribers.delete(func); - }; + error$(): Observable<{ error: Error; context?: ErrorContext }> { + return this.subject; } } diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx index 695a700738..7bc59d1f48 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx @@ -44,6 +44,7 @@ const apiRegistry = ApiRegistry.from([ post(error) { throw error; }, + error$: jest.fn(), } as ErrorApi, ], ]); diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index f47dc3d475..b337698485 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -53,7 +53,7 @@ describe('CreateAudit', () => { let errorApi: ErrorApi; beforeEach(() => { - errorApi = { post: jest.fn() }; + errorApi = { post: jest.fn(), error$: jest.fn() }; apis = ApiRegistry.from([ [lighthouseApiRef, new LighthouseRestApi('http://lighthouse')], [errorApiRef, errorApi], From 5c7d263aa8ba113a3b37110002c3df329fcc3a94 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 25 May 2020 22:07:54 +0200 Subject: [PATCH 11/58] packages/core: split alerting functionality out from ErrorApiForwarder into ErrorAlerter --- docs/getting-started/utility-apis.md | 5 ++- packages/app/src/apis.ts | 7 ++-- .../api/apis/implementations/ErrorAlerter.ts | 39 +++++++++++++++++++ .../apis/implementations/ErrorApiForwarder.ts | 8 +--- .../src/api/apis/implementations/index.ts | 1 + 5 files changed, 47 insertions(+), 13 deletions(-) create mode 100644 packages/core/src/api/apis/implementations/ErrorAlerter.ts diff --git a/docs/getting-started/utility-apis.md b/docs/getting-started/utility-apis.md index 09dc27ce7a..dba48355c7 100644 --- a/docs/getting-started/utility-apis.md +++ b/docs/getting-started/utility-apis.md @@ -55,15 +55,16 @@ import { errorApiRef, AlertApiForwarder, ErrorApiForwarder, + ErrorAlerter, } from '@backstage/core'; const builder = ApiRegistry.builder(); // The alert API is a self-contained implementation that shows alerts to the user. -builder.add(alertApiRef, new AlertApiForwarder()); +const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); // The error API uses the alert API to send error notifications to the user. -builder.add(errorApiRef, new ErrorApiForwarder(alertApiForwarder)); +builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); const app = createApp({ apis: apiBuilder.build(), diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index c11ea24aca..8d76c75d91 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -21,6 +21,7 @@ import { errorApiRef, AlertApiForwarder, ErrorApiForwarder, + ErrorAlerter, featureFlagsApiRef, FeatureFlags, GoogleAuth, @@ -40,11 +41,9 @@ import { CircleCIApi, circleCIApiRef } from '@backstage/plugin-circleci'; const builder = ApiRegistry.builder(); -export const alertApiForwarder = new AlertApiForwarder(); -builder.add(alertApiRef, alertApiForwarder); +const alertApi = builder.add(alertApiRef, new AlertApiForwarder()); -export const errorApiForwarder = new ErrorApiForwarder(alertApiForwarder); -builder.add(errorApiRef, errorApiForwarder); +builder.add(errorApiRef, new ErrorAlerter(alertApi, new ErrorApiForwarder())); builder.add(circleCIApiRef, new CircleCIApi()); builder.add(featureFlagsApiRef, new FeatureFlags()); diff --git a/packages/core/src/api/apis/implementations/ErrorAlerter.ts b/packages/core/src/api/apis/implementations/ErrorAlerter.ts new file mode 100644 index 0000000000..81d0cc8fb8 --- /dev/null +++ b/packages/core/src/api/apis/implementations/ErrorAlerter.ts @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { ErrorApi, ErrorContext, AlertApi } from '../../../'; + +/** + * Decorates an ErrorApi by also forwarding error messages + * to the alertApi with an 'error' severity. + */ +export class ErrorAlerter implements ErrorApi { + constructor( + private readonly alertApi: AlertApi, + private readonly errorApi: ErrorApi, + ) {} + + post(error: Error, context?: ErrorContext) { + if (!context?.hidden) { + this.alertApi.post({ message: error.message, severity: 'error' }); + } + + return this.errorApi.post(error, context); + } + + error$() { + return this.errorApi.error$(); + } +} diff --git a/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts b/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts index 79397c0a67..a61fdae5c1 100644 --- a/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts +++ b/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext, AlertApi } from '../../../'; +import { ErrorApi, ErrorContext } from '../../../'; import { PublishSubject } from './lib'; import { Observable } from '../../types'; @@ -23,13 +23,7 @@ export class ErrorApiForwarder implements ErrorApi { context?: ErrorContext; }>(); - constructor(private readonly alertApi: AlertApi) {} - post(error: Error, context?: ErrorContext) { - if (!context?.hidden) { - this.alertApi.post({ message: error.message, severity: 'error' }); - } - this.subject.next({ error, context }); } diff --git a/packages/core/src/api/apis/implementations/index.ts b/packages/core/src/api/apis/implementations/index.ts index 0bf5cbbb24..1f2b91c16d 100644 --- a/packages/core/src/api/apis/implementations/index.ts +++ b/packages/core/src/api/apis/implementations/index.ts @@ -21,5 +21,6 @@ export * from './auth'; export * from './AppThemeSelector'; export * from './AlertApiForwarder'; +export * from './ErrorAlerter'; export * from './ErrorApiForwarder'; export * from './OAuthRequestManager'; From fe08f81359f00ebb8294215f2151609bb543c77f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 25 May 2020 22:15:02 +0200 Subject: [PATCH 12/58] packages/dev-utils: update to reflect changes in core and include proper alert display --- packages/dev-utils/src/devApp/apiFactories.test.ts | 6 ++++-- packages/dev-utils/src/devApp/apiFactories.ts | 14 +++++--------- packages/dev-utils/src/devApp/render.tsx | 2 ++ 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/dev-utils/src/devApp/apiFactories.test.ts b/packages/dev-utils/src/devApp/apiFactories.test.ts index 55455346b9..be3ae2cdbe 100644 --- a/packages/dev-utils/src/devApp/apiFactories.test.ts +++ b/packages/dev-utils/src/devApp/apiFactories.test.ts @@ -15,12 +15,14 @@ */ import * as apiFactories from './apiFactories'; -import { ApiTestRegistry } from '@backstage/core'; +import { ApiTestRegistry, ApiFactory } from '@backstage/core'; describe('apiFactories', () => { it('should be possible to get an instance of each API', () => { const registry = new ApiTestRegistry(); - const factories = Object.values(apiFactories); + const factories: ApiFactory[] = Object.values( + apiFactories, + ); for (const factory of factories) { registry.register(factory); diff --git a/packages/dev-utils/src/devApp/apiFactories.ts b/packages/dev-utils/src/devApp/apiFactories.ts index a03faf0033..162375cf1c 100644 --- a/packages/dev-utils/src/devApp/apiFactories.ts +++ b/packages/dev-utils/src/devApp/apiFactories.ts @@ -20,6 +20,8 @@ import { ErrorApiForwarder, AlertApi, createApiFactory, + ErrorAlerter, + AlertApiForwarder, } from '@backstage/core'; // TODO(rugvip): We should likely figure out how to reuse all of these between apps @@ -30,18 +32,12 @@ import { export const alertApiFactory = createApiFactory({ implements: alertApiRef, deps: {}, - factory: (): AlertApi => ({ - // TODO: Figure out how to ship a nicer implementation without having - // to export any external references. - post(alertConfig) { - // eslint-disable-next-line no-alert - alert(`Alert[${alertConfig.severity}]: ${alertConfig.message}`); - }, - }), + factory: (): AlertApi => new AlertApiForwarder(), }); export const errorApiFactory = createApiFactory({ implements: errorApiRef, deps: { alertApi: alertApiRef }, - factory: ({ alertApi }) => new ErrorApiForwarder(alertApi), + factory: ({ alertApi }) => + new ErrorAlerter(alertApi, new ErrorApiForwarder()), }); diff --git a/packages/dev-utils/src/devApp/render.tsx b/packages/dev-utils/src/devApp/render.tsx index 1922e056a3..72c9581a84 100644 --- a/packages/dev-utils/src/devApp/render.tsx +++ b/packages/dev-utils/src/devApp/render.tsx @@ -29,6 +29,7 @@ import { createPlugin, ApiTestRegistry, ApiHolder, + AlertDisplay, } from '@backstage/core'; import * as defaultApiFactories from './apiFactories'; @@ -77,6 +78,7 @@ class DevAppBuilder { const DevApp: FC<{}> = () => { return ( + {sidebar} From 1da6ac369e8853822fe341672b04da0a919a28d0 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Mon, 25 May 2020 23:44:20 +0200 Subject: [PATCH 13/58] fix tests and add more tests --- .../src/providers/google/provider.test.ts | 206 +++++++++++++++++- .../src/providers/google/provider.ts | 11 +- 2 files changed, 207 insertions(+), 10 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index 845c46a4da..1726fbf33a 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -14,10 +14,11 @@ * limitations under the License. */ -import { GoogleAuthProvider } from './provider'; +import { GoogleAuthProvider, THOUSAND_DAYS_MS } from './provider'; import passport from 'passport'; import express from 'express'; import * as utils from './../utils'; +import refresh from 'passport-oauth2-refresh'; const googleAuthProviderConfig = { provider: 'google', @@ -51,7 +52,7 @@ describe('GoogleAuthProvider', () => { }); describe('start authentication handler', () => { - const mockResponse: any = ({} as unknown) as express.Response; + const mockResponse = ({} as unknown) as express.Response; const mockNext: express.NextFunction = jest.fn(); it('should initiate authenticate request with provided scopes', () => { @@ -116,28 +117,64 @@ describe('GoogleAuthProvider', () => { describe('redirect frame handler', () => { const mockRequest = ({} as unknown) as express.Request; const mockResponse: any = ({ - send: jest.fn(), + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + cookie: jest.fn().mockReturnThis(), } as unknown) as express.Response; const mockNext: express.NextFunction = jest.fn(); - it('should call authenticate and post a response ', () => { + it('should call authenticate and post a response', () => { const spyPostMessage = jest .spyOn(utils, 'postMessageResponse') .mockImplementation(() => jest.fn()); const spyPassport = jest .spyOn(passport, 'authenticate') - .mockImplementation(() => jest.fn()); + .mockImplementation((_x, callbackFunc) => { + const cb = callbackFunc as Function; + cb(null, { refreshToken: 'REFRESH_TOKEN' }); + return jest.fn(); + }); const googleAuthProvider = new GoogleAuthProvider( googleAuthProviderConfig, ); googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); - const callbackFunc = spyPassport.mock.calls[0][1] as Function; - callbackFunc(); expect(spyPassport).toBeCalledTimes(1); expect(spyPostMessage).toBeCalledTimes(1); + expect(mockResponse.cookie).toBeCalledTimes(1); + expect(mockResponse.cookie).toBeCalledWith( + 'grtoken', + 'REFRESH_TOKEN', + expect.objectContaining({ + path: '/auth/google', + sameSite: 'none', + httpOnly: true, + maxAge: THOUSAND_DAYS_MS, + }), + ); + }); + + it('should respond with a 401 if no refresh token returned', () => { + const spyPassport = jest + .spyOn(passport, 'authenticate') + .mockImplementation((_x, callbackFunc) => { + const cb = callbackFunc as Function; + cb(null, {}); + return jest.fn(); + }); + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); + expect(spyPassport).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith('Failed to fetch refresh token'); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); }); }); @@ -159,4 +196,159 @@ describe('GoogleAuthProvider', () => { }).toThrow(); }); }); + + describe('refresh token handler', () => { + const mockResponse = ({ + status: jest.fn().mockReturnThis(), + send: jest.fn().mockReturnThis(), + } as unknown) as express.Response; + + describe('no refresh token cookie', () => { + it('should respond with a 401', () => { + const mockRequest = ({ + cookies: jest.fn(), + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith('Missing session cookie'); + + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + }); + }); + + describe('refresh token cookie, no scope', () => { + const mockRequest = ({ + cookies: { grtoken: 'REFRESH_TOKEN' }, + query: {}, + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + it('should request for a new access token and fail if no access token returned', () => { + const spyRefresh = jest + .spyOn(refresh, 'requestNewAccessToken') + .mockImplementation((_x, _y, _z, callbackFunc) => { + const cb = callbackFunc as Function; + cb(undefined, undefined, undefined, {}); + }); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(spyRefresh).toBeCalledTimes(1); + expect(spyRefresh).toBeCalledWith( + 'google', + 'REFRESH_TOKEN', + {}, + expect.any(Function), + ); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith( + 'Failed to refresh access token', + ); + }); + + it('should request for a new access token and return 401 if any error', () => { + const spyRefresh = jest + .spyOn(refresh, 'requestNewAccessToken') + .mockImplementation((_x, _y, _z, callbackFunc) => { + const cb = callbackFunc as Function; + cb({ error: 'ERROR' }, undefined, undefined, {}); + }); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(spyRefresh).toBeCalledTimes(1); + expect(spyRefresh).toBeCalledWith( + 'google', + 'REFRESH_TOKEN', + {}, + expect.any(Function), + ); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith( + 'Failed to refresh access token', + ); + }); + + it('should fetch and return a new access token', () => { + const spyRefresh = jest + .spyOn(refresh, 'requestNewAccessToken') + .mockImplementation((_x, _y, _z, callbackFunc) => { + const cb = callbackFunc as Function; + cb(undefined, 'ACCESS_TOKEN', undefined, { + expires_in: 'EXPIRES_IN', + id_token: 'ID_TOKEN', + }); + }); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(spyRefresh).toBeCalledTimes(1); + expect(spyRefresh).toBeCalledWith( + 'google', + 'REFRESH_TOKEN', + {}, + expect.any(Function), + ); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith({ + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 'EXPIRES_IN', + scope: undefined, + }); + }); + }); + + describe('refresh token cookie and scope', () => { + const mockRequest = ({ + cookies: { grtoken: 'REFRESH_TOKEN' }, + query: { + scope: 'a,b', + }, + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + it('should fetch and return a new access token with scopes', () => { + const spyRefresh = jest + .spyOn(refresh, 'requestNewAccessToken') + .mockImplementation((_x, _y, _z, callbackFunc) => { + const cb = callbackFunc as Function; + cb(undefined, 'ACCESS_TOKEN', undefined, { + expires_in: 'EXPIRES_IN', + id_token: 'ID_TOKEN', + scope: 'a,b', + }); + }); + + googleAuthProvider.refresh(mockRequest, mockResponse); + expect(spyRefresh).toBeCalledTimes(1); + expect(spyRefresh).toBeCalledWith( + 'google', + 'REFRESH_TOKEN', + { scope: 'a,b' }, + expect.any(Function), + ); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith({ + accessToken: 'ACCESS_TOKEN', + idToken: 'ID_TOKEN', + expiresInSeconds: 'EXPIRES_IN', + scope: 'a,b', + }); + }); + }); + }); }); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 88b7345213..51845f638b 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -26,7 +26,7 @@ import { import { postMessageResponse } from './../utils'; import { InputError } from '@backstage/backend-common'; -const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; +export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; export class GoogleAuthProvider implements AuthProvider, AuthProviderRouteHandlers { private readonly providerConfig: AuthProviderConfig; @@ -57,6 +57,11 @@ export class GoogleAuthProvider ) { return passport.authenticate('google', (_, user) => { const { refreshToken } = user; + + if (!refreshToken) { + return res.status(401).send('Failed to fetch refresh token'); + } + delete user.refreshToken; const options: CookieOptions = { @@ -69,7 +74,7 @@ export class GoogleAuthProvider }; res.cookie('grtoken', refreshToken, options); - postMessageResponse(res, { + return postMessageResponse(res, { type: 'auth-result', payload: user, }); @@ -94,7 +99,7 @@ export class GoogleAuthProvider 'google', refreshToken, refreshTokenRequestParams, - (err, accessToken, _, params) => { + (err, accessToken, _refreshToken, params) => { if (err || !accessToken) { return res.status(401).send('Failed to refresh access token'); } From fabcec2c0fbe62ea22dfb5caefa1cd95e6f239b5 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 26 May 2020 09:13:42 +0200 Subject: [PATCH 14/58] fix: add eslint ignore to module mock file --- .../catalog-backend/src/ingestion/__mocks__/LocationReaders.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts index c6ac92ed0f..2fa29ccce3 100644 --- a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts @@ -32,7 +32,7 @@ export class LocationReaders implements LocationReader { }, }; } - + // eslint-disable-next-line constructor(private readonly sources: Record) {} // eslint-disable-next-line From 885eaac0a98831d5baddcbdb61c924e27253082e Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 26 May 2020 10:02:02 +0200 Subject: [PATCH 15/58] fix: disable typechecking for mock --- .../catalog-backend/src/ingestion/__mocks__/LocationReaders.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts index 2fa29ccce3..9459a858f7 100644 --- a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts +++ b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts @@ -14,6 +14,7 @@ * limitations under the License. */ +// @ts-nocheck import { LocationSource, LocationReader, ReaderOutput } from '../types'; export class LocationReaders implements LocationReader { From 33b73723637da5b138a2fe4bfd19e574d58cd009 Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 26 May 2020 10:51:07 +0200 Subject: [PATCH 16/58] fix: parametrize location reader and update test --- .../catalog/DatabaseLocationsCatalog.test.ts | 18 +++++++- .../src/catalog/DatabaseLocationsCatalog.ts | 12 +++--- .../ingestion/__mocks__/LocationReaders.ts | 43 ------------------- 3 files changed, 23 insertions(+), 50 deletions(-) delete mode 100644 plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index af1e1fdf2f..e04d0ad5c0 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -21,6 +21,7 @@ import path from 'path'; import { Database } from '../database'; import { getVoidLogger } from '../../../../packages/backend-common/src/logging/voidLogger'; +import { ReaderOutput } from '../ingestion/types'; describe('DatabaseLocationsCatalog', () => { const database = knex({ @@ -34,14 +35,29 @@ describe('DatabaseLocationsCatalog', () => { let db: Database; let catalog: DatabaseLocationsCatalog; + const mockLocationReader = { + read: async (type: string, target: string): Promise => { + if (type !== 'valid_type') { + throw new Error(`Unknown location type ${type}`); + } + if (target === 'valid_target') { + return Promise.resolve([{ type: 'data', data: {} }]); + } + throw new Error( + `Can't read location at ${target} with error: Something is broken`, + ); + }, + }; + beforeEach(async () => { await database.migrate.latest({ directory: path.resolve(__dirname, '../database/migrations'), loadExtensions: ['.ts'], }); db = new Database(database, getVoidLogger()); - catalog = new DatabaseLocationsCatalog(db); + catalog = new DatabaseLocationsCatalog(db, mockLocationReader); }); + it('resolves to location with id', async () => { return expect( catalog.addLocation({ type: 'valid_type', target: 'valid_target' }), diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index 4b2a3a8006..14d8cf7dec 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -16,16 +16,16 @@ import { Database } from '../database'; import { AddLocation, Location, LocationsCatalog } from './types'; -import { LocationReaders } from '../ingestion'; +import { LocationReader } from '../ingestion'; export class DatabaseLocationsCatalog implements LocationsCatalog { - constructor(private readonly database: Database) {} + constructor( + private readonly database: Database, + private readonly reader: LocationReader, + ) {} async addLocation(location: AddLocation): Promise { - const outputs = await LocationReaders.create().read( - location.type, - location.target, - ); + const outputs = await this.reader.read(location.type, location.target); outputs.forEach(output => { if (output.type === 'error') { throw new Error( diff --git a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts b/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts deleted file mode 100644 index 9459a858f7..0000000000 --- a/plugins/catalog-backend/src/ingestion/__mocks__/LocationReaders.ts +++ /dev/null @@ -1,43 +0,0 @@ -/* - * Copyright 2020 Spotify AB - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -// @ts-nocheck -import { LocationSource, LocationReader, ReaderOutput } from '../types'; - -export class LocationReaders implements LocationReader { - static create(): LocationReader { - return { - read: (type, target) => { - if (type !== 'valid_type') { - throw new Error(`Unknown location type ${type}`); - } - if (target === 'valid_target') { - return Promise.resolve([{ type: 'data', data: {} }]); - } - throw new Error( - `Can't read location at ${target} with error: Something is broken`, - ); - }, - }; - } - // eslint-disable-next-line - constructor(private readonly sources: Record) {} - - // eslint-disable-next-line - async read(type: string, target: string): Promise { - return []; - } -} From 920613533ef8ef746890181a61fdbf71e6818f1c Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 26 May 2020 10:56:38 +0200 Subject: [PATCH 17/58] fix: add missing reader argument --- packages/backend/src/plugins/catalog.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/backend/src/plugins/catalog.ts b/packages/backend/src/plugins/catalog.ts index e18d021395..687fd9157a 100644 --- a/packages/backend/src/plugins/catalog.ts +++ b/packages/backend/src/plugins/catalog.ts @@ -36,7 +36,7 @@ export default async function ({ logger, database }: PluginEnvironment) { ); const entitiesCatalog = new DatabaseEntitiesCatalog(db); - const locationsCatalog = new DatabaseLocationsCatalog(db); + const locationsCatalog = new DatabaseLocationsCatalog(db, reader); return await createRouter({ entitiesCatalog, locationsCatalog, logger }); } From 8c71c29b60111dd80849ad712c1c26692321c1a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 May 2020 11:08:17 +0200 Subject: [PATCH 18/58] Add basic test harness for the router --- packages/backend/package.json | 3 +- plugins/catalog-backend/package.json | 2 + .../src/service/router.test.ts | 76 +++++++++++++++++++ 3 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 plugins/catalog-backend/src/service/router.test.ts diff --git a/packages/backend/package.json b/packages/backend/package.json index 2c7b48f65f..9aedafecbb 100644 --- a/packages/backend/package.json +++ b/packages/backend/package.json @@ -40,8 +40,7 @@ "@types/helmet": "^0.0.47", "jest": "^26.0.1", "tsc-watch": "^4.2.3", - "typescript": "^3.9.2", - "winston": "^3.2.1" + "typescript": "^3.9.2" }, "nodemonConfig": { "watch": "./dist" diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index 9c0b066f4f..ce2a985efa 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -17,6 +17,7 @@ "dependencies": { "@backstage/backend-common": "^0.1.1-alpha.6", "@types/node-fetch": "^2.5.7", + "@types/supertest": "^2.0.8", "compression": "^1.7.4", "cors": "^2.8.5", "express": "^4.17.1", @@ -40,6 +41,7 @@ "@types/uuid": "^7.0.3", "@types/yup": "^0.28.2", "jest-fetch-mock": "^3.0.3", + "supertest": "^4.0.2", "tsc-watch": "^4.2.3" }, "files": [ diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts new file mode 100644 index 0000000000..c4e21c9b0e --- /dev/null +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -0,0 +1,76 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import express from 'express'; +import request from 'supertest'; +import { createRouter } from './router'; +import { EntitiesCatalog, LocationsCatalog, Location } from '../catalog'; +import { getVoidLogger } from '@backstage/backend-common'; +import { DescriptorEnvelope } from '../ingestion'; + +class MockEntitiesCatalog implements EntitiesCatalog { + entities = jest.fn(); + entity = jest.fn(); +} + +class MockLocationsCatalog implements LocationsCatalog { + addLocation = jest.fn(); + removeLocation = jest.fn(); + locations = jest.fn(); + location = jest.fn(); +} + +describe('createRouter', () => { + describe('entities', () => { + it('happy path: lists entities', async () => { + const entities: DescriptorEnvelope[] = [{ apiVersion: 'a', kind: 'b' }]; + + const catalog = new MockEntitiesCatalog(); + catalog.entities.mockResolvedValueOnce(entities); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).get('/entities'); + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual(entities); + }); + }); + + describe('locations', () => { + it('happy path: lists locations', async () => { + const locations: Location[] = [{ id: 'a', type: 'b', target: 'c' }]; + + const catalog = new MockLocationsCatalog(); + catalog.locations.mockResolvedValueOnce(locations); + + const router = await createRouter({ + locationsCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).get('/locations'); + + expect(response.status).toEqual(200); + expect(JSON.parse(response.text)).toEqual(locations); + }); + }); +}); From bbdd383ebe89d0e30253fc7d629fd16b21782a9d Mon Sep 17 00:00:00 2001 From: Nikita Nek Dudnik Date: Tue, 26 May 2020 11:31:23 +0200 Subject: [PATCH 19/58] fix: PR review clean up --- .../src/catalog/DatabaseLocationsCatalog.test.ts | 12 ++++-------- .../src/catalog/DatabaseLocationsCatalog.ts | 2 +- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts index e04d0ad5c0..0181b7fc28 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.test.ts @@ -14,14 +14,12 @@ * limitations under the License. */ import { DatabaseLocationsCatalog } from './DatabaseLocationsCatalog'; -jest.mock('../ingestion/LocationReaders'); - import knex from 'knex'; import path from 'path'; import { Database } from '../database'; -import { getVoidLogger } from '../../../../packages/backend-common/src/logging/voidLogger'; import { ReaderOutput } from '../ingestion/types'; +import { getVoidLogger } from '@backstage/backend-common'; describe('DatabaseLocationsCatalog', () => { const database = knex({ @@ -71,16 +69,14 @@ describe('DatabaseLocationsCatalog', () => { const type = 'invalid_type'; return expect( catalog.addLocation({ type, target: 'valid_target' }), - ).rejects.toEqual(new Error(`Unknown location type ${type}`)); + ).rejects.toThrow(/Unknown location type/); }); it('rejects for unreadable target ', async () => { const target = 'invalid_target'; return expect( catalog.addLocation({ type: 'valid_type', target }), - ).rejects.toEqual( - new Error( - `Can't read location at ${target} with error: Something is broken`, - ), + ).rejects.toThrow( + `Can't read location at ${target} with error: Something is broken`, ); }); }); diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index 14d8cf7dec..3f5739eed0 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -29,7 +29,7 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { outputs.forEach(output => { if (output.type === 'error') { throw new Error( - `Can't read location at ${location.target} with error: ${output.error.message}`, + `Can't read location at ${location.target}, ${output.error}`, ); } }); From 631c789754e4f0221e0df8f9ff839da1805f0b15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=A9mi=20Doreau?= <32459935+ayshiff@users.noreply.github.com> Date: Tue, 26 May 2020 11:48:15 +0200 Subject: [PATCH 20/58] feat(component): add subtitle to Table (#1011) --- .../src/components/Table/Table.stories.tsx | 36 +++++++++++++++++++ .../core/src/components/Table/Table.test.tsx | 7 ++++ packages/core/src/components/Table/Table.tsx | 21 +++++++++-- 3 files changed, 62 insertions(+), 2 deletions(-) diff --git a/packages/core/src/components/Table/Table.stories.tsx b/packages/core/src/components/Table/Table.stories.tsx index bf2c5a3fc7..dbe87963ee 100644 --- a/packages/core/src/components/Table/Table.stories.tsx +++ b/packages/core/src/components/Table/Table.stories.tsx @@ -76,6 +76,42 @@ export const DefaultTable = () => { ); }; +export const SubtitleTable = () => { + const columns: TableColumn[] = [ + { + title: 'Column 1', + field: 'col1', + highlight: true, + }, + { + title: 'Column 2', + field: 'col2', + }, + { + title: 'Numeric value', + field: 'number', + type: 'numeric', + }, + { + title: 'A Date', + field: 'date', + type: 'date', + }, + ]; + + return ( +
+ + + ); +}; + export const HiddenSearchTable = () => { const columns: TableColumn[] = [ { diff --git a/packages/core/src/components/Table/Table.test.tsx b/packages/core/src/components/Table/Table.test.tsx index f6801359eb..fb87db1b4b 100644 --- a/packages/core/src/components/Table/Table.test.tsx +++ b/packages/core/src/components/Table/Table.test.tsx @@ -47,4 +47,11 @@ describe('
', () => { const rendered = render(wrapInTestApp(
)); expect(rendered.getByText('second value, second row')).toBeInTheDocument(); }); + + it('renders with subtitle', () => { + const rendered = render( + wrapInTestApp(
), + ); + expect(rendered.getByText('subtitle')).toBeInTheDocument(); + }); }); diff --git a/packages/core/src/components/Table/Table.tsx b/packages/core/src/components/Table/Table.tsx index a4dbc89b4f..b092f6d39b 100644 --- a/packages/core/src/components/Table/Table.tsx +++ b/packages/core/src/components/Table/Table.tsx @@ -24,7 +24,7 @@ import MTable, { Column, } from 'material-table'; import { BackstageTheme } from '@backstage/theme'; -import { makeStyles, useTheme } from '@material-ui/core'; +import { makeStyles, useTheme, Typography } from '@material-ui/core'; // Material-table is not using the standard icons available in in material-ui. https://github.com/mbrn/material-table/issues/51 import AddBox from '@material-ui/icons/AddBox'; @@ -158,9 +158,16 @@ export interface TableColumn extends Column<{}> { export interface TableProps extends MaterialTableProps<{}> { columns: TableColumn[]; + subtitle?: string; } -const Table: FC = ({ columns, options, ...props }) => { +const Table: FC = ({ + columns, + options, + title, + subtitle, + ...props +}) => { const cellClasses = useCellStyles(); const headerClasses = useHeaderStyles(); const toolbarClasses = useToolbarStyles(); @@ -190,6 +197,16 @@ const Table: FC = ({ columns, options, ...props }) => { options={{ ...defaultOptions, ...options }} columns={MTColumns} icons={tableIcons} + title={ + <> + {title} + {subtitle && ( + + {subtitle} + + )} + + } {...props} /> ); From 642de90255497124768504a8a29fb8a6b5d4a199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 May 2020 14:48:37 +0200 Subject: [PATCH 21/58] Filtering --- .../src/catalog/DatabaseEntitiesCatalog.ts | 6 +- plugins/catalog-backend/src/catalog/types.ts | 10 +- .../src/database/Database.test.ts | 104 ++++++++++++++++++ .../catalog-backend/src/database/Database.ts | 27 ++++- .../src/service/router.test.ts | 52 ++++++++- plugins/catalog-backend/src/service/router.ts | 27 ++++- 6 files changed, 209 insertions(+), 17 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index 697da0ea35..fb19f04220 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -17,14 +17,14 @@ import { NotFoundError } from '@backstage/backend-common'; import { Database } from '../database'; import { DescriptorEnvelope } from '../ingestion/types'; -import { EntitiesCatalog } from './types'; +import { EntitiesCatalog, EntityFilters } from './types'; export class DatabaseEntitiesCatalog implements EntitiesCatalog { constructor(private readonly database: Database) {} - async entities(): Promise { + async entities(filters?: EntityFilters): Promise { const items = await this.database.transaction(tx => - this.database.entities(tx), + this.database.entities(tx, filters), ); return items.map(i => i.entity); } diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index 92c059d25b..e5beb801d0 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -18,11 +18,17 @@ import * as yup from 'yup'; import { DescriptorEnvelope } from '../ingestion'; // -// Items +// Entities // +export type EntityFilter = { + key: string; + values: (string | null)[]; +}; +export type EntityFilters = EntityFilter[]; + export type EntitiesCatalog = { - entities(): Promise; + entities(filters?: EntityFilters): Promise; entity( kind: string, name: string, diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index 0eb64916e5..7f02b45eee 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -28,6 +28,7 @@ import { DbEntityResponse, DbLocationsRow, } from './types'; +import { DescriptorEnvelope } from '../ingestion'; describe('Database', () => { let database: Knex; @@ -242,4 +243,107 @@ describe('Database', () => { ).rejects.toThrow(ConflictError); }); }); + + describe('entities', () => { + it('can get all entities with empty filters list', async () => { + const catalog = new Database(database, getVoidLogger()); + const e1: DescriptorEnvelope = { apiVersion: 'a', kind: 'b' }; + const e2: DescriptorEnvelope = { + apiVersion: 'a', + kind: 'b', + spec: { c: null }, + }; + await catalog.transaction(async tx => { + await catalog.addEntity(tx, { entity: e1 }); + await catalog.addEntity(tx, { entity: e2 }); + }); + await expect( + catalog.transaction(async tx => catalog.entities(tx, [])), + ).resolves.toEqual([ + { locationId: undefined, entity: expect.objectContaining(e1) }, + { locationId: undefined, entity: expect.objectContaining(e2) }, + ]); + }); + it('can get all specific entities for matching filters (naive case)', async () => { + const catalog = new Database(database, getVoidLogger()); + const entities: DescriptorEnvelope[] = [ + { apiVersion: 'a', kind: 'b' }, + { + apiVersion: 'a', + kind: 'b', + spec: { c: 'some' }, + }, + { + apiVersion: 'a', + kind: 'b', + spec: { c: null }, + }, + ]; + + await catalog.transaction(async tx => { + for (const entity of entities) { + await catalog.addEntity(tx, { entity }); + } + }); + + await expect( + catalog.transaction(async tx => + catalog.entities(tx, [ + { key: 'kind', values: ['b'] }, + { key: 'spec.c', values: ['some'] }, + ]), + ), + ).resolves.toEqual([ + { locationId: undefined, entity: expect.objectContaining(entities[1]) }, + ]); + }); + + it('can get all specific entities for matching filters with nulls (both missing and literal null value)', async () => { + const catalog = new Database(database, getVoidLogger()); + const entities: DescriptorEnvelope[] = [ + { apiVersion: 'a', kind: 'b' }, + { + apiVersion: 'a', + kind: 'b', + spec: { c: 'some' }, + }, + { + apiVersion: 'a', + kind: 'b', + spec: { c: null }, + }, + ]; + + await catalog.transaction(async tx => { + for (const entity of entities) { + await catalog.addEntity(tx, { entity }); + } + }); + + const rows = await catalog.transaction(async tx => + catalog.entities(tx, [ + { key: 'kind', values: ['b'] }, + { key: 'spec.c', values: [null, 'some'] }, + ]), + ); + + expect(rows.length).toEqual(3); + expect(rows).toEqual( + expect.arrayContaining([ + { + locationId: undefined, + entity: expect.objectContaining(entities[0]), + }, + { + locationId: undefined, + entity: expect.objectContaining(entities[1]), + }, + { + locationId: undefined, + entity: expect.objectContaining(entities[2]), + }, + ]), + ); + }); + }); }); diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index cd4ca731a3..3b616b4202 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -23,6 +23,7 @@ import Knex from 'knex'; import lodash from 'lodash'; import { v4 as uuidv4 } from 'uuid'; import { Logger } from 'winston'; +import { EntityFilters } from '../catalog'; import { DescriptorEnvelope, EntityMeta } from '../ingestion'; import { buildEntitySearch } from './search'; import { @@ -309,10 +310,30 @@ export class Database { return { locationId: request.locationId, entity: newEntity }; } - async entities(tx: Knex.Transaction): Promise { - const rows = await tx('entities') + async entities( + tx: Knex.Transaction, + filters?: EntityFilters, + ): Promise { + let builder = tx('entities'); + for (const [index, filter] of (filters ?? []).entries()) { + builder = builder + .leftOuterJoin(`entities_search as t${index}`, function join() { + this.on('entities.id', '=', `t${index}.entity_id`).onIn( + `t${index}.value`, + filter.values.filter(x => x), + ); + if (filter.values.some(x => !x)) { + this.orOnNull(`t${index}.value`); + } + }) + .where(`t${index}.key`, '=', filter.key); + } + + const rows = await builder .orderBy('namespace', 'name') - .select(); + .select('entities.*') + .groupBy('id'); + return rows.map(row => toEntityResponse(row)); } diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index c4e21c9b0e..1b1f4c5034 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -14,12 +14,16 @@ * limitations under the License. */ +import { + errorHandler, + getVoidLogger, + InputError, +} from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; -import { createRouter } from './router'; -import { EntitiesCatalog, LocationsCatalog, Location } from '../catalog'; -import { getVoidLogger } from '@backstage/backend-common'; +import { EntitiesCatalog, Location, LocationsCatalog } from '../catalog'; import { DescriptorEnvelope } from '../ingestion'; +import { createRouter } from './router'; class MockEntitiesCatalog implements EntitiesCatalog { entities = jest.fn(); @@ -50,7 +54,26 @@ describe('createRouter', () => { const response = await request(app).get('/entities'); expect(response.status).toEqual(200); - expect(JSON.parse(response.text)).toEqual(entities); + expect(response.body).toEqual(entities); + }); + + it('parses single and multiple request parameters and passes them down', async () => { + const catalog = new MockEntitiesCatalog(); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).get('/entities?a=1&a=&a=3&b=4&c='); + + expect(response.status).toEqual(200); + expect(catalog.entities).toHaveBeenCalledWith([ + { key: 'a', values: ['1', null, '3'] }, + { key: 'b', values: ['4'] }, + { key: 'c', values: [null] }, + ]); }); }); @@ -70,7 +93,26 @@ describe('createRouter', () => { const response = await request(app).get('/locations'); expect(response.status).toEqual(200); - expect(JSON.parse(response.text)).toEqual(locations); + expect(response.body).toEqual(locations); + }); + + it('rejects malformed locations', async () => { + const location = ({ + id: 'a', + typez: 'b', + target: 'c', + } as unknown) as Location; + + const catalog = new MockLocationsCatalog(); + const router = await createRouter({ + locationsCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).post('/locations').send(location); + + expect(response.status).toEqual(400); }); }); }); diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index af714e4026..81533adfc5 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -14,12 +14,14 @@ * limitations under the License. */ +import { errorHandler } from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; import { addLocationSchema, EntitiesCatalog, + EntityFilters, LocationsCatalog, } from '../catalog'; import { validateRequestBody } from './util'; @@ -34,17 +36,33 @@ export async function createRouter( options: RouterOptions, ): Promise { const { entitiesCatalog, locationsCatalog } = options; + const router = Router(); + router.use(express.json()); if (entitiesCatalog) { - // Entities - router.get('/entities', async (_req, res) => { - const entities = await entitiesCatalog.entities(); + router.get('/entities', async (req, res) => { + const filters: EntityFilters = []; + for (const [key, valueOrValues] of Object.entries(req.query)) { + const values = Array.isArray(valueOrValues) + ? valueOrValues + : [valueOrValues]; + if (values.some(v => typeof v !== 'string')) { + res.status(400).send('Complex query parameters are not supported'); + return; + } + filters.push({ + key, + values: values.map(v => v || null) as string[], + }); + } + + const entities = await entitiesCatalog.entities(filters); + res.status(200).send(entities); }); } - // Locations if (locationsCatalog) { router .post('/locations', async (req, res) => { @@ -68,5 +86,6 @@ export async function createRouter( }); } + router.use(errorHandler()); return router; } From f1ac4d8626873143be216d3b68a542d3024f84dc Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 26 May 2020 15:06:12 +0200 Subject: [PATCH 22/58] build(deps): bump uuid from 8.0.0 to 8.1.0 (#1006) Bumps [uuid](https://github.com/uuidjs/uuid) from 8.0.0 to 8.1.0. - [Release notes](https://github.com/uuidjs/uuid/releases) - [Changelog](https://github.com/uuidjs/uuid/blob/master/CHANGELOG.md) - [Commits](https://github.com/uuidjs/uuid/compare/v8.0.0...v8.1.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3c6fee1dcd..3115a4ccfe 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20880,9 +20880,9 @@ uuid@^7.0.3: integrity sha512-DPSke0pXhTZgoF/d+WSt2QaKMCFSfx7QegxEWT+JOuHF5aWrKEn0G+ztjuJg/gG8/ItK+rbPCD/yNv8yyih6Cg== uuid@^8.0.0: - version "8.0.0" - resolved "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz#bc6ccf91b5ff0ac07bbcdbf1c7c4e150db4dbb6c" - integrity sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw== + version "8.1.0" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.1.0.tgz#6f1536eb43249f473abc6bd58ff983da1ca30d8d" + integrity sha512-CI18flHDznR0lq54xBycOVmphdCYnQLKn8abKn7PXUiKUGdEd+/l9LWNJmugXel4hXq7S+RMNl34ecyC9TntWg== v8-compile-cache@^2.0.3: version "2.1.0" From bfeaf43c717c6365022c0322719ef1f9ce310c2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 May 2020 15:50:31 +0200 Subject: [PATCH 23/58] Individual entity fetches --- .../src/catalog/DatabaseEntitiesCatalog.ts | 31 +++++-- .../src/catalog/DatabaseLocationsCatalog.ts | 2 +- .../src/catalog/StaticEntitiesCatalog.ts | 10 +- plugins/catalog-backend/src/catalog/types.ts | 5 +- .../src/database/Database.test.ts | 3 +- .../catalog-backend/src/database/Database.ts | 6 +- .../src/database/DatabaseManager.test.ts | 2 +- .../src/service/router.test.ts | 92 +++++++++++++++++-- plugins/catalog-backend/src/service/router.ts | 73 +++++++++++---- 9 files changed, 180 insertions(+), 44 deletions(-) diff --git a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts index fb19f04220..9d0d8fcaf7 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseEntitiesCatalog.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -import { NotFoundError } from '@backstage/backend-common'; import { Database } from '../database'; import { DescriptorEnvelope } from '../ingestion/types'; import { EntitiesCatalog, EntityFilters } from './types'; @@ -29,17 +28,33 @@ export class DatabaseEntitiesCatalog implements EntitiesCatalog { return items.map(i => i.entity); } - async entity( + async entityByUid(uid: string): Promise { + const matches = await this.database.transaction(tx => + this.database.entities(tx, [{ key: 'uid', values: [uid] }]), + ); + + return matches.length ? matches[0].entity : undefined; + } + + async entityByName( kind: string, name: string, namespace: string | undefined, ): Promise { - const item = await this.database.transaction(tx => - this.database.entity(tx, kind, name, namespace), + const matches = await this.database.transaction(tx => + this.database.entities(tx, [ + { key: 'kind', values: [kind] }, + { key: 'name', values: [name] }, + { + key: 'namespace', + values: + !namespace || namespace === 'default' + ? [null, 'default'] + : [namespace], + }, + ]), ); - if (!item) { - throw new NotFoundError('Entity cannot be found'); - } - return item.entity; + + return matches.length ? matches[0].entity : undefined; } } diff --git a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts index e9f1512074..9effed724c 100644 --- a/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts +++ b/plugins/catalog-backend/src/catalog/DatabaseLocationsCatalog.ts @@ -35,7 +35,7 @@ export class DatabaseLocationsCatalog implements LocationsCatalog { } async location(id: string): Promise { - const item = await this.location(id); + const item = await this.database.location(id); return item; } } diff --git a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts index 17f7603fb7..371cdf1f76 100644 --- a/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/catalog/StaticEntitiesCatalog.ts @@ -30,7 +30,15 @@ export class StaticEntitiesCatalog implements EntitiesCatalog { return lodash.cloneDeep(this._entities); } - async entity( + async entityByUid(uid: string): Promise { + const item = this._entities.find(e => uid === e.metadata?.uid); + if (!item) { + throw new NotFoundError('Entity cannot be found'); + } + return lodash.cloneDeep(item); + } + + async entityByName( kind: string, name: string, namespace: string | undefined, diff --git a/plugins/catalog-backend/src/catalog/types.ts b/plugins/catalog-backend/src/catalog/types.ts index e5beb801d0..d5627ea86a 100644 --- a/plugins/catalog-backend/src/catalog/types.ts +++ b/plugins/catalog-backend/src/catalog/types.ts @@ -29,10 +29,11 @@ export type EntityFilters = EntityFilter[]; export type EntitiesCatalog = { entities(filters?: EntityFilters): Promise; - entity( + entityByUid(uid: string): Promise; + entityByName( kind: string, - name: string, namespace: string | undefined, + name: string, ): Promise; }; diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index 7f02b45eee..7e9c75d199 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -21,6 +21,7 @@ import { } from '@backstage/backend-common'; import Knex from 'knex'; import path from 'path'; +import { DescriptorEnvelope } from '../ingestion'; import { Database } from './Database'; import { AddDatabaseLocation, @@ -28,7 +29,6 @@ import { DbEntityResponse, DbLocationsRow, } from './types'; -import { DescriptorEnvelope } from '../ingestion'; describe('Database', () => { let database: Knex; @@ -264,6 +264,7 @@ describe('Database', () => { { locationId: undefined, entity: expect.objectContaining(e2) }, ]); }); + it('can get all specific entities for matching filters (naive case)', async () => { const catalog = new Database(database, getVoidLogger()); const entities: DescriptorEnvelope[] = [ diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index 3b616b4202..a3aba825f4 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -276,7 +276,7 @@ export class Database { ? oldRow.generation : oldRow.generation + 1; const newEntity = lodash.cloneDeep(request.entity); - newEntity.metadata = Object.assign({}, request.entity.metadata, { + newEntity.metadata = Object.assign({}, newEntity.metadata, { uid: oldRow.id, etag: newEtag, generation: newGeneration, @@ -357,9 +357,7 @@ export class Database { async addLocation(location: AddDatabaseLocation): Promise { return await this.database.transaction(async tx => { const existingLocation = await tx('locations') - .where({ - target: location.target, - }) + .where({ target: location.target }) .select(); if (existingLocation?.[0]) { diff --git a/plugins/catalog-backend/src/database/DatabaseManager.test.ts b/plugins/catalog-backend/src/database/DatabaseManager.test.ts index 2da3c33a00..ca50f518cb 100644 --- a/plugins/catalog-backend/src/database/DatabaseManager.test.ts +++ b/plugins/catalog-backend/src/database/DatabaseManager.test.ts @@ -15,6 +15,7 @@ */ import { getVoidLogger } from '@backstage/backend-common'; +import Knex from 'knex'; import { ComponentDescriptor, DescriptorParser, @@ -24,7 +25,6 @@ import { import { Database } from './Database'; import { DatabaseManager } from './DatabaseManager'; import { DatabaseLocationUpdateLogStatus, DbLocationsRow } from './types'; -import Knex from 'knex'; describe('DatabaseManager', () => { describe('refreshLocations', () => { diff --git a/plugins/catalog-backend/src/service/router.test.ts b/plugins/catalog-backend/src/service/router.test.ts index 1b1f4c5034..8c29362015 100644 --- a/plugins/catalog-backend/src/service/router.test.ts +++ b/plugins/catalog-backend/src/service/router.test.ts @@ -14,11 +14,7 @@ * limitations under the License. */ -import { - errorHandler, - getVoidLogger, - InputError, -} from '@backstage/backend-common'; +import { getVoidLogger } from '@backstage/backend-common'; import express from 'express'; import request from 'supertest'; import { EntitiesCatalog, Location, LocationsCatalog } from '../catalog'; @@ -27,7 +23,8 @@ import { createRouter } from './router'; class MockEntitiesCatalog implements EntitiesCatalog { entities = jest.fn(); - entity = jest.fn(); + entityByUid = jest.fn(); + entityByName = jest.fn(); } class MockLocationsCatalog implements LocationsCatalog { @@ -77,6 +74,89 @@ describe('createRouter', () => { }); }); + describe('entityByUid', () => { + it('can fetch entity by uid', async () => { + const entity: DescriptorEnvelope = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + }, + }; + const catalog = new MockEntitiesCatalog(); + catalog.entityByUid.mockResolvedValue(entity); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).get('/entities/by-uid/zzz'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(expect.objectContaining(entity)); + }); + + it('responds with a 404 for missing entities', async () => { + const catalog = new MockEntitiesCatalog(); + catalog.entityByUid.mockResolvedValue(undefined); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).get('/entities/by-uid/zzz'); + + expect(response.status).toEqual(404); + expect(response.text).toMatch(/uid/); + }); + }); + + describe('entityByName', () => { + it('can fetch entity by name', async () => { + const entity: DescriptorEnvelope = { + apiVersion: 'a', + kind: 'b', + metadata: { + name: 'c', + namespace: 'd', + }, + }; + const catalog = new MockEntitiesCatalog(); + catalog.entityByName.mockResolvedValue(entity); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).get('/entities/by-name/b/d/c'); + + expect(response.status).toEqual(200); + expect(response.body).toEqual(expect.objectContaining(entity)); + }); + + it('responds with a 404 for missing entities', async () => { + const catalog = new MockEntitiesCatalog(); + catalog.entityByName.mockResolvedValue(undefined); + + const router = await createRouter({ + entitiesCatalog: catalog, + logger: getVoidLogger(), + }); + + const app = express().use(router); + const response = await request(app).get('/entities/by-name//b/d/c'); + + expect(response.status).toEqual(404); + expect(response.text).toMatch(/name/); + }); + }); + describe('locations', () => { it('happy path: lists locations', async () => { const locations: Location[] = [{ id: 'a', type: 'b', target: 'c' }]; diff --git a/plugins/catalog-backend/src/service/router.ts b/plugins/catalog-backend/src/service/router.ts index 81533adfc5..c3318a7377 100644 --- a/plugins/catalog-backend/src/service/router.ts +++ b/plugins/catalog-backend/src/service/router.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { errorHandler } from '@backstage/backend-common'; +import { errorHandler, InputError } from '@backstage/backend-common'; import express from 'express'; import Router from 'express-promise-router'; import { Logger } from 'winston'; @@ -41,26 +41,36 @@ export async function createRouter( router.use(express.json()); if (entitiesCatalog) { - router.get('/entities', async (req, res) => { - const filters: EntityFilters = []; - for (const [key, valueOrValues] of Object.entries(req.query)) { - const values = Array.isArray(valueOrValues) - ? valueOrValues - : [valueOrValues]; - if (values.some(v => typeof v !== 'string')) { - res.status(400).send('Complex query parameters are not supported'); - return; + router + .get('/entities', async (req, res) => { + const filters = translateQueryToEntityFilters(req); + const entities = await entitiesCatalog.entities(filters); + res.status(200).send(entities); + }) + .get('/entities/by-uid/:uid', async (req, res) => { + const { uid } = req.params; + const entity = await entitiesCatalog.entityByUid(uid); + if (!entity) { + res.status(404).send(`No entity with uid ${uid}`); } - filters.push({ - key, - values: values.map(v => v || null) as string[], - }); - } - - const entities = await entitiesCatalog.entities(filters); - - res.status(200).send(entities); - }); + res.status(200).send(entity); + }) + .get('/entities/by-name/:kind/:namespace/:name', async (req, res) => { + const { kind, namespace, name } = req.params; + const entity = await entitiesCatalog.entityByName( + kind, + name, + namespace, + ); + if (!entity) { + res + .status(404) + .send( + `No entity with kind ${kind} namespace ${namespace} name ${name}`, + ); + } + res.status(200).send(entity); + }); } if (locationsCatalog) { @@ -89,3 +99,26 @@ export async function createRouter( router.use(errorHandler()); return router; } + +function translateQueryToEntityFilters( + request: express.Request, +): EntityFilters { + const filters: EntityFilters = []; + + for (const [key, valueOrValues] of Object.entries(request.query)) { + const values = Array.isArray(valueOrValues) + ? valueOrValues + : [valueOrValues]; + + if (values.some(v => typeof v !== 'string')) { + throw new InputError('Complex query parameters are not supported'); + } + + filters.push({ + key, + values: values.map(v => v || null) as string[], + }); + } + + return filters; +} From 8a1145b96cb283e667d5985256f053138c13df45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 May 2020 16:22:45 +0200 Subject: [PATCH 24/58] Fix test order --- .../src/database/Database.test.ts | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/plugins/catalog-backend/src/database/Database.test.ts b/plugins/catalog-backend/src/database/Database.test.ts index 7e9c75d199..a82385aae4 100644 --- a/plugins/catalog-backend/src/database/Database.test.ts +++ b/plugins/catalog-backend/src/database/Database.test.ts @@ -257,12 +257,16 @@ describe('Database', () => { await catalog.addEntity(tx, { entity: e1 }); await catalog.addEntity(tx, { entity: e2 }); }); - await expect( - catalog.transaction(async tx => catalog.entities(tx, [])), - ).resolves.toEqual([ - { locationId: undefined, entity: expect.objectContaining(e1) }, - { locationId: undefined, entity: expect.objectContaining(e2) }, - ]); + const result = await catalog.transaction(async tx => + catalog.entities(tx, []), + ); + expect(result.length).toEqual(2); + expect(result).toEqual( + expect.arrayContaining([ + { locationId: undefined, entity: expect.objectContaining(e1) }, + { locationId: undefined, entity: expect.objectContaining(e2) }, + ]), + ); }); it('can get all specific entities for matching filters (naive case)', async () => { From a51ad53b00c94ba8fe0bba2a80744b19fabc367d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Tue, 26 May 2020 16:38:40 +0200 Subject: [PATCH 25/58] packages/core: group api implementations by the name of the declarations --- .../{ => AlertApi}/AlertApiForwarder.ts | 9 ++++++--- .../api/apis/implementations/AlertApi/index.ts | 17 +++++++++++++++++ .../AppThemeSelector.test.ts | 0 .../AppThemeSelector.ts | 4 ++-- .../{AppThemeSelector => AppThemeApi}/index.ts | 0 .../{ => ErrorApi}/ErrorAlerter.ts | 2 +- .../{ => ErrorApi}/ErrorApiForwarder.ts | 9 ++++++--- .../api/apis/implementations/ErrorApi/index.ts | 18 ++++++++++++++++++ .../MockOAuthApi.test.ts | 0 .../MockOAuthApi.ts | 0 .../OAuthPendingRequests.test.ts | 0 .../OAuthPendingRequests.ts | 0 .../OAuthRequestManager.test.ts | 0 .../OAuthRequestManager.ts | 0 .../index.ts | 0 .../core/src/api/apis/implementations/index.ts | 10 +++++----- .../AuthConnector/DefaultAuthConnector.test.ts | 2 +- .../RefreshingAuthSessionManager.ts | 3 +-- 18 files changed, 57 insertions(+), 17 deletions(-) rename packages/core/src/api/apis/implementations/{ => AlertApi}/AlertApiForwarder.ts (78%) create mode 100644 packages/core/src/api/apis/implementations/AlertApi/index.ts rename packages/core/src/api/apis/implementations/{AppThemeSelector => AppThemeApi}/AppThemeSelector.test.ts (100%) rename packages/core/src/api/apis/implementations/{AppThemeSelector => AppThemeApi}/AppThemeSelector.ts (94%) rename packages/core/src/api/apis/implementations/{AppThemeSelector => AppThemeApi}/index.ts (100%) rename packages/core/src/api/apis/implementations/{ => ErrorApi}/ErrorAlerter.ts (94%) rename packages/core/src/api/apis/implementations/{ => ErrorApi}/ErrorApiForwarder.ts (80%) create mode 100644 packages/core/src/api/apis/implementations/ErrorApi/index.ts rename packages/core/src/api/apis/implementations/{OAuthRequestManager => OAuthRequestApi}/MockOAuthApi.test.ts (100%) rename packages/core/src/api/apis/implementations/{OAuthRequestManager => OAuthRequestApi}/MockOAuthApi.ts (100%) rename packages/core/src/api/apis/implementations/{OAuthRequestManager => OAuthRequestApi}/OAuthPendingRequests.test.ts (100%) rename packages/core/src/api/apis/implementations/{OAuthRequestManager => OAuthRequestApi}/OAuthPendingRequests.ts (100%) rename packages/core/src/api/apis/implementations/{OAuthRequestManager => OAuthRequestApi}/OAuthRequestManager.test.ts (100%) rename packages/core/src/api/apis/implementations/{OAuthRequestManager => OAuthRequestApi}/OAuthRequestManager.ts (100%) rename packages/core/src/api/apis/implementations/{OAuthRequestManager => OAuthRequestApi}/index.ts (100%) diff --git a/packages/core/src/api/apis/implementations/AlertApiForwarder.ts b/packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts similarity index 78% rename from packages/core/src/api/apis/implementations/AlertApiForwarder.ts rename to packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts index c95408283c..901d3b57cc 100644 --- a/packages/core/src/api/apis/implementations/AlertApiForwarder.ts +++ b/packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AlertApi, AlertMessage } from '../../../'; -import { PublishSubject } from './lib'; -import { Observable } from '../../types'; +import { AlertApi, AlertMessage } from '../../../..'; +import { PublishSubject } from '../lib'; +import { Observable } from '../../../types'; +/** + * Base implementation for the AlertApi that simply forwards alerts to consumers. + */ export class AlertApiForwarder implements AlertApi { private readonly subject = new PublishSubject(); diff --git a/packages/core/src/api/apis/implementations/AlertApi/index.ts b/packages/core/src/api/apis/implementations/AlertApi/index.ts new file mode 100644 index 0000000000..12ab8bc60c --- /dev/null +++ b/packages/core/src/api/apis/implementations/AlertApi/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { AlertApiForwarder } from './AlertApiForwarder'; diff --git a/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.test.ts b/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.test.ts rename to packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.test.ts diff --git a/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.ts b/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts similarity index 94% rename from packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.ts rename to packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts index 7f6659e641..0f554ed7f1 100644 --- a/packages/core/src/api/apis/implementations/AppThemeSelector/AppThemeSelector.ts +++ b/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts @@ -33,7 +33,7 @@ export class AppThemeSelector implements AppThemeApi { selector.setActiveThemeId(initialThemeId); - selector.activeThemeId$().subscribe((themeId) => { + selector.activeThemeId$().subscribe(themeId => { if (themeId) { window.localStorage.setItem(STORAGE_KEY, themeId); } else { @@ -41,7 +41,7 @@ export class AppThemeSelector implements AppThemeApi { } }); - window.addEventListener('storage', (event) => { + window.addEventListener('storage', event => { if (event.key === STORAGE_KEY) { const themeId = localStorage.getItem(STORAGE_KEY) ?? undefined; selector.setActiveThemeId(themeId); diff --git a/packages/core/src/api/apis/implementations/AppThemeSelector/index.ts b/packages/core/src/api/apis/implementations/AppThemeApi/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/AppThemeSelector/index.ts rename to packages/core/src/api/apis/implementations/AppThemeApi/index.ts diff --git a/packages/core/src/api/apis/implementations/ErrorAlerter.ts b/packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts similarity index 94% rename from packages/core/src/api/apis/implementations/ErrorAlerter.ts rename to packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts index 81d0cc8fb8..903463d7de 100644 --- a/packages/core/src/api/apis/implementations/ErrorAlerter.ts +++ b/packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext, AlertApi } from '../../../'; +import { ErrorApi, ErrorContext, AlertApi } from '../../../..'; /** * Decorates an ErrorApi by also forwarding error messages diff --git a/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts b/packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts similarity index 80% rename from packages/core/src/api/apis/implementations/ErrorApiForwarder.ts rename to packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts index a61fdae5c1..6729050401 100644 --- a/packages/core/src/api/apis/implementations/ErrorApiForwarder.ts +++ b/packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts @@ -13,10 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext } from '../../../'; -import { PublishSubject } from './lib'; -import { Observable } from '../../types'; +import { ErrorApi, ErrorContext } from '../../../..'; +import { PublishSubject } from '../lib'; +import { Observable } from '../../../types'; +/** + * Base implementation for the ErrorApi that simply forwards errors to consumers. + */ export class ErrorApiForwarder implements ErrorApi { private readonly subject = new PublishSubject<{ error: Error; diff --git a/packages/core/src/api/apis/implementations/ErrorApi/index.ts b/packages/core/src/api/apis/implementations/ErrorApi/index.ts new file mode 100644 index 0000000000..757dfd0d8f --- /dev/null +++ b/packages/core/src/api/apis/implementations/ErrorApi/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { ErrorAlerter } from './ErrorAlerter'; +export { ErrorApiForwarder } from './ErrorApiForwarder'; diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.test.ts rename to packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.ts b/packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestManager/MockOAuthApi.ts rename to packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.test.ts rename to packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts b/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthPendingRequests.ts rename to packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts b/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.test.ts rename to packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts b/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestManager/OAuthRequestManager.ts rename to packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestManager/index.ts b/packages/core/src/api/apis/implementations/OAuthRequestApi/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestManager/index.ts rename to packages/core/src/api/apis/implementations/OAuthRequestApi/index.ts diff --git a/packages/core/src/api/apis/implementations/index.ts b/packages/core/src/api/apis/implementations/index.ts index 1f2b91c16d..bb77cf5bd3 100644 --- a/packages/core/src/api/apis/implementations/index.ts +++ b/packages/core/src/api/apis/implementations/index.ts @@ -19,8 +19,8 @@ // Plugins should rely on these APIs for functionality as much as possible. export * from './auth'; -export * from './AppThemeSelector'; -export * from './AlertApiForwarder'; -export * from './ErrorAlerter'; -export * from './ErrorApiForwarder'; -export * from './OAuthRequestManager'; + +export * from './AlertApi'; +export * from './AppThemeApi'; +export * from './ErrorApi'; +export * from './OAuthRequestApi'; diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts index f3698b4e15..d4f95f7907 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -16,7 +16,7 @@ import ProviderIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from './DefaultAuthConnector'; -import MockOAuthApi from '../../OAuthRequestManager/MockOAuthApi'; +import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; const anyFetch = fetch as any; diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index 0cb0f2f1ea..cbd6d5ca46 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -20,8 +20,7 @@ import { SessionShouldRefreshFunc, } from './types'; import { AuthConnector } from '../AuthConnector'; -import { SessionScopeHelper } from './common'; -import { hasScopes } from '../../OAuthRequestManager/OAuthPendingRequests'; +import { SessionScopeHelper, hasScopes } from './common'; type Options = { /** The connector used for acting on the auth session */ From f6fc811d0a353fa4e5424720d4118315c7bbde2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Tue, 26 May 2020 16:45:24 +0200 Subject: [PATCH 26/58] Use spread --- plugins/catalog-backend/src/database/Database.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/plugins/catalog-backend/src/database/Database.ts b/plugins/catalog-backend/src/database/Database.ts index a3aba825f4..614f7d09bf 100644 --- a/plugins/catalog-backend/src/database/Database.ts +++ b/plugins/catalog-backend/src/database/Database.ts @@ -187,11 +187,12 @@ export class Database { } const newEntity = lodash.cloneDeep(request.entity); - newEntity.metadata = Object.assign({}, newEntity.metadata, { + newEntity.metadata = { + ...newEntity.metadata, uid: generateUid(), etag: generateEtag(), generation: 1, - }); + }; const newRow = toEntityRow(request.locationId, newEntity); await tx('entities').insert(newRow); @@ -276,11 +277,12 @@ export class Database { ? oldRow.generation : oldRow.generation + 1; const newEntity = lodash.cloneDeep(request.entity); - newEntity.metadata = Object.assign({}, newEntity.metadata, { + newEntity.metadata = { + ...newEntity.metadata, uid: oldRow.id, etag: newEtag, generation: newGeneration, - }); + }; // Preserve annotations that were set on the old version of the entity, // unless the new version overwrites them From 3a7e998a160f1ed91127c0864190a809b4dc3bed Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Tue, 26 May 2020 11:00:34 +0200 Subject: [PATCH 27/58] fix review comments, more error handling, moar tests --- .../src/providers/google/provider.test.ts | 54 ++++++++++++++++--- .../src/providers/google/provider.ts | 37 ++++++++++--- 2 files changed, 77 insertions(+), 14 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index 1726fbf33a..b692a35216 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -98,6 +98,7 @@ describe('GoogleAuthProvider', () => { it('should perform logout and respond with 200', () => { const mockResponse: any = ({ send: jest.fn(), + cookie: jest.fn(), } as unknown) as express.Response; const googleAuthProvider = new GoogleAuthProvider( @@ -111,6 +112,12 @@ describe('GoogleAuthProvider', () => { googleAuthProvider.logout(mockRequest, mockResponse); expect(spyResponse).toBeCalledTimes(1); expect(spyResponse).toBeCalledWith('logout!'); + expect(mockResponse.cookie).toBeCalledTimes(1); + expect(mockResponse.cookie).toBeCalledWith( + 'google-refresh-token', + '', + expect.objectContaining({ maxAge: 0 }), + ); }); }); @@ -145,7 +152,7 @@ describe('GoogleAuthProvider', () => { expect(spyPostMessage).toBeCalledTimes(1); expect(mockResponse.cookie).toBeCalledTimes(1); expect(mockResponse.cookie).toBeCalledWith( - 'grtoken', + 'google-refresh-token', 'REFRESH_TOKEN', expect.objectContaining({ path: '/auth/google', @@ -156,7 +163,7 @@ describe('GoogleAuthProvider', () => { ); }); - it('should respond with a 401 if no refresh token returned', () => { + it('should respond with a error message if no refresh token returned', () => { const spyPassport = jest .spyOn(passport, 'authenticate') .mockImplementation((_x, callbackFunc) => { @@ -165,16 +172,47 @@ describe('GoogleAuthProvider', () => { return jest.fn(); }); + const spyPostMessage = jest + .spyOn(utils, 'postMessageResponse') + .mockImplementation(() => jest.fn()); + const googleAuthProvider = new GoogleAuthProvider( googleAuthProviderConfig, ); googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); expect(spyPassport).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledTimes(1); - expect(mockResponse.send).toBeCalledWith('Failed to fetch refresh token'); - expect(mockResponse.status).toBeCalledTimes(1); - expect(mockResponse.status).toBeCalledWith(401); + expect(spyPostMessage).toBeCalledTimes(1); + expect(spyPostMessage).toBeCalledWith(mockResponse, { + type: 'auth-result', + error: new Error('Missing refresh token'), + }); + }); + + it('should respond with a error message if auth failed', () => { + const spyPassport = jest + .spyOn(passport, 'authenticate') + .mockImplementation((_x, callbackFunc) => { + const cb = callbackFunc as Function; + cb(new Error('TokenError'), null); + return jest.fn(); + }); + + const spyPostMessage = jest + .spyOn(utils, 'postMessageResponse') + .mockImplementation(() => jest.fn()); + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); + expect(spyPassport).toBeCalledTimes(1); + expect(spyPostMessage).toBeCalledTimes(1); + expect(spyPostMessage).toBeCalledWith(mockResponse, { + type: 'auth-result', + error: new Error('Google auth failed, Error: TokenError'), + }); }); }); @@ -224,7 +262,7 @@ describe('GoogleAuthProvider', () => { describe('refresh token cookie, no scope', () => { const mockRequest = ({ - cookies: { grtoken: 'REFRESH_TOKEN' }, + cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, query: {}, } as unknown) as express.Request; @@ -311,7 +349,7 @@ describe('GoogleAuthProvider', () => { describe('refresh token cookie and scope', () => { const mockRequest = ({ - cookies: { grtoken: 'REFRESH_TOKEN' }, + cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, query: { scope: 'a,b', }, diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 51845f638b..096260e044 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -55,11 +55,21 @@ export class GoogleAuthProvider res: express.Response, next: express.NextFunction, ) { - return passport.authenticate('google', (_, user) => { + return passport.authenticate('google', (err, user) => { + if (err) { + return postMessageResponse(res, { + type: 'auth-result', + error: new Error(`Google auth failed, ${err}`), + }); + } + const { refreshToken } = user; if (!refreshToken) { - return res.status(401).send('Failed to fetch refresh token'); + return postMessageResponse(res, { + type: 'auth-result', + error: new Error('Missing refresh token'), + }); } delete user.refreshToken; @@ -69,11 +79,15 @@ export class GoogleAuthProvider secure: false, sameSite: 'none', domain: 'localhost', - path: '/auth/google', + path: `/auth/${this.providerConfig.provider}`, httpOnly: true, }; - res.cookie('grtoken', refreshToken, options); + res.cookie( + `${this.providerConfig.provider}-refresh-token`, + refreshToken, + options, + ); return postMessageResponse(res, { type: 'auth-result', payload: user, @@ -82,11 +96,22 @@ export class GoogleAuthProvider } async logout(_req: express.Request, res: express.Response) { + const options: CookieOptions = { + maxAge: 0, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/${this.providerConfig.provider}`, + httpOnly: true, + }; + + res.cookie(`${this.providerConfig.provider}-refresh-token`, '', options); return res.send('logout!'); } async refresh(req: express.Request, res: express.Response) { - const refreshToken = req.cookies.grtoken; + const refreshToken = + req.cookies[`${this.providerConfig.provider}-refresh-token`]; if (!refreshToken) { return res.status(401).send('Missing session cookie'); @@ -96,7 +121,7 @@ export class GoogleAuthProvider const refreshTokenRequestParams = scope ? { scope } : {}; return refresh.requestNewAccessToken( - 'google', + this.providerConfig.provider, refreshToken, refreshTokenRequestParams, (err, accessToken, _refreshToken, params) => { From 77d1e7382d660d902b49139bb52f4e9315f2d608 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Tue, 26 May 2020 14:04:46 +0200 Subject: [PATCH 28/58] add check for x-requested-with header --- .../src/providers/google/provider.test.ts | 32 +++++++++++++++++-- .../src/providers/google/provider.ts | 12 +++++-- plugins/auth-backend/src/providers/utils.ts | 9 ++++++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index b692a35216..874b89d92c 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -52,11 +52,15 @@ describe('GoogleAuthProvider', () => { }); describe('start authentication handler', () => { - const mockResponse = ({} as unknown) as express.Response; + const mockResponse = ({ + send: jest.fn().mockReturnThis(), + status: jest.fn().mockReturnThis(), + } as unknown) as express.Response; const mockNext: express.NextFunction = jest.fn(); it('should initiate authenticate request with provided scopes', () => { const mockRequest = ({ + header: () => 'XMLHttpRequest', query: { scope: 'a,b', }, @@ -80,6 +84,7 @@ describe('GoogleAuthProvider', () => { it('should throw error if no scopes provided', () => { const mockRequest = ({ + header: () => 'XMLHttpRequest', query: {}, } as unknown) as express.Request; @@ -93,7 +98,9 @@ describe('GoogleAuthProvider', () => { }); describe('logout handler', () => { - const mockRequest = ({} as unknown) as express.Request; + const mockRequest = ({ + header: () => 'XMLHttpRequest', + } as unknown) as express.Request; it('should perform logout and respond with 200', () => { const mockResponse: any = ({ @@ -122,7 +129,9 @@ describe('GoogleAuthProvider', () => { }); describe('redirect frame handler', () => { - const mockRequest = ({} as unknown) as express.Request; + const mockRequest = ({ + header: () => 'XMLHttpRequest', + } as unknown) as express.Request; const mockResponse: any = ({ status: jest.fn().mockReturnThis(), send: jest.fn().mockReturnThis(), @@ -245,6 +254,7 @@ describe('GoogleAuthProvider', () => { it('should respond with a 401', () => { const mockRequest = ({ cookies: jest.fn(), + header: () => 'XMLHttpRequest', } as unknown) as express.Request; const googleAuthProvider = new GoogleAuthProvider( @@ -262,6 +272,7 @@ describe('GoogleAuthProvider', () => { describe('refresh token cookie, no scope', () => { const mockRequest = ({ + header: () => 'XMLHttpRequest', cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, query: {}, } as unknown) as express.Request; @@ -349,6 +360,7 @@ describe('GoogleAuthProvider', () => { describe('refresh token cookie and scope', () => { const mockRequest = ({ + header: () => 'XMLHttpRequest', cookies: { 'google-refresh-token': 'REFRESH_TOKEN' }, query: { scope: 'a,b', @@ -387,6 +399,20 @@ describe('GoogleAuthProvider', () => { scope: 'a,b', }); }); + + it('ensures x-requested-with header', () => { + const mockHeaderRequest = ({ + header: () => 'TEST', + } as unknown) as express.Request; + + googleAuthProvider.refresh(mockHeaderRequest, mockResponse); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith( + 'Invalid X-Requested-With header', + ); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + }); }); }); }); diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 096260e044..35c7b3f6e2 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -23,7 +23,7 @@ import { AuthProviderRouteHandlers, AuthProviderConfig, } from './../types'; -import { postMessageResponse } from './../utils'; +import { postMessageResponse, ensuresXRequestedWith } from './../utils'; import { InputError } from '@backstage/backend-common'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; @@ -95,7 +95,11 @@ export class GoogleAuthProvider })(req, res, next); } - async logout(_req: express.Request, res: express.Response) { + async logout(req: express.Request, res: express.Response) { + if (!ensuresXRequestedWith(req)) { + return res.status(401).send('Invalid X-Requested-With header'); + } + const options: CookieOptions = { maxAge: 0, secure: false, @@ -110,6 +114,10 @@ export class GoogleAuthProvider } async refresh(req: express.Request, res: express.Response) { + if (!ensuresXRequestedWith(req)) { + return res.status(401).send('Invalid X-Requested-With header'); + } + const refreshToken = req.cookies[`${this.providerConfig.provider}-refresh-token`]; diff --git a/plugins/auth-backend/src/providers/utils.ts b/plugins/auth-backend/src/providers/utils.ts index 7fb906ad7c..83229e55d4 100644 --- a/plugins/auth-backend/src/providers/utils.ts +++ b/plugins/auth-backend/src/providers/utils.ts @@ -39,3 +39,12 @@ export const postMessageResponse = ( `); }; + +export const ensuresXRequestedWith = (req: express.Request) => { + const requiredHeader = req.header('X-Requested-With'); + + if (!requiredHeader || requiredHeader !== 'XMLHttpRequest') { + return false; + } + return true; +}; From fccdb7e3ef4da63e158841cc735c07ad9c54b621 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Tue, 26 May 2020 14:55:24 +0200 Subject: [PATCH 29/58] add nonce check for the google auth dance in popup --- .../src/providers/google/provider.ts | 42 ++++++++++++++----- 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 35c7b3f6e2..6850c527f1 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -16,6 +16,7 @@ import passport from 'passport'; import express, { CookieOptions } from 'express'; +import crypto from 'crypto'; import { Strategy as GoogleStrategy } from 'passport-google-oauth20'; import refresh from 'passport-oauth2-refresh'; import { @@ -27,6 +28,7 @@ import { postMessageResponse, ensuresXRequestedWith } from './../utils'; import { InputError } from '@backstage/backend-common'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; +const TEN_MINUTES_MS = 600 * 1000; export class GoogleAuthProvider implements AuthProvider, AuthProviderRouteHandlers { private readonly providerConfig: AuthProviderConfig; @@ -39,6 +41,19 @@ export class GoogleAuthProvider res: express.Response, next: express.NextFunction, ) { + const nonce = crypto.randomBytes(16).toString('base64'); + + const options: CookieOptions = { + maxAge: TEN_MINUTES_MS, + secure: false, + sameSite: 'none', + domain: 'localhost', + path: `/auth/google/handler`, + httpOnly: true, + }; + + res.cookie(`google-nonce`, nonce, options); + const scope = req.query.scope?.toString() ?? ''; if (!scope) { throw new InputError('missing scope parameter'); @@ -47,6 +62,7 @@ export class GoogleAuthProvider scope, accessType: 'offline', prompt: 'consent', + state: nonce, })(req, res, next); } @@ -55,6 +71,17 @@ export class GoogleAuthProvider res: express.Response, next: express.NextFunction, ) { + const cookieNonce = req.cookies[`google-nonce`]; + const stateNonce = req.query.state; + + if (!cookieNonce || !stateNonce) { + return res.status(401).send('Missing nonce'); + } + + if (cookieNonce !== stateNonce) { + return res.status(401).send('Invalid nonce'); + } + return passport.authenticate('google', (err, user) => { if (err) { return postMessageResponse(res, { @@ -79,15 +106,11 @@ export class GoogleAuthProvider secure: false, sameSite: 'none', domain: 'localhost', - path: `/auth/${this.providerConfig.provider}`, + path: `/auth/google`, httpOnly: true, }; - res.cookie( - `${this.providerConfig.provider}-refresh-token`, - refreshToken, - options, - ); + res.cookie(`google-refresh-token`, refreshToken, options); return postMessageResponse(res, { type: 'auth-result', payload: user, @@ -105,11 +128,11 @@ export class GoogleAuthProvider secure: false, sameSite: 'none', domain: 'localhost', - path: `/auth/${this.providerConfig.provider}`, + path: `/auth/google`, httpOnly: true, }; - res.cookie(`${this.providerConfig.provider}-refresh-token`, '', options); + res.cookie(`google-refresh-token`, '', options); return res.send('logout!'); } @@ -118,8 +141,7 @@ export class GoogleAuthProvider return res.status(401).send('Invalid X-Requested-With header'); } - const refreshToken = - req.cookies[`${this.providerConfig.provider}-refresh-token`]; + const refreshToken = req.cookies[`google-refresh-token`]; if (!refreshToken) { return res.status(401).send('Missing session cookie'); From 12d41d0f0a51b1ffa97ee95a70aff818c09ef7b7 Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Tue, 26 May 2020 16:43:08 +0200 Subject: [PATCH 30/58] use providerFactory.provider instead of hardcoding google --- .../src/providers/google/provider.ts | 21 ++++++++++++------- plugins/auth-backend/src/providers/index.ts | 8 +++---- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index 6850c527f1..a631f86fa6 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -48,11 +48,11 @@ export class GoogleAuthProvider secure: false, sameSite: 'none', domain: 'localhost', - path: `/auth/google/handler`, + path: `/auth/${this.providerConfig.provider}/handler`, httpOnly: true, }; - res.cookie(`google-nonce`, nonce, options); + res.cookie(`${this.providerConfig.provider}-nonce`, nonce, options); const scope = req.query.scope?.toString() ?? ''; if (!scope) { @@ -71,7 +71,7 @@ export class GoogleAuthProvider res: express.Response, next: express.NextFunction, ) { - const cookieNonce = req.cookies[`google-nonce`]; + const cookieNonce = req.cookies[`${this.providerConfig.provider}-nonce`]; const stateNonce = req.query.state; if (!cookieNonce || !stateNonce) { @@ -106,11 +106,15 @@ export class GoogleAuthProvider secure: false, sameSite: 'none', domain: 'localhost', - path: `/auth/google`, + path: `/auth/${this.providerConfig.provider}`, httpOnly: true, }; - res.cookie(`google-refresh-token`, refreshToken, options); + res.cookie( + `${this.providerConfig.provider}-refresh-token`, + refreshToken, + options, + ); return postMessageResponse(res, { type: 'auth-result', payload: user, @@ -128,11 +132,11 @@ export class GoogleAuthProvider secure: false, sameSite: 'none', domain: 'localhost', - path: `/auth/google`, + path: `/auth/${this.providerConfig.provider}`, httpOnly: true, }; - res.cookie(`google-refresh-token`, '', options); + res.cookie(`${this.providerConfig.provider}-refresh-token`, '', options); return res.send('logout!'); } @@ -141,7 +145,8 @@ export class GoogleAuthProvider return res.status(401).send('Invalid X-Requested-With header'); } - const refreshToken = req.cookies[`google-refresh-token`]; + const refreshToken = + req.cookies[`${this.providerConfig.provider}-refresh-token`]; if (!refreshToken) { return res.status(401).send('Missing session cookie'); diff --git a/plugins/auth-backend/src/providers/index.ts b/plugins/auth-backend/src/providers/index.ts index 68fe61c7a6..1b33391c27 100644 --- a/plugins/auth-backend/src/providers/index.ts +++ b/plugins/auth-backend/src/providers/index.ts @@ -20,11 +20,11 @@ import { ProviderFactories } from './factories'; export const defaultRouter = (provider: AuthProviderRouteHandlers) => { const router = Router(); - router.get('/start', provider.start); - router.get('/handler/frame', provider.frameHandler); - router.get('/logout', provider.logout); + router.get('/start', provider.start.bind(provider)); + router.get('/handler/frame', provider.frameHandler.bind(provider)); + router.get('/logout', provider.logout.bind(provider)); if (provider.refresh) { - router.get('/refresh', provider.refresh); + router.get('/refresh', provider.refresh.bind(provider)); } return router; }; From c954f9d5ec6c485f2ff5f47f19163eb0918e7d42 Mon Sep 17 00:00:00 2001 From: Ben Lambert Date: Tue, 26 May 2020 20:06:21 +0200 Subject: [PATCH 31/58] Catalog Filters (#1000) * feat(catalog-plugin): Starting to add the sidebar filters component for the different filters applicable * chore(catalog-frontend): fixing pr code review comments * feat(packages/core): export IconComponent for use in other packages * chore(catalog-frontend): Added some tests for the catalog filter component to make sure it renders the correct data * feat(catalog-frontend): added the ability for click handlers when changing selected filter * feat(catalog-frontend): store state in the page component for active filter and moving out the mock data to a better place * feat(catalog-frontend): reworking the selected state and fixing the selected text on the table when you choose the correct filter --- packages/core/src/icons/types.ts | 1 - packages/core/src/index.ts | 1 + .../CatalogFilter/CatalogFilter.test.tsx | 131 ++++++++++++++++++ .../CatalogFilter/CatalogFilter.tsx | 117 ++++++++++++++++ .../src/components/CatalogFilter/index.ts | 17 +++ .../CatalogPage/CatalogPage.test.tsx | 12 +- .../components/CatalogPage/CatalogPage.tsx | 47 ++++++- .../CatalogTable/CatalogTable.test.tsx | 14 +- .../components/CatalogTable/CatalogTable.tsx | 4 +- plugins/catalog/src/data/filters.ts | 53 +++++++ 10 files changed, 382 insertions(+), 15 deletions(-) create mode 100644 plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx create mode 100644 plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx create mode 100644 plugins/catalog/src/components/CatalogFilter/index.ts create mode 100644 plugins/catalog/src/data/filters.ts diff --git a/packages/core/src/icons/types.ts b/packages/core/src/icons/types.ts index 30e0ba53b2..599cb969df 100644 --- a/packages/core/src/icons/types.ts +++ b/packages/core/src/icons/types.ts @@ -16,7 +16,6 @@ import { ComponentType } from 'react'; import { SvgIconProps } from '@material-ui/core'; - export type IconComponent = ComponentType; export type SystemIconKey = 'user' | 'group'; export type SystemIcons = { [key in SystemIconKey]: IconComponent }; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 013c80e2fa..816ad92578 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -46,3 +46,4 @@ export { default as TrendLine } from './components/TrendLine'; export { FeatureCalloutCircular } from './components/FeatureDiscovery/FeatureCalloutCircular'; export * from './components/Status'; export { default as WarningPanel } from './components/WarningPanel'; +export type { IconComponent } from './icons'; diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx new file mode 100644 index 0000000000..762a881302 --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.test.tsx @@ -0,0 +1,131 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { render, fireEvent } from '@testing-library/react'; +import { wrapInThemedTestApp } from '@backstage/test-utils'; +import { CatalogFilter, CatalogFilterGroup } from './CatalogFilter'; + +describe('Catalog Filter', () => { + it('should render the different groups', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { name: 'Test Group 1', items: [] }, + { name: 'Test Group 2', items: [] }, + ]; + const { findByText } = render( + wrapInThemedTestApp(), + ); + + for (const group of mockGroups) { + expect(await findByText(group.name)).toBeInTheDocument(); + } + }); + + it('should render the different items and their names', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'first', + label: 'First Label', + }, + { + id: 'second', + label: 'Second Label', + }, + ], + }, + ]; + + const { findByText } = render( + wrapInThemedTestApp(), + ); + + const [group] = mockGroups; + for (const item of group.items) { + expect(await findByText(item.label)).toBeInTheDocument(); + } + }); + + it('should render the count in each item', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'first', + label: 'First Label', + count: 100, + }, + { + id: 'second', + label: 'Second Label', + count: 400, + }, + ], + }, + ]; + + const { findByText } = render( + wrapInThemedTestApp(), + ); + + const [group] = mockGroups; + for (const item of group.items) { + expect(await findByText(item.count!.toString())).toBeInTheDocument(); + } + }); + + it('should fire the callback when an item is clicked', async () => { + const mockGroups: CatalogFilterGroup[] = [ + { + name: 'Test Group 1', + items: [ + { + id: 'first', + label: 'First Label', + count: 100, + }, + { + id: 'second', + label: 'Second Label', + count: 400, + }, + ], + }, + ]; + + const onSelectedChangeHandler = jest.fn(); + + const { findByText } = render( + wrapInThemedTestApp( + , + ), + ); + + const item = mockGroups[0].items[0]; + + const element = await findByText(item.label); + + fireEvent.click(element); + + expect(onSelectedChangeHandler).toHaveBeenCalledWith(item); + }); +}); diff --git a/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx new file mode 100644 index 0000000000..eb45d8a80b --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/CatalogFilter.tsx @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { + Card, + List, + ListItemIcon, + ListItemText, + MenuItem, + Typography, + Theme, + makeStyles, +} from '@material-ui/core'; +import type { IconComponent } from '@backstage/core'; + +export type CatalogFilterItem = { + id: string; + label: string; + icon?: IconComponent; + count?: number; + loading?: boolean; +}; + +export type CatalogFilterGroup = { + name: string; + items: CatalogFilterItem[]; +}; + +export type CatalogFilterProps = { + groups: CatalogFilterGroup[]; + selectedId?: string; + onSelectedChange?: (item: CatalogFilterItem) => void; +}; + +const useStyles = makeStyles(theme => ({ + root: { + backgroundColor: 'rgba(0, 0, 0, .11)', + boxShadow: 'none', + }, + title: { + margin: theme.spacing(1, 0, 0, 1), + textTransform: 'uppercase', + fontSize: 12, + fontWeight: 'bold', + }, + listIcon: { + minWidth: 30, + color: theme.palette.text.primary, + }, + menuItem: { + minHeight: theme.spacing(6), + }, + groupWrapper: { + margin: theme.spacing(1, 1, 2, 1), + }, + menuTitle: { + fontWeight: 500, + }, +})); + +export const CatalogFilter: React.FC = ({ + groups, + selectedId, + onSelectedChange, +}) => { + const classes = useStyles(); + return ( + + {groups.map(group => ( + + + {group.name} + + + + {group.items.map(item => ( + onSelectedChange?.(item)} + selected={item.id === selectedId} + className={classes.menuItem} + > + {item.icon && ( + + + + )} + + + {item.label} + + + {item.count} + + ))} + + + + ))} + + ); +}; diff --git a/plugins/catalog/src/components/CatalogFilter/index.ts b/plugins/catalog/src/components/CatalogFilter/index.ts new file mode 100644 index 0000000000..5103b16307 --- /dev/null +++ b/plugins/catalog/src/components/CatalogFilter/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { CatalogFilter } from './CatalogFilter'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx index 60af5f7d24..5dee366696 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.test.tsx @@ -17,8 +17,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import CatalogPage from './CatalogPage'; -import { ThemeProvider } from '@material-ui/core'; -import { lightTheme } from '@backstage/theme'; +import { wrapInThemedTestApp } from '@backstage/test-utils'; import { ComponentFactory } from '../../data/component'; const testComponentFactory: ComponentFactory = { @@ -28,11 +27,14 @@ const testComponentFactory: ComponentFactory = { }; describe('CatalogPage', () => { + // this test right now causes some red lines in the log output when running tests + // related to some theme issues in mui-table + // https://github.com/mbrn/material-table/issues/1293 it('should render', async () => { const rendered = render( - - - , + wrapInThemedTestApp( + , + ), ); expect( await rendered.findByText('Keep track of your software'), diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index ec6c1cf09f..f5094542a5 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -27,13 +27,38 @@ import { import { useAsync } from 'react-use'; import { ComponentFactory } from '../../data/component'; import CatalogTable from '../CatalogTable/CatalogTable'; -import { Button } from '@material-ui/core'; +import { + CatalogFilter, + CatalogFilterItem, +} from '../CatalogFilter/CatalogFilter'; +import { Button, makeStyles } from '@material-ui/core'; +import { filterGroups, defaultFilter } from '../../data/filters'; + +const useStyles = makeStyles(theme => ({ + contentWrapper: { + display: 'grid', + gridTemplateAreas: "'filters' 'table'", + gridTemplateColumns: '250px 1fr', + gridColumnGap: theme.spacing(2), + }, +})); type CatalogPageProps = { componentFactory: ComponentFactory; }; + const CatalogPage: FC = ({ componentFactory }) => { const { value, error, loading } = useAsync(componentFactory.getAllComponents); + const [selectedFilter, setSelectedFilter] = React.useState( + defaultFilter, + ); + + const onFilterSelected = React.useCallback( + selected => setSelectedFilter(selected), + [], + ); + + const styles = useStyles(); return (
@@ -46,11 +71,21 @@ const CatalogPage: FC = ({ componentFactory }) => { All your components - +
+
+ +
+ +
); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx index 99debb12d4..9bb1db4519 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.test.tsx @@ -28,7 +28,9 @@ const components: Component[] = [ describe('CatalogTable component', () => { it('should render loading when loading prop it set to true', async () => { const rendered = render( - wrapInThemedTestApp(), + wrapInThemedTestApp( + , + ), ); const progress = await rendered.findByTestId('progress'); expect(progress).toBeInTheDocument(); @@ -38,6 +40,7 @@ describe('CatalogTable component', () => { const rendered = render( wrapInThemedTestApp( { it('should display component names when loading has finished and no error occurred', async () => { const rendered = render( wrapInThemedTestApp( - , + , ), ); + expect( + await rendered.findByText(`Owned (${components.length})`), + ).toBeInTheDocument(); expect(await rendered.findByText('component1')).toBeInTheDocument(); expect(await rendered.findByText('component2')).toBeInTheDocument(); expect(await rendered.findByText('component3')).toBeInTheDocument(); diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 57f1dfe7f5..537deb1b30 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -63,6 +63,7 @@ const columns: TableColumn[] = [ type CatalogTableProps = { components: Component[]; + titlePreamble: string; loading: boolean; error?: any; }; @@ -70,6 +71,7 @@ const CatalogTable: FC = ({ components, loading, error, + titlePreamble, }) => { if (loading) { return ; @@ -87,7 +89,7 @@ const CatalogTable: FC = ({
); diff --git a/plugins/catalog/src/data/filters.ts b/plugins/catalog/src/data/filters.ts new file mode 100644 index 0000000000..66eb55b478 --- /dev/null +++ b/plugins/catalog/src/data/filters.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { + CatalogFilterGroup, + CatalogFilterItem, +} from '../components/CatalogFilter/CatalogFilter'; +import SettingsIcon from '@material-ui/icons/Settings'; +import StarIcon from '@material-ui/icons/Star'; + +export const filterGroups: CatalogFilterGroup[] = [ + { + name: 'Personal', + items: [ + { + id: 'owned', + label: 'Owned', + count: 123, + icon: SettingsIcon, + }, + { + id: 'starred', + label: 'Starred', + count: 10, + icon: StarIcon, + }, + ], + }, + { + name: 'Spotify', + items: [ + { + id: 'all', + label: 'All Services', + count: 123, + }, + ], + }, +]; + +export const defaultFilter: CatalogFilterItem = filterGroups[0].items[0]; From 4c4b48490bab5939d2fdfd190bbf1792ebbc5313 Mon Sep 17 00:00:00 2001 From: Niklas Ek Date: Tue, 26 May 2020 21:16:41 +0200 Subject: [PATCH 32/58] Remove WIP label on Storybook in readme (#1014) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 086c1ace60..2e22818f4b 100644 --- a/README.md +++ b/README.md @@ -85,7 +85,7 @@ Take a look at the [Getting Started](docs/getting-started/README.md) guide to le - [Architecture](docs/architecture-terminology.md) - [API references](docs/reference/README.md) - [Designing for Backstage](docs/design.md) -- [Storybook - UI components](http://storybook.backstage.io) ([WIP](https://github.com/spotify/backstage/milestone/9)) +- [Storybook - UI components](http://storybook.backstage.io) - [Contributing to Storybook](docs/getting-started/contributing-to-storybook.md) - Using Backstage components (TODO) From 1042635159e3ee80461b971326c3fbaea4affe9b Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 26 May 2020 21:20:14 +0200 Subject: [PATCH 33/58] build(deps): bump rollup from 2.3.2 to 2.10.9 (#1004) Bumps [rollup](https://github.com/rollup/rollup) from 2.3.2 to 2.10.9. - [Release notes](https://github.com/rollup/rollup/releases) - [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md) - [Commits](https://github.com/rollup/rollup/compare/v2.3.2...v2.10.9) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c23e7a42a5..c2398e238c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18370,9 +18370,9 @@ rollup@^0.63.4: "@types/node" "*" rollup@^2.3.2: - version "2.3.2" - resolved "https://registry.npmjs.org/rollup/-/rollup-2.3.2.tgz#afa68e4f3325bcef4e150d082056bef450bcac60" - integrity sha512-p66+fbfaUUOGE84sHXAOgfeaYQMslgAazoQMp//nlR519R61213EPFgrMZa48j31jNacJwexSAR1Q8V/BwGKBA== + version "2.10.9" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.10.9.tgz#17dcc6753c619efcc1be2cf61d73a87827eebdf9" + integrity sha512-dY/EbjiWC17ZCUSyk14hkxATAMAShkMsD43XmZGWjLrgFj15M3Dw2kEkA9ns64BiLFm9PKN6vTQw8neHwK74eg== optionalDependencies: fsevents "~2.1.2" From 93263f6bd103128359f86514f0df57d451327d3f Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 26 May 2020 22:59:26 +0200 Subject: [PATCH 34/58] build(deps-dev): bump @types/uuid from 7.0.3 to 8.0.0 (#944) Bumps [@types/uuid](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/HEAD/types/uuid) from 7.0.3 to 8.0.0. - [Release notes](https://github.com/DefinitelyTyped/DefinitelyTyped/releases) - [Commits](https://github.com/DefinitelyTyped/DefinitelyTyped/commits/HEAD/types/uuid) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- plugins/catalog-backend/package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/plugins/catalog-backend/package.json b/plugins/catalog-backend/package.json index ce2a985efa..f32ffffb6c 100644 --- a/plugins/catalog-backend/package.json +++ b/plugins/catalog-backend/package.json @@ -38,7 +38,7 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", "@types/lodash": "^4.14.151", - "@types/uuid": "^7.0.3", + "@types/uuid": "^8.0.0", "@types/yup": "^0.28.2", "jest-fetch-mock": "^3.0.3", "supertest": "^4.0.2", diff --git a/yarn.lock b/yarn.lock index c2398e238c..a255b7355c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4510,10 +4510,10 @@ dependencies: source-map "^0.6.1" -"@types/uuid@^7.0.3": - version "7.0.3" - resolved "https://registry.npmjs.org/@types/uuid/-/uuid-7.0.3.tgz#45cd03e98e758f8581c79c535afbd4fc27ba7ac8" - integrity sha512-PUdqTZVrNYTNcIhLHkiaYzoOIaUi5LFg/XLerAdgvwQrUCx+oSbtoBze1AMyvYbcwzUSNC+Isl58SM4Sm/6COw== +"@types/uuid@^8.0.0": + version "8.0.0" + resolved "https://registry.npmjs.org/@types/uuid/-/uuid-8.0.0.tgz#165aae4819ad2174a17476dbe66feebd549556c0" + integrity sha512-xSQfNcvOiE5f9dyd4Kzxbof1aTrLobL278pGLKOZI6esGfZ7ts9Ka16CzIN6Y8hFHE1C7jIBZokULhK1bOgjRw== "@types/webpack-dev-server@*", "@types/webpack-dev-server@^3.10.0": version "3.10.1" From 147926fbd6a35a9583df6d9d03578c14419ac992 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Tue, 26 May 2020 23:01:05 +0200 Subject: [PATCH 35/58] build(deps): bump fork-ts-checker-webpack-plugin from 4.1.0 to 4.1.5 (#1009) Bumps [fork-ts-checker-webpack-plugin](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin) from 4.1.0 to 4.1.5. - [Release notes](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/releases) - [Changelog](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/blob/master/CHANGELOG.md) - [Commits](https://github.com/TypeStrong/fork-ts-checker-webpack-plugin/compare/v4.1.0...v4.1.5) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index a255b7355c..d535894e20 100644 --- a/yarn.lock +++ b/yarn.lock @@ -9,7 +9,7 @@ dependencies: "@babel/highlight" "^7.0.0" -"@babel/code-frame@7.8.3", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.8.3": +"@babel/code-frame@7.8.3", "@babel/code-frame@^7.0.0", "@babel/code-frame@^7.5.5", "@babel/code-frame@^7.8.3": version "7.8.3" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.8.3.tgz#33e25903d7481181534e12ec0a25f16b6fcf419e" integrity sha512-a9gxpmdXtZEInkCSHUJDLHZVBgb1QS0jhss4cPP93EW7s+uC5bikET2twEF3KV+7rDblJcmNvTR7VJejqd2C2g== @@ -9796,11 +9796,11 @@ fork-ts-checker-webpack-plugin@3.1.1: worker-rpc "^0.1.0" fork-ts-checker-webpack-plugin@^4.0.5: - version "4.1.0" - resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.0.tgz#62bffe704426770fee33f15f0c0d56c86297fefd" - integrity sha512-2DLwUVUR/AdNmMD2utfmSR8r4qHRFhnfL6QQDQS5q4g5uBZzXYDgg8MXPIbu0HzyLjyvbogqjBNKILG5fufwzg== + version "4.1.5" + resolved "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-4.1.5.tgz#780d52c65183742d8c885fff42a9ec9ea7006672" + integrity sha512-nuD4IDqoOfkEIlS6shhjLGaLBDSNyVJulAlr5lFbPe0saGqlsTo+/HmhtIrs/cNLFtmaudL10byivhxr+Qhh4w== dependencies: - babel-code-frame "^6.22.0" + "@babel/code-frame" "^7.5.5" chalk "^2.4.1" micromatch "^3.1.10" minimatch "^3.0.4" From ec16280876c2c8c70b4af394f191423e5c955bf8 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 27 May 2020 08:51:48 +0200 Subject: [PATCH 36/58] build(deps-dev): bump typescript from 3.9.2 to 3.9.3 (#1024) Bumps [typescript](https://github.com/Microsoft/TypeScript) from 3.9.2 to 3.9.3. - [Release notes](https://github.com/Microsoft/TypeScript/releases) - [Commits](https://github.com/Microsoft/TypeScript/compare/v3.9.2...v3.9.3) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index d535894e20..fdca7768a5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -20469,15 +20469,10 @@ typedarray@^0.0.6: resolved "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" integrity sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c= -typescript@^3.7.4: - version "3.8.3" - resolved "https://registry.npmjs.org/typescript/-/typescript-3.8.3.tgz#409eb8544ea0335711205869ec458ab109ee1061" - integrity sha512-MYlEfn5VrLNsgudQTVJeNaQFUAI7DkhnOjdpAp4T+ku1TfQClewlbSuTVHiA+8skNBgaf02TL/kLOvig4y3G8w== - -typescript@^3.9.2: - version "3.9.2" - resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.2.tgz#64e9c8e9be6ea583c54607677dd4680a1cf35db9" - integrity sha512-q2ktq4n/uLuNNShyayit+DTobV2ApPEo/6so68JaD5ojvc/6GClBipedB9zNWYxRSAlZXAe405Rlijzl6qDiSw== +typescript@^3.7.4, typescript@^3.9.2: + version "3.9.3" + resolved "https://registry.npmjs.org/typescript/-/typescript-3.9.3.tgz#d3ac8883a97c26139e42df5e93eeece33d610b8a" + integrity sha512-D/wqnB2xzNFIcoBG9FG8cXRDjiqSTbG2wd8DMZeQyJlP1vfTkIxH4GKveWaEBYySKIg+USu+E+EDIR47SqnaMQ== uc.micro@^1.0.1, uc.micro@^1.0.5: version "1.0.6" From 4a93c608a333bbbff47da09ff9808111a126b176 Mon Sep 17 00:00:00 2001 From: "dependabot-preview[bot]" <27856297+dependabot-preview[bot]@users.noreply.github.com> Date: Wed, 27 May 2020 08:52:23 +0200 Subject: [PATCH 37/58] build(deps): bump webpack-dev-server from 3.10.3 to 3.11.0 (#1022) Bumps [webpack-dev-server](https://github.com/webpack/webpack-dev-server) from 3.10.3 to 3.11.0. - [Release notes](https://github.com/webpack/webpack-dev-server/releases) - [Changelog](https://github.com/webpack/webpack-dev-server/blob/master/CHANGELOG.md) - [Commits](https://github.com/webpack/webpack-dev-server/compare/v3.10.3...v3.11.0) Signed-off-by: dependabot-preview[bot] Co-authored-by: dependabot-preview[bot] <27856297+dependabot-preview[bot]@users.noreply.github.com> --- yarn.lock | 133 ++++++++++++++++++++++++++---------------------------- 1 file changed, 63 insertions(+), 70 deletions(-) diff --git a/yarn.lock b/yarn.lock index fdca7768a5..772229aa98 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10903,10 +10903,10 @@ html-encoding-sniffer@^2.0.1: dependencies: whatwg-encoding "^1.0.5" -html-entities@^1.2.0, html-entities@^1.2.1: - version "1.2.1" - resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.2.1.tgz#0df29351f0721163515dfb9e5543e5f6eed5162f" - integrity sha1-DfKTUfByEWNRXfueVUPl9u7VFi8= +html-entities@^1.2.0, html-entities@^1.3.1: + version "1.3.1" + resolved "https://registry.npmjs.org/html-entities/-/html-entities-1.3.1.tgz#fb9a1a4b5b14c5daba82d3e34c6ae4fe701a0e44" + integrity sha512-rhE/4Z3hIhzHAUKbW8jVcCyuT5oJCXXqhN/6mXXVCpzTmvJnoH2HL/bt3EZ6p55jbFJBeAe1ZNpL5BugLujxNA== html-escaper@^2.0.0: version "2.0.1" @@ -13958,10 +13958,10 @@ logform@^2.1.1: ms "^2.1.1" triple-beam "^1.3.0" -loglevel@^1.6.6: - version "1.6.7" - resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.7.tgz#b3e034233188c68b889f5b862415306f565e2c56" - integrity sha512-cY2eLFrQSAfVPhCgH1s7JI73tMbg9YC3v3+ZHVW67sBS7UxWzNEk/ZBbSfLykBWHp33dqqtOv82gjhKEi81T/A== +loglevel@^1.6.8: + version "1.6.8" + resolved "https://registry.npmjs.org/loglevel/-/loglevel-1.6.8.tgz#8a25fb75d092230ecd4457270d80b54e28011171" + integrity sha512-bsU7+gc9AJ2SqpzxwU3+1fedl8zAntbtC5XYlt3s2j1hJcn2PsXSmgN8TaLG/J1/2mod4+cE/3vNL70/c1RNCA== lolex@^5.0.0: version "5.1.2" @@ -15594,7 +15594,7 @@ os-locale@^2.0.0: lcid "^1.0.0" mem "^1.1.0" -os-locale@^3.0.0, os-locale@^3.1.0: +os-locale@^3.1.0: version "3.1.0" resolved "https://registry.npmjs.org/os-locale/-/os-locale-3.1.0.tgz#a802a6ee17f24c10483ab9935719cef4ed16bf1a" integrity sha512-Z8l3R4wYWM40/52Z+S265okfFj8Kt2cC2MKY+xNi3kFs+XGI7WXu/I309QQQYbRW4ijiZ+yxs9pqEhJh0DqW3Q== @@ -16306,10 +16306,10 @@ popper.js@^1.14.1, popper.js@^1.14.4, popper.js@^1.14.7: resolved "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1.tgz#2a223cb3dc7b6213d740e40372be40de43e65b1b" integrity sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ== -portfinder@^1.0.25: - version "1.0.25" - resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.25.tgz#254fd337ffba869f4b9d37edc298059cb4d35eca" - integrity sha512-6ElJnHBbxVA1XSLgBp7G1FiCkQdlqGzuF7DswL5tcea+E8UpuvPU7beVAjjRwCioTS9ZluNbu+ZyRvgTsmqEBg== +portfinder@^1.0.26: + version "1.0.26" + resolved "https://registry.npmjs.org/portfinder/-/portfinder-1.0.26.tgz#475658d56ca30bed72ac7f1378ed350bd1b64e70" + integrity sha512-Xi7mKxJHHMI3rIUrnm/jjUgwhbYMkp/XKEcZX3aG4BrumLpq3nmoQMX+ClYnDZnZ/New7IatC1no5RX0zo1vXQ== dependencies: async "^2.6.2" debug "^3.1.1" @@ -18504,15 +18504,7 @@ schema-utils@^1.0.0: ajv-errors "^1.0.0" ajv-keywords "^3.1.0" -schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.4: - version "2.6.5" - resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.5.tgz#c758f0a7e624263073d396e29cd40aa101152d8a" - integrity sha512-5KXuwKziQrTVHh8j/Uxz+QUbxkaLW9X/86NBlx/gnKgtsZA2GIVMUn17qWhRFwF8jdYb3Dig5hRO/W5mZqy6SQ== - dependencies: - ajv "^6.12.0" - ajv-keywords "^3.4.1" - -schema-utils@^2.6.5, schema-utils@^2.6.6: +schema-utils@^2.0.1, schema-utils@^2.5.0, schema-utils@^2.6.0, schema-utils@^2.6.4, schema-utils@^2.6.5, schema-utils@^2.6.6: version "2.6.6" resolved "https://registry.npmjs.org/schema-utils/-/schema-utils-2.6.6.tgz#299fe6bd4a3365dc23d99fd446caff8f1d6c330c" integrity sha512-wHutF/WPSbIi9x6ctjGGk2Hvl0VOz5l3EKEuKbjPlB30mKZUzb9A5k9yEXRX3pwyqVLPvpfZZEllaFq/M718hA== @@ -18942,13 +18934,14 @@ sockjs-client@1.4.0: json3 "^3.3.2" url-parse "^1.4.3" -sockjs@0.3.19: - version "0.3.19" - resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.19.tgz#d976bbe800af7bd20ae08598d582393508993c0d" - integrity sha512-V48klKZl8T6MzatbLlzzRNhMepEys9Y4oGFpypBFFn1gLI/QQ9HtLLyWJNbPlwGLelOVOEijUbTTJeLLI59jLw== +sockjs@0.3.20: + version "0.3.20" + resolved "https://registry.npmjs.org/sockjs/-/sockjs-0.3.20.tgz#b26a283ec562ef8b2687b44033a4eeceac75d855" + integrity sha512-SpmVOVpdq0DJc0qArhF3E5xsxvaiqGNb73XfgBpK1y3UD5gs8DSo8aCTsuT5pX8rssdc2NDIzANwP9eCAiSdTA== dependencies: faye-websocket "^0.10.0" - uuid "^3.0.1" + uuid "^3.4.0" + websocket-driver "0.6.5" socks-proxy-agent@^4.0.0: version "4.0.2" @@ -19095,10 +19088,10 @@ spdy-transport@^3.0.0: readable-stream "^3.0.6" wbuf "^1.7.3" -spdy@^4.0.1: - version "4.0.1" - resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.1.tgz#6f12ed1c5db7ea4f24ebb8b89ba58c87c08257f2" - integrity sha512-HeZS3PBdMA+sZSu0qwpCxl3DeALD5ASx8pAX0jZdKXSpPWbQ6SYGnlg3BBmYLx5LtiZrmkAZfErCm2oECBcioA== +spdy@^4.0.2: + version "4.0.2" + resolved "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz#b74f466203a3eda452c02492b91fb9e84a27677b" + integrity sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA== dependencies: debug "^4.1.0" handle-thing "^2.0.0" @@ -20892,7 +20885,7 @@ utils-merge@1.0.1, utils-merge@1.x.x: resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" integrity sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM= -uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: +uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3, uuid@^3.4.0: version "3.4.0" resolved "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== @@ -21117,9 +21110,9 @@ webpack-dev-middleware@^3.7.0, webpack-dev-middleware@^3.7.2: webpack-log "^2.0.0" webpack-dev-server@^3.10.3: - version "3.10.3" - resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.10.3.tgz#f35945036813e57ef582c2420ef7b470e14d3af0" - integrity sha512-e4nWev8YzEVNdOMcNzNeCN947sWJNd43E5XvsJzbAL08kGc2frm1tQ32hTJslRS+H65LCb/AaUCYU7fjHCpDeQ== + version "3.11.0" + resolved "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.0.tgz#8f154a3bce1bcfd1cc618ef4e703278855e7ff8c" + integrity sha512-PUxZ+oSTxogFQgkTtFndEtJIPNmml7ExwufBZ9L2/Xyyd5PnOL5UreWe5ZT7IU25DSdykL9p1MLQzmLh2ljSeg== dependencies: ansi-html "0.0.7" bonjour "^3.5.0" @@ -21129,31 +21122,31 @@ webpack-dev-server@^3.10.3: debug "^4.1.1" del "^4.1.1" express "^4.17.1" - html-entities "^1.2.1" + html-entities "^1.3.1" http-proxy-middleware "0.19.1" import-local "^2.0.0" internal-ip "^4.3.0" ip "^1.1.5" is-absolute-url "^3.0.3" killable "^1.0.1" - loglevel "^1.6.6" + loglevel "^1.6.8" opn "^5.5.0" p-retry "^3.0.1" - portfinder "^1.0.25" + portfinder "^1.0.26" schema-utils "^1.0.0" selfsigned "^1.10.7" semver "^6.3.0" serve-index "^1.9.1" - sockjs "0.3.19" + sockjs "0.3.20" sockjs-client "1.4.0" - spdy "^4.0.1" + spdy "^4.0.2" strip-ansi "^3.0.1" supports-color "^6.1.0" url "^0.11.0" webpack-dev-middleware "^3.7.2" webpack-log "^2.0.0" ws "^6.2.1" - yargs "12.0.5" + yargs "^13.3.2" webpack-hot-middleware@^2.25.0: version "2.25.0" @@ -21217,6 +21210,13 @@ webpack@^4.33.0, webpack@^4.38.0, webpack@^4.41.6: watchpack "^1.6.1" webpack-sources "^1.4.1" +websocket-driver@0.6.5: + version "0.6.5" + resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.6.5.tgz#5cb2556ceb85f4373c6d8238aa691c8454e13a36" + integrity sha1-XLJVbOuF9Dc8bYI4qmkchFThOjY= + dependencies: + websocket-extensions ">=0.1.1" + websocket-driver@>=0.5.1: version "0.7.3" resolved "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.3.tgz#a2d4e0d4f4f116f1e6297eba58b05d430100e9f9" @@ -21490,12 +21490,7 @@ ws@^6.1.2, ws@^6.2.1: dependencies: async-limiter "~1.0.0" -ws@^7.0.0: - version "7.2.3" - resolved "https://registry.npmjs.org/ws/-/ws-7.2.3.tgz#a5411e1fb04d5ed0efee76d26d5c46d830c39b46" - integrity sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ== - -ws@^7.2.3: +ws@^7.0.0, ws@^7.2.3: version "7.3.0" resolved "https://registry.npmjs.org/ws/-/ws-7.3.0.tgz#4b2f7f219b3d3737bc1a2fbf145d825b94d38ffd" integrity sha512-iFtXzngZVXPGgpTlP1rBqsUK82p9tKqsWRPg5L56egiljujJT3vGAYnHANvFxBieXrTFavhzhxW52jnaWV+w2w== @@ -21552,7 +21547,7 @@ y18n@^3.2.1: resolved "https://registry.npmjs.org/y18n/-/y18n-3.2.1.tgz#6d15fba884c08679c0d77e88e7759e811e07fa41" integrity sha1-bRX7qITAhnnA136I53WegR4H+kE= -"y18n@^3.2.1 || ^4.0.0", y18n@^4.0.0: +y18n@^4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/y18n/-/y18n-4.0.0.tgz#95ef94f85ecc81d007c264e190a120f0a3c8566b" integrity sha512-r9S/ZyXu/Xu9q1tYlpsLIsa3EeLXXk0VwlxqTcFRfg9EhMW+17kbt9G0NrgCmhGb5vT2hyhJZLfDGx+7+5Uj/w== @@ -21601,10 +21596,10 @@ yargs-parser@^10.0.0: dependencies: camelcase "^4.1.0" -yargs-parser@^11.1.1: - version "11.1.1" - resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-11.1.1.tgz#879a0865973bca9f6bab5cbdf3b1c67ec7d3bcf4" - integrity sha512-C6kB/WJDiaxONLJQnF8ccx9SEeoTTLek8RVbaOIsrAUS8VrBEXfmeSnCZxygc+XC2sNMBIwOOnfcxiynjHsVSQ== +yargs-parser@^13.1.2: + version "13.1.2" + resolved "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz#130f09702ebaeef2650d54ce6e3e5706f7a4fb38" + integrity sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg== dependencies: camelcase "^5.0.0" decamelize "^1.2.0" @@ -21647,24 +21642,6 @@ yargs-parser@^9.0.2: dependencies: camelcase "^4.1.0" -yargs@12.0.5: - version "12.0.5" - resolved "https://registry.npmjs.org/yargs/-/yargs-12.0.5.tgz#05f5997b609647b64f66b81e3b4b10a368e7ad13" - integrity sha512-Lhz8TLaYnxq/2ObqHDql8dX8CJi97oHxrjUcYtzKbbykPtVW9WB+poxI+NM2UIzsMgNCZTIf0AQwsjK5yMAqZw== - dependencies: - cliui "^4.0.0" - decamelize "^1.2.0" - find-up "^3.0.0" - get-caller-file "^1.0.1" - os-locale "^3.0.0" - require-directory "^2.1.1" - require-main-filename "^1.0.1" - set-blocking "^2.0.0" - string-width "^2.0.0" - which-module "^2.0.0" - y18n "^3.2.1 || ^4.0.0" - yargs-parser "^11.1.1" - yargs@^11.0.0: version "11.1.1" resolved "https://registry.npmjs.org/yargs/-/yargs-11.1.1.tgz#5052efe3446a4df5ed669c995886cc0f13702766" @@ -21683,6 +21660,22 @@ yargs@^11.0.0: y18n "^3.2.1" yargs-parser "^9.0.2" +yargs@^13.3.2: + version "13.3.2" + resolved "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz#ad7ffefec1aa59565ac915f82dccb38a9c31a2dd" + integrity sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw== + dependencies: + cliui "^5.0.0" + find-up "^3.0.0" + get-caller-file "^2.0.1" + require-directory "^2.1.1" + require-main-filename "^2.0.0" + set-blocking "^2.0.0" + string-width "^3.0.0" + which-module "^2.0.0" + y18n "^4.0.0" + yargs-parser "^13.1.2" + yargs@^14.2.2: version "14.2.3" resolved "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz#1a1c3edced1afb2a2fea33604bc6d1d8d688a414" From 6572ea3ecc777dbf0c5e146b9930ff778c7ea83d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 May 2020 08:53:59 +0200 Subject: [PATCH 38/58] packages/core: move LoginPage to layouts and remove it as a hardcoded route in the app (#1020) --- packages/app/src/App.tsx | 10 ++++++++-- packages/core/src/api/app/App.tsx | 5 ----- packages/core/src/index.ts | 1 + .../app => layout}/LoginPage/LoginPage.tsx | 18 ++++++++---------- .../src/{api/app => layout}/LoginPage/index.ts | 2 +- 5 files changed, 18 insertions(+), 18 deletions(-) rename packages/core/src/{api/app => layout}/LoginPage/LoginPage.tsx (92%) rename packages/core/src/{api/app => layout}/LoginPage/index.ts (93%) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index b4d01e067b..b0ff8b14c7 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -14,9 +14,14 @@ * limitations under the License. */ -import { createApp, AlertDisplay, OAuthRequestDialog } from '@backstage/core'; +import { + createApp, + AlertDisplay, + OAuthRequestDialog, + LoginPage, +} from '@backstage/core'; import React, { FC } from 'react'; -import { BrowserRouter as Router } from 'react-router-dom'; +import { BrowserRouter as Router, Route } from 'react-router-dom'; import Root from './components/Root'; import * as plugins from './plugins'; import apis from './apis'; @@ -35,6 +40,7 @@ const App: FC<{}> = () => ( + , diff --git a/packages/core/src/api/app/App.tsx b/packages/core/src/api/app/App.tsx index 3d1faf0d56..afdb6ffc58 100644 --- a/packages/core/src/api/app/App.tsx +++ b/packages/core/src/api/app/App.tsx @@ -38,7 +38,6 @@ import { AppThemeSelector, appThemeApiRef, } from '../apis'; -import LoginPage from './LoginPage'; import { lightTheme, darkTheme } from '@backstage/theme'; import { ApiAggregator } from '../apis/ApiAggregator'; @@ -138,10 +137,6 @@ class AppImpl implements BackstageApp { FeatureFlags.registeredFeatureFlags = registeredFeatureFlags; } - routes.push( - , - ); - const rendered = ( {routes} diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 816ad92578..fc6e2988bc 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -28,6 +28,7 @@ export { default as InfoCard } from './layout/InfoCard'; export { CardTab, TabbedCard } from './layout/TabbedCard'; export { default as ErrorBoundary } from './layout/ErrorBoundary'; export * from './layout/Sidebar'; +export * from './layout/LoginPage'; export { AlertDisplay } from './components/AlertDisplay'; export { default as HorizontalScrollGrid } from './components/HorizontalScrollGrid'; export { default as ProgressCard } from './components/ProgressBars/ProgressCard'; diff --git a/packages/core/src/api/app/LoginPage/LoginPage.tsx b/packages/core/src/layout/LoginPage/LoginPage.tsx similarity index 92% rename from packages/core/src/api/app/LoginPage/LoginPage.tsx rename to packages/core/src/layout/LoginPage/LoginPage.tsx index cdf0b55fd4..e7c83d563c 100644 --- a/packages/core/src/api/app/LoginPage/LoginPage.tsx +++ b/packages/core/src/layout/LoginPage/LoginPage.tsx @@ -16,10 +16,10 @@ import React, { FC, useState } from 'react'; import GitHubIcon from '@material-ui/icons/GitHub'; -import Page from '../../../layout/Page'; -import Header from '../../../layout/Header'; -import Content from '../../../layout/Content/Content'; -import ContentHeader from '../../../layout/ContentHeader/ContentHeader'; +import Page from '../Page'; +import Header from '../Header'; +import Content from '../Content/Content'; +import ContentHeader from '../ContentHeader/ContentHeader'; import { Grid, Typography, @@ -29,13 +29,13 @@ import { ListItem, Link, } from '@material-ui/core'; -import InfoCard from '../../../layout/InfoCard/InfoCard'; +import InfoCard from '../InfoCard/InfoCard'; enum AuthType { GitHub, } -const LoginPage: FC<{}> = () => { +export const LoginPage: FC<{}> = () => { const [githubUsername, setGithubUsername] = useState(String); const [githubPersonalAuthToken, setGithubPersonalAuthToken] = useState( String, @@ -72,11 +72,11 @@ const LoginPage: FC<{}> = () => { 'Content-Type': 'application/x-www-form-urlencoded', }), }) - .then((response) => { + .then(response => { if (response.status === 200) return response.json(); throw Error(`${response.status} ${response.statusText}`); }) - .then((data) => { + .then(data => { const info = { username: username, token: token, @@ -176,5 +176,3 @@ const LoginPage: FC<{}> = () => { ); }; - -export default LoginPage; diff --git a/packages/core/src/api/app/LoginPage/index.ts b/packages/core/src/layout/LoginPage/index.ts similarity index 93% rename from packages/core/src/api/app/LoginPage/index.ts rename to packages/core/src/layout/LoginPage/index.ts index 094029448c..caa94bd6d7 100644 --- a/packages/core/src/api/app/LoginPage/index.ts +++ b/packages/core/src/layout/LoginPage/index.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export { default } from './LoginPage'; +export { LoginPage } from './LoginPage'; From 86518a0210e502fd152d2e886eb9893ba9ba673f Mon Sep 17 00:00:00 2001 From: Raghunandan Date: Tue, 26 May 2020 17:48:38 +0200 Subject: [PATCH 39/58] add test cases for nonce checks --- .../src/providers/google/provider.test.ts | 112 +++++++++++++++++- .../src/providers/google/provider.ts | 2 +- 2 files changed, 109 insertions(+), 5 deletions(-) diff --git a/plugins/auth-backend/src/providers/google/provider.test.ts b/plugins/auth-backend/src/providers/google/provider.test.ts index 874b89d92c..327bf0260b 100644 --- a/plugins/auth-backend/src/providers/google/provider.test.ts +++ b/plugins/auth-backend/src/providers/google/provider.test.ts @@ -14,7 +14,11 @@ * limitations under the License. */ -import { GoogleAuthProvider, THOUSAND_DAYS_MS } from './provider'; +import { + GoogleAuthProvider, + THOUSAND_DAYS_MS, + TEN_MINUTES_MS, +} from './provider'; import passport from 'passport'; import express from 'express'; import * as utils from './../utils'; @@ -55,6 +59,7 @@ describe('GoogleAuthProvider', () => { const mockResponse = ({ send: jest.fn().mockReturnThis(), status: jest.fn().mockReturnThis(), + cookie: jest.fn().mockReturnThis(), } as unknown) as express.Response; const mockNext: express.NextFunction = jest.fn(); @@ -79,9 +84,33 @@ describe('GoogleAuthProvider', () => { scope: 'a,b', accessType: 'offline', prompt: 'consent', + state: expect.any(String), }); }); + it('should set a nonce cookie', () => { + const mockRequest = ({ + header: () => 'XMLHttpRequest', + query: { + scope: 'a,b', + }, + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + googleAuthProvider.start(mockRequest, mockResponse, mockNext); + expect(mockResponse.cookie).toBeCalledTimes(1); + expect(mockResponse.cookie).toBeCalledWith( + 'google-nonce', + expect.any(String), + expect.objectContaining({ + maxAge: TEN_MINUTES_MS, + path: `/auth/${googleAuthProviderConfig.provider}/handler`, + }), + ); + }); + it('should throw error if no scopes provided', () => { const mockRequest = ({ header: () => 'XMLHttpRequest', @@ -129,9 +158,6 @@ describe('GoogleAuthProvider', () => { }); describe('redirect frame handler', () => { - const mockRequest = ({ - header: () => 'XMLHttpRequest', - } as unknown) as express.Request; const mockResponse: any = ({ status: jest.fn().mockReturnThis(), send: jest.fn().mockReturnThis(), @@ -140,6 +166,14 @@ describe('GoogleAuthProvider', () => { const mockNext: express.NextFunction = jest.fn(); it('should call authenticate and post a response', () => { + const mockRequest = ({ + header: () => 'XMLHttpRequest', + cookies: { 'google-nonce': 'NONCE' }, + query: { + state: 'NONCE', + }, + } as unknown) as express.Request; + const spyPostMessage = jest .spyOn(utils, 'postMessageResponse') .mockImplementation(() => jest.fn()); @@ -173,6 +207,14 @@ describe('GoogleAuthProvider', () => { }); it('should respond with a error message if no refresh token returned', () => { + const mockRequest = ({ + header: () => 'XMLHttpRequest', + cookies: { 'google-nonce': 'NONCE' }, + query: { + state: 'NONCE', + }, + } as unknown) as express.Request; + const spyPassport = jest .spyOn(passport, 'authenticate') .mockImplementation((_x, callbackFunc) => { @@ -199,6 +241,14 @@ describe('GoogleAuthProvider', () => { }); it('should respond with a error message if auth failed', () => { + const mockRequest = ({ + header: () => 'XMLHttpRequest', + cookies: { 'google-nonce': 'NONCE' }, + query: { + state: 'NONCE', + }, + } as unknown) as express.Request; + const spyPassport = jest .spyOn(passport, 'authenticate') .mockImplementation((_x, callbackFunc) => { @@ -223,6 +273,60 @@ describe('GoogleAuthProvider', () => { error: new Error('Google auth failed, Error: TokenError'), }); }); + + it('should respond with a error message if cookie nonce is missing', () => { + const mockRequest = ({ + header: () => 'XMLHttpRequest', + cookies: {}, + query: { state: 'NONCE' }, + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith('Missing nonce'); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + }); + + it('should respond with a error message if state nonce is missing', () => { + const mockRequest = ({ + header: () => 'XMLHttpRequest', + cookies: { 'google-nonce': 'NONCE' }, + query: {}, + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith('Missing nonce'); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + }); + + it('should respond with a error message if nonce mismatch', () => { + const mockRequest = ({ + header: () => 'XMLHttpRequest', + cookies: { 'google-nonce': 'NONCA' }, + query: { state: 'NONCEB' }, + } as unknown) as express.Request; + + const googleAuthProvider = new GoogleAuthProvider( + googleAuthProviderConfig, + ); + + googleAuthProvider.frameHandler(mockRequest, mockResponse, mockNext); + expect(mockResponse.send).toBeCalledTimes(1); + expect(mockResponse.send).toBeCalledWith('Invalid nonce'); + expect(mockResponse.status).toBeCalledTimes(1); + expect(mockResponse.status).toBeCalledWith(401); + }); }); describe('strategy handler', () => { diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index a631f86fa6..cb080e2fd3 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -28,7 +28,7 @@ import { postMessageResponse, ensuresXRequestedWith } from './../utils'; import { InputError } from '@backstage/backend-common'; export const THOUSAND_DAYS_MS = 1000 * 24 * 60 * 60 * 1000; -const TEN_MINUTES_MS = 600 * 1000; +export const TEN_MINUTES_MS = 600 * 1000; export class GoogleAuthProvider implements AuthProvider, AuthProviderRouteHandlers { private readonly providerConfig: AuthProviderConfig; From f72d97dc171424785f9f7ae694c0c88241f4c52c Mon Sep 17 00:00:00 2001 From: Adil Alimbetov Date: Wed, 27 May 2020 14:13:41 +0600 Subject: [PATCH 40/58] Sidebar logo update (#814) * Testing logo ux * Updated logo on sidebar * removed unused vars * adjusted types on icon components --- CONTRIBUTING.md | 1 - packages/app/src/components/Root/LogoFull.tsx | 46 ++++++++++++++++++ packages/app/src/components/Root/LogoIcon.tsx | 47 +++++++++++++++++++ packages/app/src/components/Root/Root.tsx | 28 +++-------- packages/backend/src/plugins/sentry.ts | 2 +- .../cli/src/commands/create-app/createApp.ts | 4 +- .../commands/create-plugin/createPlugin.ts | 10 ++-- packages/cli/src/lib/bundler/bundle.ts | 2 +- packages/cli/src/lib/tasks.ts | 12 ++--- .../default-app/packages/app/src/App.tsx | 2 +- packages/core/src/api/apis/ApiProvider.tsx | 2 +- .../api/apis/implementations/lib/subjects.ts | 16 +++---- .../core/src/api/app/AppThemeProvider.tsx | 6 +-- .../core/src/api/app/FeatureFlags.test.tsx | 2 +- packages/core/src/api/app/FeatureFlags.tsx | 6 +-- .../components/CodeSnippet/CodeSnippet.tsx | 2 +- .../HorizontalScrollGrid.test.jsx | 2 +- .../LoginRequestListItem.tsx | 2 +- .../OAuthRequestDialog/OAuthRequestDialog.tsx | 6 +-- .../core/src/components/Status/Status.tsx | 14 +++--- .../StructuredMetadataTable.test.jsx | 10 ++-- .../core/src/layout/ErrorPage/ErrorPage.tsx | 2 +- packages/core/src/layout/Sidebar/Intro.tsx | 10 ++-- packages/core/src/layout/Sidebar/Items.tsx | 10 ++-- packages/core/src/layout/Sidebar/Page.tsx | 2 +- packages/storybook/.storybook/main.js | 2 +- plugins/auth-backend/src/run.ts | 2 +- plugins/catalog/src/data/mock-factory.ts | 8 ++-- .../src/components/Settings/Settings.tsx | 6 +-- .../BuildWithStepsPage/BuildWithStepsPage.tsx | 2 +- .../lib/ActionOutput/ActionOutput.tsx | 4 +- plugins/circleci/src/state/useAsyncPolling.ts | 2 +- plugins/circleci/src/state/useSettings.ts | 2 +- plugins/identity-backend/src/run.ts | 2 +- .../src/components/AuditView/index.test.tsx | 14 +++--- .../src/components/CreateAudit/index.test.tsx | 2 +- .../RegisterComponentForm.tsx | 2 +- plugins/sentry-backend/README.md | 2 +- .../src/components/ErrorCell/ErrorCell.tsx | 2 +- .../SentryIssuesTable/SentryIssuesTable.tsx | 8 ++-- plugins/sentry/src/data/mock-api.ts | 2 +- .../tech-radar/src/components/Radar/Radar.jsx | 8 ++-- .../components/RadarBubble/RadarBubble.jsx | 8 ++-- .../src/components/RadarGrid/RadarGrid.jsx | 2 +- .../components/RadarLegend/RadarLegend.jsx | 6 +-- .../src/components/RadarPlot/RadarPlot.jsx | 6 +-- 46 files changed, 208 insertions(+), 130 deletions(-) create mode 100644 packages/app/src/components/Root/LogoFull.tsx create mode 100644 packages/app/src/components/Root/LogoIcon.tsx diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index f4b31a74b2..43c1f0d7b8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,6 @@ Contributions are welcome, and they are greatly appreciated! Every little bit he Backstage is released under the Apache2.0 License, and original creations contributed to this repo are accepted under the same license. - # Types of Contributions ## Report bugs diff --git a/packages/app/src/components/Root/LogoFull.tsx b/packages/app/src/components/Root/LogoFull.tsx new file mode 100644 index 0000000000..d2b1bf1080 --- /dev/null +++ b/packages/app/src/components/Root/LogoFull.tsx @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + width: 'auto', + height: 30, + }, + path: { + fill: '#7df3e1', + }, +}); +const LogoFull: FC<{}> = () => { + const classes = useStyles(); + + return ( + + + + ); +}; + +export default LogoFull; diff --git a/packages/app/src/components/Root/LogoIcon.tsx b/packages/app/src/components/Root/LogoIcon.tsx new file mode 100644 index 0000000000..d70be3dd32 --- /dev/null +++ b/packages/app/src/components/Root/LogoIcon.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC } from 'react'; +import { makeStyles } from '@material-ui/core'; + +const useStyles = makeStyles({ + svg: { + width: 'auto', + height: 28, + }, + path: { + fill: '#7df3e1', + }, +}); + +const LogoIcon: FC<{}> = () => { + const classes = useStyles(); + + return ( + + + + ); +}; + +export default LogoIcon; diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index c26336c18b..3c5ec4ae3a 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -16,12 +16,13 @@ import React, { FC, useContext } from 'react'; import PropTypes from 'prop-types'; -import { Link, makeStyles, Typography } from '@material-ui/core'; +import { Link, makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExploreIcon from '@material-ui/icons/Explore'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; import AccountTreeIcon from '@material-ui/icons/AccountTree'; - +import LogoFull from './LogoFull'; +import LogoIcon from './LogoIcon'; import { Sidebar, SidebarPage, @@ -44,21 +45,9 @@ const useSidebarLogoStyles = makeStyles({ alignItems: 'center', marginBottom: -14, }, - logoContainer: { + link: { width: sidebarConfig.drawerWidthClosed, - display: 'flex', - alignItems: 'center', - justifyContent: 'center', - }, - title: { - fontSize: sidebarConfig.logoHeight, - fontWeight: 'bold', - marginLeft: 20, - whiteSpace: 'nowrap', - color: '#fff', - }, - titleDot: { - color: '#68c5b5', + marginLeft: 24, }, }); @@ -68,11 +57,8 @@ const SidebarLogo: FC<{}> = () => { return (
- - - {isOpen ? 'Backstage' : 'B'} - . - + + {isOpen ? : }
); diff --git a/packages/backend/src/plugins/sentry.ts b/packages/backend/src/plugins/sentry.ts index ddb7ddd540..34506ee3de 100644 --- a/packages/backend/src/plugins/sentry.ts +++ b/packages/backend/src/plugins/sentry.ts @@ -17,6 +17,6 @@ import { createRouter } from '@backstage/plugin-sentry-backend'; import { Logger } from 'winston'; -export default async function(logger: Logger) { +export default async function (logger: Logger) { return await createRouter(logger); } diff --git a/packages/cli/src/commands/create-app/createApp.ts b/packages/cli/src/commands/create-app/createApp.ts index e28cb376b9..a42271435b 100644 --- a/packages/cli/src/commands/create-app/createApp.ts +++ b/packages/cli/src/commands/create-app/createApp.ts @@ -62,7 +62,7 @@ async function buildApp(appDir: string) { await Task.forItem('executing', cmd, async () => { process.chdir(appDir); - await exec(cmd).catch((error) => { + await exec(cmd).catch(error => { process.stdout.write(error.stderr); process.stdout.write(error.stdout); throw new Error(`Could not execute command ${chalk.cyan(cmd)}`); @@ -81,7 +81,7 @@ export async function moveApp( id: string, ) { await Task.forItem('moving', id, async () => { - await fs.move(tempDir, destination).catch((error) => { + await fs.move(tempDir, destination).catch(error => { throw new Error( `Failed to move app from ${tempDir} to ${destination}: ${error.message}`, ); diff --git a/packages/cli/src/commands/create-plugin/createPlugin.ts b/packages/cli/src/commands/create-plugin/createPlugin.ts index 1441b16d05..3a91044500 100644 --- a/packages/cli/src/commands/create-plugin/createPlugin.ts +++ b/packages/cli/src/commands/create-plugin/createPlugin.ts @@ -107,7 +107,7 @@ export async function addPluginDependencyToApp( packageFileJson.dependencies = sortObjectByKeys(dependencies); const newContents = `${JSON.stringify(packageFileJson, null, 2)}\n`; - await fs.writeFile(packageFile, newContents, 'utf-8').catch((error) => { + await fs.writeFile(packageFile, newContents, 'utf-8').catch(error => { throw new Error( `Failed to add plugin as dependency to app: ${packageFile}: ${error.message}`, ); @@ -119,14 +119,14 @@ export async function addPluginToApp(rootDir: string, pluginName: string) { const pluginPackage = `@backstage/plugin-${pluginName}`; const pluginNameCapitalized = pluginName .split('-') - .map((name) => capitalize(name)) + .map(name => capitalize(name)) .join(''); const pluginExport = `export { plugin as ${pluginNameCapitalized} } from '${pluginPackage}';`; const pluginsFilePath = 'packages/app/src/plugins.ts'; const pluginsFile = resolvePath(rootDir, pluginsFilePath); await Task.forItem('processing', pluginsFilePath, async () => { - await addExportStatement(pluginsFile, pluginExport).catch((error) => { + await addExportStatement(pluginsFile, pluginExport).catch(error => { throw new Error( `Failed to import plugin in app: ${pluginsFile}: ${error.message}`, ); @@ -148,7 +148,7 @@ async function buildPlugin(pluginFolder: string) { await Task.forItem('executing', command, async () => { process.chdir(pluginFolder); - await exec(command).catch((error) => { + await exec(command).catch(error => { process.stdout.write(error.stderr); process.stdout.write(error.stdout); throw new Error(`Could not execute command ${chalk.cyan(command)}`); @@ -163,7 +163,7 @@ export async function movePlugin( id: string, ) { await Task.forItem('moving', id, async () => { - await fs.move(tempDir, destination).catch((error) => { + await fs.move(tempDir, destination).catch(error => { throw new Error( `Failed to move plugin from ${tempDir} to ${destination}: ${error.message}`, ); diff --git a/packages/cli/src/lib/bundler/bundle.ts b/packages/cli/src/lib/bundler/bundle.ts index f24da5b0c8..c7860c0f42 100644 --- a/packages/cli/src/lib/bundler/bundle.ts +++ b/packages/cli/src/lib/bundler/bundle.ts @@ -48,7 +48,7 @@ export async function buildBundle(options: BuildOptions) { const previousFileSizes = await measureFileSizesBeforeBuild(paths.targetDist); await fs.emptyDir(paths.targetDist); - const { stats } = await build(compiler, isCi).catch((error) => { + const { stats } = await build(compiler, isCi).catch(error => { console.log(chalk.red('Failed to compile.\n')); throw new Error(`Failed to compile.\n${error.message || error}`); }); diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 721e1a355b..6b752a36cc 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -73,7 +73,7 @@ export async function templatingTask( destinationDir: string, context: any, ) { - const files = await recursive(templateDir).catch((error) => { + const files = await recursive(templateDir).catch(error => { throw new Error(`Failed to read template directory: ${error.message}`); }); @@ -89,7 +89,7 @@ export async function templatingTask( const compiled = handlebars.compile(template.toString()); const contents = compiled({ name: basename(destination), ...context }); - await fs.writeFile(destination, contents).catch((error) => { + await fs.writeFile(destination, contents).catch(error => { throw new Error( `Failed to create file: ${destination}: ${error.message}`, ); @@ -97,7 +97,7 @@ export async function templatingTask( }); } else { await Task.forItem('copying', basename(file), async () => { - await fs.copyFile(file, destinationFile).catch((error) => { + await fs.copyFile(file, destinationFile).catch(error => { const destination = destinationFile; throw new Error( `Failed to copy file to ${destination} : ${error.message}`, @@ -145,7 +145,7 @@ export async function installWithLocalDeps(dir: string) { await fs .writeJSON(pkgJsonPath, pkgJson, { encoding: 'utf8', spaces: 2 }) - .catch((error) => { + .catch(error => { throw new Error( `Failed to add resolutions to package.json: ${error.message}`, ); @@ -157,7 +157,7 @@ export async function installWithLocalDeps(dir: string) { } await Task.forItem('executing', 'yarn install', async () => { - await exec('yarn install', { cwd: dir }).catch((error) => { + await exec('yarn install', { cwd: dir }).catch(error => { process.stdout.write(error.stderr); process.stdout.write(error.stdout); throw new Error( @@ -193,7 +193,7 @@ export async function installWithLocalDeps(dir: string) { await fs .writeJSON(depJsonPath, depJson, { encoding: 'utf8', spaces: 2 }) - .catch((error) => { + .catch(error => { throw new Error( `Failed to add resolutions to package.json: ${error.message}`, ); diff --git a/packages/cli/templates/default-app/packages/app/src/App.tsx b/packages/cli/templates/default-app/packages/app/src/App.tsx index c32efd2a6b..84bc1b2e7d 100644 --- a/packages/cli/templates/default-app/packages/app/src/App.tsx +++ b/packages/cli/templates/default-app/packages/app/src/App.tsx @@ -4,7 +4,7 @@ import React, { FC } from 'react'; import { BrowserRouter as Router } from 'react-router-dom'; import * as plugins from './plugins'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ '@global': { html: { height: '100%', diff --git a/packages/core/src/api/apis/ApiProvider.tsx b/packages/core/src/api/apis/ApiProvider.tsx index 46f89cfa0d..1c5abf1c56 100644 --- a/packages/core/src/api/apis/ApiProvider.tsx +++ b/packages/core/src/api/apis/ApiProvider.tsx @@ -53,7 +53,7 @@ export function withApis(apis: TypesToApiRefs) { return function withApisWrapper

( WrappedComponent: React.ComponentType

, ) { - const Hoc: FC> = (props) => { + const Hoc: FC> = props => { const apiHolder = useContext(Context); if (!apiHolder) { diff --git a/packages/core/src/api/apis/implementations/lib/subjects.ts b/packages/core/src/api/apis/implementations/lib/subjects.ts index b33df70a65..5b424b7e55 100644 --- a/packages/core/src/api/apis/implementations/lib/subjects.ts +++ b/packages/core/src/api/apis/implementations/lib/subjects.ts @@ -33,7 +33,7 @@ export class PublishSubject private isClosed = false; private terminatingError?: Error; - private readonly observable = new ObservableImpl((subscriber) => { + private readonly observable = new ObservableImpl(subscriber => { if (this.isClosed) { if (this.terminatingError) { subscriber.error(this.terminatingError); @@ -61,7 +61,7 @@ export class PublishSubject if (this.isClosed) { throw new Error('PublishSubject is closed'); } - this.subscribers.forEach((subscriber) => subscriber.next(value)); + this.subscribers.forEach(subscriber => subscriber.next(value)); } error(error: Error) { @@ -70,7 +70,7 @@ export class PublishSubject } this.isClosed = true; this.terminatingError = error; - this.subscribers.forEach((subscriber) => subscriber.error(error)); + this.subscribers.forEach(subscriber => subscriber.error(error)); } complete() { @@ -78,7 +78,7 @@ export class PublishSubject throw new Error('PublishSubject is closed'); } this.isClosed = true; - this.subscribers.forEach((subscriber) => subscriber.complete()); + this.subscribers.forEach(subscriber => subscriber.complete()); } subscribe(observer: ZenObservable.Observer): ZenObservable.Subscription; @@ -126,7 +126,7 @@ export class BehaviorSubject this.currentValue = value; } - private readonly observable = new ObservableImpl((subscriber) => { + private readonly observable = new ObservableImpl(subscriber => { if (this.isClosed) { if (this.terminatingError) { subscriber.error(this.terminatingError); @@ -157,7 +157,7 @@ export class BehaviorSubject throw new Error('BehaviorSubject is closed'); } this.currentValue = value; - this.subscribers.forEach((subscriber) => subscriber.next(value)); + this.subscribers.forEach(subscriber => subscriber.next(value)); } error(error: Error) { @@ -166,7 +166,7 @@ export class BehaviorSubject } this.isClosed = true; this.terminatingError = error; - this.subscribers.forEach((subscriber) => subscriber.error(error)); + this.subscribers.forEach(subscriber => subscriber.error(error)); } complete() { @@ -174,7 +174,7 @@ export class BehaviorSubject throw new Error('BehaviorSubject is closed'); } this.isClosed = true; - this.subscribers.forEach((subscriber) => subscriber.complete()); + this.subscribers.forEach(subscriber => subscriber.complete()); } subscribe(observer: ZenObservable.Observer): ZenObservable.Subscription; diff --git a/packages/core/src/api/app/AppThemeProvider.tsx b/packages/core/src/api/app/AppThemeProvider.tsx index 6e68fe9ba0..775d8293ba 100644 --- a/packages/core/src/api/app/AppThemeProvider.tsx +++ b/packages/core/src/api/app/AppThemeProvider.tsx @@ -27,20 +27,20 @@ function resolveTheme( themes: AppTheme[], ) { if (themeId !== undefined) { - const selectedTheme = themes.find((theme) => theme.id === themeId); + const selectedTheme = themes.find(theme => theme.id === themeId); if (selectedTheme) { return selectedTheme; } } if (shouldPreferDark) { - const darkTheme = themes.find((theme) => theme.variant === 'dark'); + const darkTheme = themes.find(theme => theme.variant === 'dark'); if (darkTheme) { return darkTheme; } } - const lightTheme = themes.find((theme) => theme.variant === 'light'); + const lightTheme = themes.find(theme => theme.variant === 'light'); if (lightTheme) { return lightTheme; } diff --git a/packages/core/src/api/app/FeatureFlags.test.tsx b/packages/core/src/api/app/FeatureFlags.test.tsx index ca4a819f13..99974f9b1d 100644 --- a/packages/core/src/api/app/FeatureFlags.test.tsx +++ b/packages/core/src/api/app/FeatureFlags.test.tsx @@ -147,7 +147,7 @@ describe('FeatureFlags', () => { it('should get the correct values', () => { const getByName = (name: string) => - featureFlags.getRegisteredFlags().find((flag) => flag.name === name); + featureFlags.getRegisteredFlags().find(flag => flag.name === name); expect(getByName('registered-flag-0')).toBeUndefined(); expect(getByName('registered-flag-1')).toEqual({ diff --git a/packages/core/src/api/app/FeatureFlags.tsx b/packages/core/src/api/app/FeatureFlags.tsx index 6af10f228e..11c084d3ed 100644 --- a/packages/core/src/api/app/FeatureFlags.tsx +++ b/packages/core/src/api/app/FeatureFlags.tsx @@ -127,12 +127,12 @@ export interface FeatureFlagsRegistryItem { export class FeatureFlagsRegistry extends Array { static from(entries: FeatureFlagsRegistryItem[]) { - Array.from(entries).forEach((entry) => validateFlagName(entry.name)); + Array.from(entries).forEach(entry => validateFlagName(entry.name)); return new FeatureFlagsRegistry(...entries); } push(...entries: FeatureFlagsRegistryItem[]): number { - Array.from(entries).forEach((entry) => validateFlagName(entry.name)); + Array.from(entries).forEach(entry => validateFlagName(entry.name)); return super.push(...entries); } @@ -143,7 +143,7 @@ export class FeatureFlagsRegistry extends Array { )[] ): FeatureFlagsRegistryItem[] { const _concat = super.concat(...entries); - Array.from(_concat).forEach((entry) => validateFlagName(entry.name)); + Array.from(_concat).forEach(entry => validateFlagName(entry.name)); return _concat; } diff --git a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx index fc7c262882..c77dc5bd43 100644 --- a/packages/core/src/components/CodeSnippet/CodeSnippet.tsx +++ b/packages/core/src/components/CodeSnippet/CodeSnippet.tsx @@ -31,7 +31,7 @@ const defaultProps = { showLineNumbers: false, }; -const CodeSnippet: FC = (props) => { +const CodeSnippet: FC = props => { const { text, language, showLineNumbers } = { ...defaultProps, ...props, diff --git a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx index 36079854fd..d7f1e2387a 100644 --- a/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx +++ b/packages/core/src/components/HorizontalScrollGrid/HorizontalScrollGrid.test.jsx @@ -25,7 +25,7 @@ describe('', () => { jest.spyOn(window.performance, 'now').mockReturnValue(5); jest .spyOn(window, 'requestAnimationFrame') - .mockImplementation((cb) => cb(20)); + .mockImplementation(cb => cb(20)); }); afterEach(() => { diff --git a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx index 8e3d98c998..d97956c1f0 100644 --- a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx +++ b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx @@ -25,7 +25,7 @@ import { import React, { FC, useState } from 'react'; import { PendingAuthRequest } from '../../api'; -const useItemStyles = makeStyles((theme) => ({ +const useItemStyles = makeStyles(theme => ({ root: { paddingLeft: theme.spacing(3), }, diff --git a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx index 617cce2aee..bde12ccb04 100644 --- a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx +++ b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx @@ -29,7 +29,7 @@ import { useObservable } from 'react-use'; import LoginRequestListItem from './LoginRequestListItem'; import { useApi, oauthRequestApiRef } from '../../api'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ dialog: { paddingTop: theme.spacing(1), }, @@ -53,7 +53,7 @@ export const OAuthRequestDialog: FC = () => { ); const handleRejectAll = () => { - requests.forEach((request) => request.reject()); + requests.forEach(request => request.reject()); }; return ( @@ -69,7 +69,7 @@ export const OAuthRequestDialog: FC = () => { - {requests.map((request) => ( + {requests.map(request => ( ((theme) => ({ +const useStyles = makeStyles(theme => ({ status: { fontWeight: 500, '&::before': { @@ -63,26 +63,26 @@ const useStyles = makeStyles((theme) => ({ }, })); -export const StatusOK: FC<{}> = (props) => { +export const StatusOK: FC<{}> = props => { const classes = useStyles(props); return ; }; -export const StatusWarning: FC<{}> = (props) => { +export const StatusWarning: FC<{}> = props => { const classes = useStyles(props); return ( ); }; -export const StatusError: FC<{}> = (props) => { +export const StatusError: FC<{}> = props => { const classes = useStyles(props); return ( ); }; -export const StatusPending: FC<{}> = (props) => { +export const StatusPending: FC<{}> = props => { const classes = useStyles(props); return ( = (props) => { ); }; -export const StatusRunning: FC<{}> = (props) => { +export const StatusRunning: FC<{}> = props => { const classes = useStyles(props); return ( = (props) => { ); }; -export const StatusAborted: FC<{}> = (props) => { +export const StatusAborted: FC<{}> = props => { const classes = useStyles(props); return ( ', () => { , ); const keys = Object.keys(metadata); - keys.forEach((value) => { + keys.forEach(value => { expect(getByText(startCase(value))).toBeInTheDocument(); expect(getByText(metadata[value])).toBeInTheDocument(); }); @@ -49,7 +49,7 @@ describe('', () => { ); const keys = Object.keys(metadata); - keys.forEach((value) => { + keys.forEach(value => { expect(getByText(startCase(value))).toBeInTheDocument(); expect(getByText(metadata[value].toString())).toBeInTheDocument(); }); @@ -61,10 +61,10 @@ describe('', () => { , ); const keys = Object.keys(metadata); - keys.forEach((value) => { + keys.forEach(value => { expect(getByText(startCase(value))).toBeInTheDocument(); }); - metadata.arrayField.forEach((value) => { + metadata.arrayField.forEach(value => { expect(getByText(value)).toBeInTheDocument(); }); }); @@ -85,7 +85,7 @@ describe('', () => { ); const keys = Object.keys(metadata.config); - keys.forEach((value) => { + keys.forEach(value => { expect( getByText(startCase(value), { exact: false }), ).toBeInTheDocument(); diff --git a/packages/core/src/layout/ErrorPage/ErrorPage.tsx b/packages/core/src/layout/ErrorPage/ErrorPage.tsx index 14241c1ee7..747f2f6036 100644 --- a/packages/core/src/layout/ErrorPage/ErrorPage.tsx +++ b/packages/core/src/layout/ErrorPage/ErrorPage.tsx @@ -26,7 +26,7 @@ interface IErrorPageProps { statusMessage: string; } -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ container: { padding: theme.spacing(8), }, diff --git a/packages/core/src/layout/Sidebar/Intro.tsx b/packages/core/src/layout/Sidebar/Intro.tsx index 595697c466..b4bb56588e 100644 --- a/packages/core/src/layout/Sidebar/Intro.tsx +++ b/packages/core/src/layout/Sidebar/Intro.tsx @@ -26,7 +26,7 @@ import { } from './config'; import { SidebarDivider } from './Items'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ introCard: { color: '#b5b5b5', // XXX (@koroeskohr): should I be using a Mui theme variable? @@ -74,7 +74,7 @@ type IntroCardProps = { onClose: () => void; }; -export const IntroCard: FC = (props) => { +export const IntroCard: FC = props => { const classes = useStyles(); const { text, onClose } = props; const handleClose = () => onClose(); @@ -109,7 +109,7 @@ type SidebarIntroCardProps = { onDismiss: () => void; }; -const SidebarIntroCard: FC = (props) => { +const SidebarIntroCard: FC = props => { const { text, onDismiss } = props; const [collapsing, setCollapsing] = useState(false); const startDismissing = () => { @@ -138,10 +138,10 @@ export const SidebarIntro: FC = () => { }); const dismissStarred = () => { - setDismissedIntro((state) => ({ ...state, starredItemsDismissed: true })); + setDismissedIntro(state => ({ ...state, starredItemsDismissed: true })); }; const dismissRecentlyViewed = () => { - setDismissedIntro((state) => ({ + setDismissedIntro(state => ({ ...state, recentlyViewedItemsDismissed: true, })); diff --git a/packages/core/src/layout/Sidebar/Items.tsx b/packages/core/src/layout/Sidebar/Items.tsx index bd5faa5631..4696f53a37 100644 --- a/packages/core/src/layout/Sidebar/Items.tsx +++ b/packages/core/src/layout/Sidebar/Items.tsx @@ -29,7 +29,7 @@ import { NavLink } from 'react-router-dom'; import { sidebarConfig, SidebarContext } from './config'; import { IconComponent } from '../../icons'; -const useStyles = makeStyles((theme) => { +const useStyles = makeStyles(theme => { const { selectedIndicatorWidth, drawerWidthClosed, @@ -139,7 +139,7 @@ export const SidebarItem: FC = ({ Boolean(match && !disableSelected)} + isActive={match => Boolean(match && !disableSelected)} exact to={to} onClick={onClick} @@ -153,7 +153,7 @@ export const SidebarItem: FC = ({ Boolean(match && !disableSelected)} + isActive={match => Boolean(match && !disableSelected)} exact to={to} onClick={onClick} @@ -175,11 +175,11 @@ type SidebarSearchFieldProps = { onSearch: (input: string) => void; }; -export const SidebarSearchField: FC = (props) => { +export const SidebarSearchField: FC = props => { const [input, setInput] = useState(''); const classes = useStyles(); - const handleEnter: KeyboardEventHandler = (ev) => { + const handleEnter: KeyboardEventHandler = ev => { if (ev.key === 'Enter') { props.onSearch(input); } diff --git a/packages/core/src/layout/Sidebar/Page.tsx b/packages/core/src/layout/Sidebar/Page.tsx index 79df2852d9..750f764c2d 100644 --- a/packages/core/src/layout/Sidebar/Page.tsx +++ b/packages/core/src/layout/Sidebar/Page.tsx @@ -44,7 +44,7 @@ export const SidebarPinStateContext = createContext( }, ); -export const SidebarPage: FC<{}> = (props) => { +export const SidebarPage: FC<{}> = props => { const [isPinned, setIsPinned] = useState(LocalStorage.getSidebarPinState()); useEffect(() => { diff --git a/packages/storybook/.storybook/main.js b/packages/storybook/.storybook/main.js index 5f9bde16c3..b8f78f5d2c 100644 --- a/packages/storybook/.storybook/main.js +++ b/packages/storybook/.storybook/main.js @@ -12,7 +12,7 @@ module.exports = { '@storybook/addon-storysource', 'storybook-dark-mode/register', ], - webpackFinal: async (config) => { + webpackFinal: async config => { const coreSrc = path.resolve(__dirname, '../../core/src'); // Mirror config in packages/cli/src/lib/bundler diff --git a/plugins/auth-backend/src/run.ts b/plugins/auth-backend/src/run.ts index 0a684c379e..a2c2601258 100644 --- a/plugins/auth-backend/src/run.ts +++ b/plugins/auth-backend/src/run.ts @@ -22,7 +22,7 @@ const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003; const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); const logger = getRootLogger(); -startStandaloneServer({ port, enableCors, logger }).catch((err) => { +startStandaloneServer({ port, enableCors, logger }).catch(err => { logger.error(err); process.exit(1); }); diff --git a/plugins/catalog/src/data/mock-factory.ts b/plugins/catalog/src/data/mock-factory.ts index bf39fa436c..19f04195f9 100644 --- a/plugins/catalog/src/data/mock-factory.ts +++ b/plugins/catalog/src/data/mock-factory.ts @@ -20,7 +20,7 @@ const ARTIFICIAL_TIMEOUT = 800; let inMemoryStore = [...mock]; export const MockComponentFactory: ComponentFactory = { getAllComponents(): Promise { - return new Promise((resolve) => + return new Promise(resolve => setTimeout(() => resolve(inMemoryStore), ARTIFICIAL_TIMEOUT), ); }, @@ -28,7 +28,7 @@ export const MockComponentFactory: ComponentFactory = { return new Promise((resolve, reject) => setTimeout(() => { const mockComponent = inMemoryStore.find( - (component) => component.name === name, + component => component.name === name, ); if (mockComponent) return resolve(mockComponent); return reject({ code: 'Component not found!' }); @@ -36,10 +36,10 @@ export const MockComponentFactory: ComponentFactory = { ); }, removeComponentByName(name: string): Promise { - return new Promise((resolve) => + return new Promise(resolve => setTimeout(() => { inMemoryStore = inMemoryStore.filter( - (component) => component.name !== name, + component => component.name !== name, ); resolve(true); }, ARTIFICIAL_TIMEOUT), diff --git a/plugins/circleci/src/components/Settings/Settings.tsx b/plugins/circleci/src/components/Settings/Settings.tsx index e853bb46bb..f0ed3f2ceb 100644 --- a/plugins/circleci/src/components/Settings/Settings.tsx +++ b/plugins/circleci/src/components/Settings/Settings.tsx @@ -80,7 +80,7 @@ const Settings = () => { value={token} fullWidth variant="outlined" - onChange={(e) => setToken(e.target.value)} + onChange={e => setToken(e.target.value)} /> @@ -90,7 +90,7 @@ const Settings = () => { label="Owner" variant="outlined" value={owner} - onChange={(e) => setOwner(e.target.value)} + onChange={e => setOwner(e.target.value)} /> @@ -100,7 +100,7 @@ const Settings = () => { fullWidth variant="outlined" value={repo} - onChange={(e) => setRepo(e.target.value)} + onChange={e => setRepo(e.target.value)} /> diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx index 8493913576..75d09610b5 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/BuildWithStepsPage.tsx @@ -35,7 +35,7 @@ const BuildName: FC<{ build?: BuildWithSteps }> = ({ build }) => ( ); -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ neutral: {}, failed: { position: 'relative', diff --git a/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx index e8e1bfe958..dd526c1b5f 100644 --- a/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx +++ b/plugins/circleci/src/pages/BuildWithStepsPage/lib/ActionOutput/ActionOutput.tsx @@ -50,8 +50,8 @@ export const ActionOutput: FC<{ const [messages, setMessages] = useState([]); useEffect(() => { fetch(url) - .then((res) => res.json()) - .then((actionOutput) => { + .then(res => res.json()) + .then(actionOutput => { if (typeof actionOutput !== 'undefined') { setMessages( actionOutput.map(({ message }: { message: string }) => message), diff --git a/plugins/circleci/src/state/useAsyncPolling.ts b/plugins/circleci/src/state/useAsyncPolling.ts index 2f8de0c2fe..7ea0755368 100644 --- a/plugins/circleci/src/state/useAsyncPolling.ts +++ b/plugins/circleci/src/state/useAsyncPolling.ts @@ -26,7 +26,7 @@ export const useAsyncPolling = ( while (isPolling.current === true) { await pollingFn(); - await new Promise((resolve) => setTimeout(resolve, interval)); + await new Promise(resolve => setTimeout(resolve, interval)); } }; diff --git a/plugins/circleci/src/state/useSettings.ts b/plugins/circleci/src/state/useSettings.ts index 5482abeb8b..c8dc9ce39b 100644 --- a/plugins/circleci/src/state/useSettings.ts +++ b/plugins/circleci/src/state/useSettings.ts @@ -29,7 +29,7 @@ export function useSettings() { if ( stateFromStorage && Object.keys(stateFromStorage).some( - (k) => (settings as any)[k] !== stateFromStorage[k], + k => (settings as any)[k] !== stateFromStorage[k], ) ) dispatch({ diff --git a/plugins/identity-backend/src/run.ts b/plugins/identity-backend/src/run.ts index 0a684c379e..a2c2601258 100644 --- a/plugins/identity-backend/src/run.ts +++ b/plugins/identity-backend/src/run.ts @@ -22,7 +22,7 @@ const port = process.env.PLUGIN_PORT ? Number(process.env.PLUGIN_PORT) : 3003; const enableCors = yn(process.env.PLUGIN_CORS, { default: false }); const logger = getRootLogger(); -startStandaloneServer({ port, enableCors, logger }).catch((err) => { +startStandaloneServer({ port, enableCors, logger }).catch(err => { logger.error(err); process.exit(1); }); diff --git a/plugins/lighthouse/src/components/AuditView/index.test.tsx b/plugins/lighthouse/src/components/AuditView/index.test.tsx index 330b88517a..4e261fcf39 100644 --- a/plugins/lighthouse/src/components/AuditView/index.test.tsx +++ b/plugins/lighthouse/src/components/AuditView/index.test.tsx @@ -49,7 +49,7 @@ describe('AuditView', () => { apis = ApiRegistry.from([ [lighthouseApiRef, new LighthouseRestApi('https://lighthouse')], ]); - id = websiteResponse.audits.find((a) => a.status === 'COMPLETED') + id = websiteResponse.audits.find(a => a.status === 'COMPLETED') ?.id as string; useParams.mockReturnValue({ id }); }); @@ -101,7 +101,7 @@ describe('AuditView', () => { await rendered.findByTestId('audit-sidebar'); - websiteResponse.audits.forEach((a) => { + websiteResponse.audits.forEach(a => { expect( rendered.queryByText(formatTime(a.timeCreated)), ).toBeInTheDocument(); @@ -119,14 +119,14 @@ describe('AuditView', () => { await rendered.findByTestId('audit-sidebar'); - const audit = websiteResponse.audits.find((a) => a.id === id) as Audit; + const audit = websiteResponse.audits.find(a => a.id === id) as Audit; const auditElement = rendered.getByText(formatTime(audit.timeCreated)); expect(auditElement.parentElement?.parentElement?.className).toContain( 'selected', ); const notSelectedAudit = websiteResponse.audits.find( - (a) => a.id !== id, + a => a.id !== id, ) as Audit; const notSelectedAuditElement = rendered.getByText( formatTime(notSelectedAudit.timeCreated), @@ -147,7 +147,7 @@ describe('AuditView', () => { await rendered.findByTestId('audit-sidebar'); - websiteResponse.audits.forEach((a) => { + websiteResponse.audits.forEach(a => { expect( rendered.getByText(formatTime(a.timeCreated)).parentElement ?.parentElement, @@ -186,7 +186,7 @@ describe('AuditView', () => { describe.skip('when a loading audit is accessed', () => { it('shows a loading view', async () => { - id = websiteResponse.audits.find((a) => a.status === 'RUNNING') + id = websiteResponse.audits.find(a => a.status === 'RUNNING') ?.id as string; useParams.mockReturnValueOnce({ id }); @@ -206,7 +206,7 @@ describe('AuditView', () => { describe.skip('when a failed audit is accessed', () => { it('shows an error message', async () => { - id = websiteResponse.audits.find((a) => a.status === 'FAILED') + id = websiteResponse.audits.find(a => a.status === 'FAILED') ?.id as string; useParams.mockReturnValueOnce({ id }); diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index b337698485..79897539c4 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -165,7 +165,7 @@ describe('CreateAudit', () => { fireEvent.click(rendered.getByText(/Create Audit/)); await wait(() => expect(rendered.getByLabelText(/URL/)).toBeEnabled()); - await new Promise((r) => setTimeout(r, 0)); + await new Promise(r => setTimeout(r, 0)); expect(errorApi.post).toHaveBeenCalledWith(expect.any(Error)); }); diff --git a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx index fe4ba7845a..d279afac25 100644 --- a/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx +++ b/plugins/register-component/src/components/RegisterComponentForm/RegisterComponentForm.tsx @@ -28,7 +28,7 @@ import { BackstageTheme } from '@backstage/theme'; import { Progress } from '@backstage/core'; import { ComponentIdValidators } from '../../util/validate'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ form: { alignItems: 'flex-start', display: 'flex', diff --git a/plugins/sentry-backend/README.md b/plugins/sentry-backend/README.md index 412306dc8b..efd4c1b472 100644 --- a/plugins/sentry-backend/README.md +++ b/plugins/sentry-backend/README.md @@ -1,3 +1,3 @@ # sentry-backend -Simple plugin forwarding requests to [Sentry](https://sentry.io) API. +Simple plugin forwarding requests to [Sentry](https://sentry.io) API. diff --git a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx index 408014b790..617bdb3ce9 100644 --- a/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx +++ b/plugins/sentry/src/components/ErrorCell/ErrorCell.tsx @@ -22,7 +22,7 @@ import { BackstageTheme } from '@backstage/theme'; function stripText(text: string, maxLength: number) { return text.length > maxLength ? `${text.substr(0, maxLength)}...` : text; } -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ root: { minWidth: 260, position: 'relative', diff --git a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx index 3e5ed98dba..e861c6bbe5 100644 --- a/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx +++ b/plugins/sentry/src/components/SentryIssuesTable/SentryIssuesTable.tsx @@ -24,16 +24,16 @@ import { ErrorGraph } from '../ErrorGraph/ErrorGraph'; const columns: TableColumn[] = [ { title: 'Error', - render: (data) => , + render: data => , }, { title: 'Graph', - render: (data) => , + render: data => , }, { title: 'First seen', field: 'firstSeen', - render: (data) => { + render: data => { const { firstSeen } = data as SentryIssue; return format(firstSeen); }, @@ -41,7 +41,7 @@ const columns: TableColumn[] = [ { title: 'Last seen', field: 'lastSeen', - render: (data) => { + render: data => { const { lastSeen } = data as SentryIssue; return format(lastSeen); }, diff --git a/plugins/sentry/src/data/mock-api.ts b/plugins/sentry/src/data/mock-api.ts index e26cce1762..215e9f7734 100644 --- a/plugins/sentry/src/data/mock-api.ts +++ b/plugins/sentry/src/data/mock-api.ts @@ -33,7 +33,7 @@ function getMockIssues(number: number): SentryIssue[] { } export class MockSentryApi implements SentryApi { fetchIssues(): Promise { - return new Promise((resolve) => { + return new Promise(resolve => { setTimeout(() => resolve(getMockIssues(14)), 800); }); } diff --git a/plugins/tech-radar/src/components/Radar/Radar.jsx b/plugins/tech-radar/src/components/Radar/Radar.jsx index ee956bfb89..11f763f465 100644 --- a/plugins/tech-radar/src/components/Radar/Radar.jsx +++ b/plugins/tech-radar/src/components/Radar/Radar.jsx @@ -105,14 +105,14 @@ export default class Radar extends React.Component { static adjustEntries(entries, activeEntry, quadrants, rings, radius) { let seed = 42; entries.forEach((entry, idx) => { - const quadrant = quadrants.find((q) => { + const quadrant = quadrants.find(q => { const match = typeof entry.quadrant === 'object' ? entry.quadrant.id : entry.quadrant; return q.id === match; }); - const ring = rings.find((r) => { + const ring = rings.find(r => { const match = typeof entry.ring === 'object' ? entry.ring.id : entry.ring; return r.id === match; @@ -190,7 +190,7 @@ export default class Radar extends React.Component { return ( { + ref={node => { this.node = node; }} width={width} @@ -205,7 +205,7 @@ export default class Radar extends React.Component { quadrants={quadrants} rings={rings} activeEntry={activeEntry} - onEntryMouseEnter={(entry) => this._setActiveEntry(entry)} + onEntryMouseEnter={entry => this._setActiveEntry(entry)} onEntryMouseLeave={() => this._clearActiveEntry()} /> diff --git a/plugins/tech-radar/src/components/RadarBubble/RadarBubble.jsx b/plugins/tech-radar/src/components/RadarBubble/RadarBubble.jsx index 83487d637e..072f80ca20 100644 --- a/plugins/tech-radar/src/components/RadarBubble/RadarBubble.jsx +++ b/plugins/tech-radar/src/components/RadarBubble/RadarBubble.jsx @@ -49,16 +49,16 @@ class RadarBubble extends React.PureComponent { this._updatePosition(); } - _setRect = (rect) => { + _setRect = rect => { this.rect = rect; }; - _setNode = (node) => { + _setNode = node => { this.node = node; }; - _setText = (text) => { + _setText = text => { this.text = text; }; - _setPath = (path) => { + _setPath = path => { this.path = path; }; diff --git a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.jsx b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.jsx index f48a8e9514..ce6c046242 100644 --- a/plugins/tech-radar/src/components/RadarGrid/RadarGrid.jsx +++ b/plugins/tech-radar/src/components/RadarGrid/RadarGrid.jsx @@ -84,7 +84,7 @@ class RadarGrid extends React.PureComponent { />, ]; - const ringNodes = rings.map((r) => r.outerRadius).map(makeRingNode); + const ringNodes = rings.map(r => r.outerRadius).map(makeRingNode); return axisNodes.concat(ringNodes); } diff --git a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.jsx b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.jsx index 5fb7b5a1ba..674dd34cbb 100644 --- a/plugins/tech-radar/src/components/RadarLegend/RadarLegend.jsx +++ b/plugins/tech-radar/src/components/RadarLegend/RadarLegend.jsx @@ -88,7 +88,7 @@ class RadarLegend extends React.PureComponent {

{quadrant.name}

- {rings.map((ring) => + {rings.map(ring => RadarLegend._renderRing( ring, RadarLegend._getSegment(segments, quadrant, ring), @@ -117,7 +117,7 @@ class RadarLegend extends React.PureComponent {

(empty)

) : (
    - {entries.map((entry) => { + {entries.map(entry => { let node = {entry.title}; if (entry.url) { @@ -175,7 +175,7 @@ class RadarLegend extends React.PureComponent { return ( - {quadrants.map((quadrant) => + {quadrants.map(quadrant => RadarLegend._renderQuadrant( segments, quadrant, diff --git a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.jsx b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.jsx index afa4c4cd95..b93d20cbff 100644 --- a/plugins/tech-radar/src/components/RadarPlot/RadarPlot.jsx +++ b/plugins/tech-radar/src/components/RadarPlot/RadarPlot.jsx @@ -46,16 +46,16 @@ export default class RadarPlot extends React.PureComponent { rings={rings} entries={entries} onEntryMouseEnter={ - onEntryMouseEnter && ((entry) => onEntryMouseEnter(entry)) + onEntryMouseEnter && (entry => onEntryMouseEnter(entry)) } onEntryMouseLeave={ - onEntryMouseLeave && ((entry) => onEntryMouseLeave(entry)) + onEntryMouseLeave && (entry => onEntryMouseLeave(entry)) } /> - {entries.map((entry) => ( + {entries.map(entry => ( Date: Wed, 27 May 2020 10:46:26 +0200 Subject: [PATCH 41/58] Bump Helmet to 6.0.0 (#1019) * Bump Helmet to 6.0.0 * fix: failing tests Co-authored-by: Nikita Nek Dudnik --- packages/core/package.json | 2 +- .../ContentHeader/ContentHeader.test.tsx | 4 ++- .../layout/ContentHeader/ContentHeader.tsx | 2 +- .../core/src/layout/Header/Header.test.tsx | 4 ++- packages/core/src/layout/Header/Header.tsx | 7 ++--- yarn.lock | 30 +++++++++---------- 6 files changed, 25 insertions(+), 24 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index b1800f5db3..00e80e8e0b 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -48,7 +48,7 @@ "react": "^16.12.0", "react-addons-text-content": "0.0.4", "react-dom": "^16.12.0", - "react-helmet": "5.2.1", + "react-helmet": "6.0.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-sparklines": "^1.7.0", diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx index 6f5a1b24cc..703c27e32c 100644 --- a/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx +++ b/packages/core/src/layout/ContentHeader/ContentHeader.test.tsx @@ -20,7 +20,9 @@ import ContentHeader from './ContentHeader'; import { wrapInThemedTestApp } from '@backstage/test-utils'; jest.mock('react-helmet', () => { - return ({ defaultTitle }: any) =>
    defaultTitle: {defaultTitle}
    ; + return { + Helmet: ({ defaultTitle }: any) =>
    defaultTitle: {defaultTitle}
    , + }; }); describe('', () => { diff --git a/packages/core/src/layout/ContentHeader/ContentHeader.tsx b/packages/core/src/layout/ContentHeader/ContentHeader.tsx index 409cfe6b90..1a51d5d19b 100644 --- a/packages/core/src/layout/ContentHeader/ContentHeader.tsx +++ b/packages/core/src/layout/ContentHeader/ContentHeader.tsx @@ -20,7 +20,7 @@ import React, { ComponentType, Fragment, FC } from 'react'; import { Typography, makeStyles } from '@material-ui/core'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; const useStyles = makeStyles(theme => ({ container: { diff --git a/packages/core/src/layout/Header/Header.test.tsx b/packages/core/src/layout/Header/Header.test.tsx index 85b15e33aa..afedf2ad35 100644 --- a/packages/core/src/layout/Header/Header.test.tsx +++ b/packages/core/src/layout/Header/Header.test.tsx @@ -20,7 +20,9 @@ import { wrapInThemedTestApp } from '@backstage/test-utils'; import Header from './Header'; jest.mock('react-helmet', () => { - return ({ defaultTitle }: any) =>
    defaultTitle: {defaultTitle}
    ; + return { + Helmet: ({ defaultTitle }: any) =>
    defaultTitle: {defaultTitle}
    , + }; }); describe('
    ', () => { diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index ab3d0080e4..9fc4807733 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -15,15 +15,14 @@ */ import React, { Fragment, ReactNode, CSSProperties, FC } from 'react'; -import Helmet from 'react-helmet'; +import { Helmet } from 'react-helmet'; import { Typography, Tooltip, makeStyles } from '@material-ui/core'; import { BackstageTheme } from '@backstage/theme'; import { Theme } from '../Page/Page'; -// import { Link } from 'shared/components'; import Waves from './Waves'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ header: { gridArea: 'pageHeader', padding: theme.spacing(3), @@ -175,7 +174,7 @@ export const Header: FC = ({ - {(theme) => ( + {theme => (
    diff --git a/yarn.lock b/yarn.lock index 772229aa98..55255c89d9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16892,7 +16892,7 @@ promzard@^0.3.0: dependencies: read "1" -prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: +prop-types@^15.5.10, prop-types@^15.6.0, prop-types@^15.6.1, prop-types@^15.6.2, prop-types@^15.7.2: version "15.7.2" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz#52c41e75b8c87e72b9d9360e0206b99dcbffa6c5" integrity sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== @@ -17298,7 +17298,7 @@ react-error-overlay@^6.0.3, react-error-overlay@^6.0.7: resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.7.tgz#1dcfb459ab671d53f660a991513cb2f0a0553108" integrity sha512-TAv1KJFh3RhqxNvhzxj6LeT5NWklP6rDr2a0jaTfsZ5wSZWHOGeqQyejUp3xxLfPt2UpyJEcVQB/zyPcmonNFA== -react-fast-compare@^2.0.2, react-fast-compare@^2.0.4: +react-fast-compare@^2.0.4: version "2.0.4" resolved "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-2.0.4.tgz#e84b4d455b0fec113e0402c329352715196f81f9" integrity sha512-suNP+J1VU1MWFKcyt7RtjiSWUjvidmQSlqu+eHslq+342xCbGTYmC0mEhPCOHxlW0CywylOC1u2DFAT+bv4dBw== @@ -17326,15 +17326,15 @@ react-helmet-async@^1.0.2: react-fast-compare "^2.0.4" shallowequal "^1.1.0" -react-helmet@5.2.1: - version "5.2.1" - resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-5.2.1.tgz#16a7192fdd09951f8e0fe22ffccbf9bb3e591ffa" - integrity sha512-CnwD822LU8NDBnjCpZ4ySh8L6HYyngViTZLfBBb3NjtrpN8m49clH8hidHouq20I51Y6TpCTISCBbqiY5GamwA== +react-helmet@6.0.0: + version "6.0.0" + resolved "https://registry.npmjs.org/react-helmet/-/react-helmet-6.0.0.tgz#fcb93ebaca3ba562a686eb2f1f9d46093d83b5f8" + integrity sha512-My6S4sa0uHN/IuVUn0HFmasW5xj9clTkB9qmMngscVycQ5vVG51Qp44BEvLJ4lixupTwDlU9qX1/sCrMN4AEPg== dependencies: object-assign "^4.1.1" - prop-types "^15.5.4" - react-fast-compare "^2.0.2" - react-side-effect "^1.1.0" + prop-types "^15.7.2" + react-fast-compare "^2.0.4" + react-side-effect "^2.1.0" react-hook-form@^5.7.2: version "5.7.2" @@ -17471,12 +17471,10 @@ react-router@5.2.0, react-router@^5.1.2, react-router@^5.2.0: tiny-invariant "^1.0.2" tiny-warning "^1.0.0" -react-side-effect@^1.1.0: - version "1.2.0" - resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-1.2.0.tgz#0e940c78faba0c73b9b0eba9cd3dda8dfb7e7dae" - integrity sha512-v1ht1aHg5k/thv56DRcjw+WtojuuDHFUgGfc+bFHOWsF4ZK6C2V57DO0Or0GPsg6+LSTE0M6Ry/gfzhzSwbc5w== - dependencies: - shallowequal "^1.0.1" +react-side-effect@^2.1.0: + version "2.1.0" + resolved "https://registry.npmjs.org/react-side-effect/-/react-side-effect-2.1.0.tgz#1ce4a8b4445168c487ed24dab886421f74d380d3" + integrity sha512-IgmcegOSi5SNX+2Snh1vqmF0Vg/CbkycU9XZbOHJlZ6kMzTmi3yc254oB1WCkgA7OQtIAoLmcSFuHTc/tlcqXg== react-sizeme@^2.6.7: version "2.6.12" @@ -18742,7 +18740,7 @@ shallow-equal@^1.1.0: resolved "https://registry.npmjs.org/shallow-equal/-/shallow-equal-1.2.1.tgz#4c16abfa56043aa20d050324efa68940b0da79da" integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA== -shallowequal@^1.0.1, shallowequal@^1.1.0: +shallowequal@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz#188d521de95b9087404fd4dcb68b13df0ae4e7f8" integrity sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ== From aeffe49ad3d9f39df7146ebe11b0c14acdce338b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Wed, 27 May 2020 10:53:59 +0200 Subject: [PATCH 42/58] Add Stories for Header variants (#1027) * Add Stories for Header variants * Set service type --- .../core/src/layout/Header/Header.stories.tsx | 94 +++++++++++++++++++ packages/core/src/layout/Header/Header.tsx | 2 +- .../core/src/layout/Page/PageThemeProvider.ts | 15 +++ .../ComponentPage/ComponentPage.tsx | 4 +- .../components/PluginHeader/PluginHeader.tsx | 2 +- 5 files changed, 113 insertions(+), 4 deletions(-) create mode 100644 packages/core/src/layout/Header/Header.stories.tsx diff --git a/packages/core/src/layout/Header/Header.stories.tsx b/packages/core/src/layout/Header/Header.stories.tsx new file mode 100644 index 0000000000..e1198d7b65 --- /dev/null +++ b/packages/core/src/layout/Header/Header.stories.tsx @@ -0,0 +1,94 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import Header from '.'; +import HeaderLabel from '../HeaderLabel'; +import Page, { pageTheme } from '../Page'; + +export default { + title: 'Header', + component: Header, +}; + +const labels = ( + <> + + + + +); + +export const Home = () => ( + +
    + {labels} +
    +
    +); + +export const HomeWithSubtitle = () => ( +
    + {labels} +
    +); + +export const Tool = () => ( + +
    + {labels} +
    +
    +); + +export const Service = () => ( + +
    + {labels} +
    +
    +); + +export const Website = () => ( + +
    + {labels} +
    +
    +); + +export const Library = () => ( + +
    + {labels} +
    +
    +); + +export const App = () => ( + +
    + {labels} +
    +
    +); + +export const Other = () => ( + +
    + {labels} +
    +
    +); diff --git a/packages/core/src/layout/Header/Header.tsx b/packages/core/src/layout/Header/Header.tsx index 9fc4807733..47fcc324a7 100644 --- a/packages/core/src/layout/Header/Header.tsx +++ b/packages/core/src/layout/Header/Header.tsx @@ -148,7 +148,7 @@ const SubtitleFragment: FC = ({ classes, subtitle }) => { } return ( - + {subtitle} ); diff --git a/packages/core/src/layout/Page/PageThemeProvider.ts b/packages/core/src/layout/Page/PageThemeProvider.ts index a767033029..9c31fcb4ac 100644 --- a/packages/core/src/layout/Page/PageThemeProvider.ts +++ b/packages/core/src/layout/Page/PageThemeProvider.ts @@ -89,4 +89,19 @@ export const pageTheme: Record = { tool: { gradient: gradients.purpleBlue, }, + service: { + gradient: gradients.green, + }, + website: { + gradient: gradients.purple, + }, + library: { + gradient: gradients.sunset, + }, + other: { + gradient: gradients.brown, + }, + app: { + gradient: gradients.redOrange, + }, }; diff --git a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx index 0cf7017173..a1e33597a2 100644 --- a/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx +++ b/plugins/catalog/src/components/ComponentPage/ComponentPage.tsx @@ -83,8 +83,8 @@ const ComponentPage: FC = ({ }; return ( - -
    + +
    {confirmationDialogOpen && catalogRequest.value && ( diff --git a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx index 4bab4b5921..9f94cff480 100644 --- a/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx +++ b/plugins/circleci/src/components/PluginHeader/PluginHeader.tsx @@ -22,7 +22,7 @@ import SettingsIcon from '@material-ui/icons/Settings'; import { useSettings } from '../../state'; export type Props = { title?: string }; -export const PluginHeader: FC = ({ title = 'Circle CI' }) => { +export const PluginHeader: FC = ({ title = 'CircleCI' }) => { const [, { showSettings }] = useSettings(); const location = useLocation(); const notRoot = !location.pathname.match(/\/circleci\/?$/); From 3a27313f78ed7812686be89a2616580a6e88a0af Mon Sep 17 00:00:00 2001 From: mutugiii Date: Wed, 27 May 2020 12:06:47 +0300 Subject: [PATCH 43/58] Fix app entry point with extra comma (Fixes #1028) --- packages/app/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index b0ff8b14c7..56faf4402b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -40,7 +40,7 @@ const App: FC<{}> = () => ( - , + From 1a6a72e3d5abacb8f9ad1a6836204bda556c1a5e Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 27 May 2020 11:07:28 +0200 Subject: [PATCH 44/58] bug: removing the extraneous comma --- packages/app/src/App.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index b0ff8b14c7..56faf4402b 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -40,7 +40,7 @@ const App: FC<{}> = () => ( - , + From e17ff776c12c58640008264720cab596d8e084f8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 May 2020 02:00:18 +0200 Subject: [PATCH 45/58] packages/core: move api dir into separate core-api package --- packages/core-api/.eslintrc.js | 8 +++ packages/core-api/README.md | 22 ++++++ packages/core-api/package.json | 71 +++++++++++++++++++ .../src}/apis/ApiAggregator.test.ts | 0 .../src}/apis/ApiAggregator.ts | 0 .../src}/apis/ApiProvider.test.tsx | 0 .../api => core-api/src}/apis/ApiProvider.tsx | 0 .../api => core-api/src}/apis/ApiRef.test.ts | 0 .../src/api => core-api/src}/apis/ApiRef.ts | 0 .../src}/apis/ApiRegistry.test.ts | 0 .../api => core-api/src}/apis/ApiRegistry.ts | 0 .../src}/apis/ApiTestRegistry.test.ts | 0 .../src}/apis/ApiTestRegistry.ts | 0 .../src}/apis/definitions/AlertApi.ts | 0 .../src}/apis/definitions/AppThemeApi.ts | 0 .../src}/apis/definitions/ErrorApi.ts | 0 .../src}/apis/definitions/FeatureFlagsApi.ts | 0 .../src}/apis/definitions/OAuthRequestApi.ts | 2 +- .../src}/apis/definitions/auth.ts | 0 .../src}/apis/definitions/index.ts | 0 .../src/api => core-api/src}/apis/helpers.ts | 0 .../AlertApi/AlertApiForwarder.ts | 4 +- .../apis/implementations/AlertApi/index.ts | 0 .../AppThemeApi/AppThemeSelector.test.ts | 0 .../AppThemeApi/AppThemeSelector.ts | 2 +- .../apis/implementations/AppThemeApi/index.ts | 0 .../implementations/ErrorApi/ErrorAlerter.ts | 2 +- .../ErrorApi/ErrorApiForwarder.ts | 4 +- .../apis/implementations/ErrorApi/index.ts | 0 .../OAuthRequestApi/MockOAuthApi.test.ts | 0 .../OAuthRequestApi/MockOAuthApi.ts | 0 .../OAuthPendingRequests.test.ts | 0 .../OAuthRequestApi/OAuthPendingRequests.ts | 2 +- .../OAuthRequestManager.test.ts | 0 .../OAuthRequestApi/OAuthRequestManager.ts | 2 +- .../implementations/OAuthRequestApi/index.ts | 0 .../auth/google/GoogleAuth.test.ts | 0 .../implementations/auth/google/GoogleAuth.ts | 6 +- .../apis/implementations/auth/google/index.ts | 0 .../apis/implementations/auth/google/types.ts | 0 .../src}/apis/implementations/auth/index.ts | 0 .../src}/apis/implementations/index.ts | 0 .../src/api => core-api/src}/apis/index.ts | 0 .../src/api => core-api/src}/apis/types.ts | 0 .../src/api => core-api/src}/app/App.tsx | 50 +------------ .../api => core-api/src}/app/AppContext.tsx | 0 .../src}/app/AppThemeProvider.tsx | 0 .../src}/app/FeatureFlags.test.tsx | 0 .../api => core-api/src}/app/FeatureFlags.tsx | 0 .../src/api => core-api/src}/app/index.ts | 1 - .../src/api => core-api/src}/app/types.ts | 2 +- packages/core-api/src/icons/icons.tsx | 39 ++++++++++ packages/core-api/src/icons/index.ts | 18 +++++ packages/core-api/src/icons/types.ts | 22 ++++++ packages/core-api/src/index.ts | 19 +++++ .../DefaultAuthConnector.test.ts | 2 +- .../lib/AuthConnector/DefaultAuthConnector.ts | 4 +- .../AuthConnector/MockAuthConnector.test.ts | 0 .../lib/AuthConnector/MockAuthConnector.ts | 0 .../src}/lib/AuthConnector/index.ts | 0 .../src}/lib/AuthConnector/types.ts | 0 .../AuthSessionStore.test.ts | 0 .../AuthSessionManager/AuthSessionStore.ts | 0 .../RefreshingAuthSessionManager.test.ts | 0 .../RefreshingAuthSessionManager.ts | 0 .../StaticAuthSessionManager.test.ts | 0 .../StaticAuthSessionManager.ts | 0 .../src}/lib/AuthSessionManager/common.ts | 0 .../src}/lib/AuthSessionManager/index.ts | 0 .../src}/lib/AuthSessionManager/types.ts | 0 packages/core-api/src/lib/index.ts | 20 ++++++ .../src}/lib/loginPopup.test.ts | 0 .../src}/lib/loginPopup.ts | 0 .../src}/lib/subjects.test.ts | 0 .../src}/lib/subjects.ts | 2 +- .../api => core-api/src}/plugin/Plugin.tsx | 0 .../src/api => core-api/src}/plugin/index.ts | 0 .../src/api => core-api/src}/plugin/types.ts | 0 .../lib/index.ts => core-api/src/private.ts} | 2 +- packages/core-api/src/public.ts | 22 ++++++ .../api => core-api/src}/routing/RouteRef.ts | 0 .../src/api => core-api/src}/routing/index.ts | 0 .../src/api => core-api/src}/routing/types.ts | 2 +- packages/core-api/src/setupTests.ts | 18 +++++ .../{core/src/api => core-api/src}/types.ts | 0 packages/core/package.json | 1 + packages/core/src/api/createApp.tsx | 69 ++++++++++++++++++ packages/core/src/api/index.ts | 6 +- .../components/AlertDisplay/AlertDisplay.tsx | 2 +- .../CopyTextButton/CopyTextButton.stories.tsx | 7 +- .../CopyTextButton/CopyTextButton.test.tsx | 7 +- .../CopyTextButton/CopyTextButton.tsx | 8 +-- .../LoginRequestListItem.tsx | 2 +- .../OAuthRequestDialog/OAuthRequestDialog.tsx | 2 +- packages/core/src/icons/icons.tsx | 4 +- packages/core/src/index.ts | 2 + .../src/layout/Sidebar/SidebarThemeToggle.tsx | 2 +- 97 files changed, 376 insertions(+), 84 deletions(-) create mode 100644 packages/core-api/.eslintrc.js create mode 100644 packages/core-api/README.md create mode 100644 packages/core-api/package.json rename packages/{core/src/api => core-api/src}/apis/ApiAggregator.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/ApiAggregator.ts (100%) rename packages/{core/src/api => core-api/src}/apis/ApiProvider.test.tsx (100%) rename packages/{core/src/api => core-api/src}/apis/ApiProvider.tsx (100%) rename packages/{core/src/api => core-api/src}/apis/ApiRef.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/ApiRef.ts (100%) rename packages/{core/src/api => core-api/src}/apis/ApiRegistry.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/ApiRegistry.ts (100%) rename packages/{core/src/api => core-api/src}/apis/ApiTestRegistry.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/ApiTestRegistry.ts (100%) rename packages/{core/src/api => core-api/src}/apis/definitions/AlertApi.ts (100%) rename packages/{core/src/api => core-api/src}/apis/definitions/AppThemeApi.ts (100%) rename packages/{core/src/api => core-api/src}/apis/definitions/ErrorApi.ts (100%) rename packages/{core/src/api => core-api/src}/apis/definitions/FeatureFlagsApi.ts (100%) rename packages/{core/src/api => core-api/src}/apis/definitions/OAuthRequestApi.ts (98%) rename packages/{core/src/api => core-api/src}/apis/definitions/auth.ts (100%) rename packages/{core/src/api => core-api/src}/apis/definitions/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/helpers.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/AlertApi/AlertApiForwarder.ts (91%) rename packages/{core/src/api => core-api/src}/apis/implementations/AlertApi/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/AppThemeApi/AppThemeSelector.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/AppThemeApi/AppThemeSelector.ts (97%) rename packages/{core/src/api => core-api/src}/apis/implementations/AppThemeApi/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/ErrorApi/ErrorAlerter.ts (94%) rename packages/{core/src/api => core-api/src}/apis/implementations/ErrorApi/ErrorApiForwarder.ts (91%) rename packages/{core/src/api => core-api/src}/apis/implementations/ErrorApi/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/OAuthRequestApi/MockOAuthApi.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts (98%) rename packages/{core/src/api => core-api/src}/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts (98%) rename packages/{core/src/api => core-api/src}/apis/implementations/OAuthRequestApi/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/auth/google/GoogleAuth.test.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/auth/google/GoogleAuth.ts (94%) rename packages/{core/src/api => core-api/src}/apis/implementations/auth/google/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/auth/google/types.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/auth/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/implementations/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/index.ts (100%) rename packages/{core/src/api => core-api/src}/apis/types.ts (100%) rename packages/{core/src/api => core-api/src}/app/App.tsx (80%) rename packages/{core/src/api => core-api/src}/app/AppContext.tsx (100%) rename packages/{core/src/api => core-api/src}/app/AppThemeProvider.tsx (100%) rename packages/{core/src/api => core-api/src}/app/FeatureFlags.test.tsx (100%) rename packages/{core/src/api => core-api/src}/app/FeatureFlags.tsx (100%) rename packages/{core/src/api => core-api/src}/app/index.ts (95%) rename packages/{core/src/api => core-api/src}/app/types.ts (97%) create mode 100644 packages/core-api/src/icons/icons.tsx create mode 100644 packages/core-api/src/icons/index.ts create mode 100644 packages/core-api/src/icons/types.ts create mode 100644 packages/core-api/src/index.ts rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthConnector/DefaultAuthConnector.test.ts (98%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthConnector/DefaultAuthConnector.ts (97%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthConnector/MockAuthConnector.test.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthConnector/MockAuthConnector.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthConnector/index.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthConnector/types.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/AuthSessionStore.test.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/AuthSessionStore.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/RefreshingAuthSessionManager.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/StaticAuthSessionManager.test.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/StaticAuthSessionManager.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/common.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/index.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/AuthSessionManager/types.ts (100%) create mode 100644 packages/core-api/src/lib/index.ts rename packages/{core/src/api/apis/implementations => core-api/src}/lib/loginPopup.test.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/loginPopup.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/subjects.test.ts (100%) rename packages/{core/src/api/apis/implementations => core-api/src}/lib/subjects.ts (99%) rename packages/{core/src/api => core-api/src}/plugin/Plugin.tsx (100%) rename packages/{core/src/api => core-api/src}/plugin/index.ts (100%) rename packages/{core/src/api => core-api/src}/plugin/types.ts (100%) rename packages/{core/src/api/apis/implementations/lib/index.ts => core-api/src/private.ts} (93%) create mode 100644 packages/core-api/src/public.ts rename packages/{core/src/api => core-api/src}/routing/RouteRef.ts (100%) rename packages/{core/src/api => core-api/src}/routing/index.ts (100%) rename packages/{core/src/api => core-api/src}/routing/types.ts (95%) create mode 100644 packages/core-api/src/setupTests.ts rename packages/{core/src/api => core-api/src}/types.ts (100%) create mode 100644 packages/core/src/api/createApp.tsx diff --git a/packages/core-api/.eslintrc.js b/packages/core-api/.eslintrc.js new file mode 100644 index 0000000000..d592a653c8 --- /dev/null +++ b/packages/core-api/.eslintrc.js @@ -0,0 +1,8 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], + rules: { + // TODO: add prop types to JS and remove + 'react/prop-types': 0, + 'jest/expect-expect': 0, + }, +}; diff --git a/packages/core-api/README.md b/packages/core-api/README.md new file mode 100644 index 0000000000..5732b5ef23 --- /dev/null +++ b/packages/core-api/README.md @@ -0,0 +1,22 @@ +# @backstage/core-api + +This package provides the core API used by Backstage plugins and apps. + +## Installation + +Do not install this package directly, it is reexported by `@backstage/core`. Install that instead: + +```sh +$ npm install --save @backstage/core +``` + +or + +```sh +$ yarn add @backstage/core +``` + +## Documentation + +- [Backstage Readme](https://github.com/spotify/backstage/blob/master/README.md) +- [Backstage Documentation](https://github.com/spotify/backstage/blob/master/docs/README.md) diff --git a/packages/core-api/package.json b/packages/core-api/package.json new file mode 100644 index 0000000000..88d593dcf8 --- /dev/null +++ b/packages/core-api/package.json @@ -0,0 +1,71 @@ +{ + "name": "@backstage/core-api", + "description": "Core API used by Backstage plugins and apps", + "version": "0.1.1-alpha.6", + "private": false, + "publishConfig": { + "access": "public" + }, + "homepage": "https://backstage.io", + "repository": { + "type": "git", + "url": "https://github.com/spotify/backstage", + "directory": "packages/core" + }, + "keywords": [ + "backstage" + ], + "license": "Apache-2.0", + "main": "dist/index.esm.js", + "main:src": "src/index.ts", + "types": "src/index.ts", + "scripts": { + "build": "backstage-cli plugin:build", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/theme": "^0.1.1-alpha.6", + "@material-ui/core": "^4.9.1", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@types/classnames": "^2.2.9", + "@types/google-protobuf": "^3.7.2", + "@types/jest": "^25.2.2", + "@types/node": "^12.0.0", + "@types/react-helmet": "^5.0.15", + "@types/react-sparklines": "^1.7.0", + "@types/zen-observable": "^0.8.0", + "classnames": "^2.2.6", + "clsx": "^1.1.0", + "lodash": "^4.17.15", + "material-table": "^1.58.0", + "prop-types": "^15.7.2", + "rc-progress": "^3.0.0", + "react": "^16.12.0", + "react-addons-text-content": "0.0.4", + "react-dom": "^16.12.0", + "react-helmet": "5.2.1", + "react-router": "^5.2.0", + "react-router-dom": "^5.2.0", + "react-sparklines": "^1.7.0", + "react-syntax-highlighter": "^12.2.1", + "react-use": "^14.2.0", + "zen-observable": "^0.8.15" + }, + "devDependencies": { + "@backstage/cli": "^0.1.1-alpha.6", + "@backstage/test-utils": "^0.1.1-alpha.6", + "@backstage/test-utils-core": "^0.1.1-alpha.6", + "@testing-library/jest-dom": "^5.7.0", + "@testing-library/react": "^9.3.2", + "@testing-library/user-event": "^10.2.4", + "jest-fetch-mock": "^3.0.3" + }, + "files": [ + "dist/**/*.{js,d.ts}" + ] +} diff --git a/packages/core/src/api/apis/ApiAggregator.test.ts b/packages/core-api/src/apis/ApiAggregator.test.ts similarity index 100% rename from packages/core/src/api/apis/ApiAggregator.test.ts rename to packages/core-api/src/apis/ApiAggregator.test.ts diff --git a/packages/core/src/api/apis/ApiAggregator.ts b/packages/core-api/src/apis/ApiAggregator.ts similarity index 100% rename from packages/core/src/api/apis/ApiAggregator.ts rename to packages/core-api/src/apis/ApiAggregator.ts diff --git a/packages/core/src/api/apis/ApiProvider.test.tsx b/packages/core-api/src/apis/ApiProvider.test.tsx similarity index 100% rename from packages/core/src/api/apis/ApiProvider.test.tsx rename to packages/core-api/src/apis/ApiProvider.test.tsx diff --git a/packages/core/src/api/apis/ApiProvider.tsx b/packages/core-api/src/apis/ApiProvider.tsx similarity index 100% rename from packages/core/src/api/apis/ApiProvider.tsx rename to packages/core-api/src/apis/ApiProvider.tsx diff --git a/packages/core/src/api/apis/ApiRef.test.ts b/packages/core-api/src/apis/ApiRef.test.ts similarity index 100% rename from packages/core/src/api/apis/ApiRef.test.ts rename to packages/core-api/src/apis/ApiRef.test.ts diff --git a/packages/core/src/api/apis/ApiRef.ts b/packages/core-api/src/apis/ApiRef.ts similarity index 100% rename from packages/core/src/api/apis/ApiRef.ts rename to packages/core-api/src/apis/ApiRef.ts diff --git a/packages/core/src/api/apis/ApiRegistry.test.ts b/packages/core-api/src/apis/ApiRegistry.test.ts similarity index 100% rename from packages/core/src/api/apis/ApiRegistry.test.ts rename to packages/core-api/src/apis/ApiRegistry.test.ts diff --git a/packages/core/src/api/apis/ApiRegistry.ts b/packages/core-api/src/apis/ApiRegistry.ts similarity index 100% rename from packages/core/src/api/apis/ApiRegistry.ts rename to packages/core-api/src/apis/ApiRegistry.ts diff --git a/packages/core/src/api/apis/ApiTestRegistry.test.ts b/packages/core-api/src/apis/ApiTestRegistry.test.ts similarity index 100% rename from packages/core/src/api/apis/ApiTestRegistry.test.ts rename to packages/core-api/src/apis/ApiTestRegistry.test.ts diff --git a/packages/core/src/api/apis/ApiTestRegistry.ts b/packages/core-api/src/apis/ApiTestRegistry.ts similarity index 100% rename from packages/core/src/api/apis/ApiTestRegistry.ts rename to packages/core-api/src/apis/ApiTestRegistry.ts diff --git a/packages/core/src/api/apis/definitions/AlertApi.ts b/packages/core-api/src/apis/definitions/AlertApi.ts similarity index 100% rename from packages/core/src/api/apis/definitions/AlertApi.ts rename to packages/core-api/src/apis/definitions/AlertApi.ts diff --git a/packages/core/src/api/apis/definitions/AppThemeApi.ts b/packages/core-api/src/apis/definitions/AppThemeApi.ts similarity index 100% rename from packages/core/src/api/apis/definitions/AppThemeApi.ts rename to packages/core-api/src/apis/definitions/AppThemeApi.ts diff --git a/packages/core/src/api/apis/definitions/ErrorApi.ts b/packages/core-api/src/apis/definitions/ErrorApi.ts similarity index 100% rename from packages/core/src/api/apis/definitions/ErrorApi.ts rename to packages/core-api/src/apis/definitions/ErrorApi.ts diff --git a/packages/core/src/api/apis/definitions/FeatureFlagsApi.ts b/packages/core-api/src/apis/definitions/FeatureFlagsApi.ts similarity index 100% rename from packages/core/src/api/apis/definitions/FeatureFlagsApi.ts rename to packages/core-api/src/apis/definitions/FeatureFlagsApi.ts diff --git a/packages/core/src/api/apis/definitions/OAuthRequestApi.ts b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts similarity index 98% rename from packages/core/src/api/apis/definitions/OAuthRequestApi.ts rename to packages/core-api/src/apis/definitions/OAuthRequestApi.ts index 5a0f399a6d..6fefbd1b9c 100644 --- a/packages/core/src/api/apis/definitions/OAuthRequestApi.ts +++ b/packages/core-api/src/apis/definitions/OAuthRequestApi.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IconComponent } from '../../../icons'; +import { IconComponent } from '../../icons'; import { Observable } from '../../types'; import { createApiRef } from '../ApiRef'; diff --git a/packages/core/src/api/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts similarity index 100% rename from packages/core/src/api/apis/definitions/auth.ts rename to packages/core-api/src/apis/definitions/auth.ts diff --git a/packages/core/src/api/apis/definitions/index.ts b/packages/core-api/src/apis/definitions/index.ts similarity index 100% rename from packages/core/src/api/apis/definitions/index.ts rename to packages/core-api/src/apis/definitions/index.ts diff --git a/packages/core/src/api/apis/helpers.ts b/packages/core-api/src/apis/helpers.ts similarity index 100% rename from packages/core/src/api/apis/helpers.ts rename to packages/core-api/src/apis/helpers.ts diff --git a/packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts b/packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts similarity index 91% rename from packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts rename to packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts index 901d3b57cc..91a606059d 100644 --- a/packages/core/src/api/apis/implementations/AlertApi/AlertApiForwarder.ts +++ b/packages/core-api/src/apis/implementations/AlertApi/AlertApiForwarder.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { AlertApi, AlertMessage } from '../../../..'; -import { PublishSubject } from '../lib'; +import { AlertApi, AlertMessage } from '../..'; +import { PublishSubject } from '../../../lib'; import { Observable } from '../../../types'; /** diff --git a/packages/core/src/api/apis/implementations/AlertApi/index.ts b/packages/core-api/src/apis/implementations/AlertApi/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/AlertApi/index.ts rename to packages/core-api/src/apis/implementations/AlertApi/index.ts diff --git a/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.test.ts b/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.test.ts rename to packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.test.ts diff --git a/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts b/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts similarity index 97% rename from packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts rename to packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts index 0f554ed7f1..43b52a254b 100644 --- a/packages/core/src/api/apis/implementations/AppThemeApi/AppThemeSelector.ts +++ b/packages/core-api/src/apis/implementations/AppThemeApi/AppThemeSelector.ts @@ -15,7 +15,7 @@ */ import { AppThemeApi, AppTheme } from '../../definitions'; -import { BehaviorSubject } from '../lib'; +import { BehaviorSubject } from '../../../lib'; import { Observable } from '../../../types'; const STORAGE_KEY = 'theme'; diff --git a/packages/core/src/api/apis/implementations/AppThemeApi/index.ts b/packages/core-api/src/apis/implementations/AppThemeApi/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/AppThemeApi/index.ts rename to packages/core-api/src/apis/implementations/AppThemeApi/index.ts diff --git a/packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts b/packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts similarity index 94% rename from packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts rename to packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts index 903463d7de..162b28f334 100644 --- a/packages/core/src/api/apis/implementations/ErrorApi/ErrorAlerter.ts +++ b/packages/core-api/src/apis/implementations/ErrorApi/ErrorAlerter.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext, AlertApi } from '../../../..'; +import { ErrorApi, ErrorContext, AlertApi } from '../..'; /** * Decorates an ErrorApi by also forwarding error messages diff --git a/packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts b/packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts similarity index 91% rename from packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts rename to packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts index 6729050401..2ebf9db7a8 100644 --- a/packages/core/src/api/apis/implementations/ErrorApi/ErrorApiForwarder.ts +++ b/packages/core-api/src/apis/implementations/ErrorApi/ErrorApiForwarder.ts @@ -13,8 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ErrorApi, ErrorContext } from '../../../..'; -import { PublishSubject } from '../lib'; +import { ErrorApi, ErrorContext } from '../..'; +import { PublishSubject } from '../../../lib'; import { Observable } from '../../../types'; /** diff --git a/packages/core/src/api/apis/implementations/ErrorApi/index.ts b/packages/core-api/src/apis/implementations/ErrorApi/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/ErrorApi/index.ts rename to packages/core-api/src/apis/implementations/ErrorApi/index.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts rename to packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.test.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestApi/MockOAuthApi.ts rename to packages/core-api/src/apis/implementations/OAuthRequestApi/MockOAuthApi.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts rename to packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.test.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts similarity index 98% rename from packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts rename to packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts index aa5609b1e9..6287f35015 100644 --- a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts +++ b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthPendingRequests.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { BehaviorSubject } from '../lib'; +import { BehaviorSubject } from '../../../lib'; import { Observable } from '../../../types'; type RequestQueueEntry = { diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts rename to packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.test.ts diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts similarity index 98% rename from packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts rename to packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts index 1168d6abd4..23e9e1ddc4 100644 --- a/packages/core/src/api/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts +++ b/packages/core-api/src/apis/implementations/OAuthRequestApi/OAuthRequestManager.ts @@ -21,7 +21,7 @@ import { AuthRequesterOptions, } from '../../definitions'; import { OAuthPendingRequests, PendingRequest } from './OAuthPendingRequests'; -import { BehaviorSubject } from '../lib'; +import { BehaviorSubject } from '../../../lib'; import { Observable } from '../../../types'; /** diff --git a/packages/core/src/api/apis/implementations/OAuthRequestApi/index.ts b/packages/core-api/src/apis/implementations/OAuthRequestApi/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/OAuthRequestApi/index.ts rename to packages/core-api/src/apis/implementations/OAuthRequestApi/index.ts diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.test.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/auth/google/GoogleAuth.test.ts rename to packages/core-api/src/apis/implementations/auth/google/GoogleAuth.test.ts diff --git a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts similarity index 94% rename from packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts rename to packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts index dbcf53dbe0..e52e8f0397 100644 --- a/packages/core/src/api/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -15,7 +15,7 @@ */ import GoogleIcon from '@material-ui/icons/AcUnit'; -import { DefaultAuthConnector } from '../../lib/AuthConnector'; +import { DefaultAuthConnector } from '../../../../lib/AuthConnector'; import { GoogleSession } from './types'; import { OAuthApi, @@ -23,8 +23,8 @@ import { IdTokenOptions, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; -import { SessionManager } from '../../lib/AuthSessionManager/types'; -import { RefreshingAuthSessionManager } from '../../lib/AuthSessionManager'; +import { SessionManager } from '../../../../lib/AuthSessionManager/types'; +import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth diff --git a/packages/core/src/api/apis/implementations/auth/google/index.ts b/packages/core-api/src/apis/implementations/auth/google/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/auth/google/index.ts rename to packages/core-api/src/apis/implementations/auth/google/index.ts diff --git a/packages/core/src/api/apis/implementations/auth/google/types.ts b/packages/core-api/src/apis/implementations/auth/google/types.ts similarity index 100% rename from packages/core/src/api/apis/implementations/auth/google/types.ts rename to packages/core-api/src/apis/implementations/auth/google/types.ts diff --git a/packages/core/src/api/apis/implementations/auth/index.ts b/packages/core-api/src/apis/implementations/auth/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/auth/index.ts rename to packages/core-api/src/apis/implementations/auth/index.ts diff --git a/packages/core/src/api/apis/implementations/index.ts b/packages/core-api/src/apis/implementations/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/index.ts rename to packages/core-api/src/apis/implementations/index.ts diff --git a/packages/core/src/api/apis/index.ts b/packages/core-api/src/apis/index.ts similarity index 100% rename from packages/core/src/api/apis/index.ts rename to packages/core-api/src/apis/index.ts diff --git a/packages/core/src/api/apis/types.ts b/packages/core-api/src/apis/types.ts similarity index 100% rename from packages/core/src/api/apis/types.ts rename to packages/core-api/src/apis/types.ts diff --git a/packages/core/src/api/app/App.tsx b/packages/core-api/src/app/App.tsx similarity index 80% rename from packages/core/src/api/app/App.tsx rename to packages/core-api/src/app/App.tsx index afdb6ffc58..844c38ecce 100644 --- a/packages/core/src/api/app/App.tsx +++ b/packages/core-api/src/app/App.tsx @@ -17,19 +17,13 @@ import React, { ComponentType, FC } from 'react'; import { Route, Switch, Redirect } from 'react-router-dom'; import { AppContextProvider } from './AppContext'; -import { BackstageApp, AppOptions, AppComponents } from './types'; +import { BackstageApp, AppComponents } from './types'; import { BackstagePlugin } from '../plugin'; import { FeatureFlagsRegistryItem } from './FeatureFlags'; import { featureFlagsApiRef } from '../apis/definitions'; -import ErrorPage from '../../layout/ErrorPage'; import { AppThemeProvider } from './AppThemeProvider'; -import { - IconComponent, - SystemIcons, - SystemIconKey, - defaultSystemIcons, -} from '../../icons'; +import { IconComponent, SystemIcons, SystemIconKey } from '../icons'; import { ApiHolder, ApiProvider, @@ -38,7 +32,6 @@ import { AppThemeSelector, appThemeApiRef, } from '../apis'; -import { lightTheme, darkTheme } from '@backstage/theme'; import { ApiAggregator } from '../apis/ApiAggregator'; type FullAppOptions = { @@ -49,7 +42,7 @@ type FullAppOptions = { themes: AppTheme[]; }; -class AppImpl implements BackstageApp { +export class PrivateAppImpl implements BackstageApp { private readonly apis: ApiHolder; private readonly icons: SystemIcons; private readonly plugins: BackstagePlugin[]; @@ -175,40 +168,3 @@ class AppImpl implements BackstageApp { } } } - -/** - * Creates a new Backstage App. - */ -export function createApp(options?: AppOptions) { - const DefaultNotFoundPage = () => ( - - ); - - const apis = options?.apis ?? ApiRegistry.from([]); - const icons = { ...defaultSystemIcons, ...options?.icons }; - const plugins = options?.plugins ?? []; - const components = { - NotFoundErrorPage: DefaultNotFoundPage, - ...options?.components, - }; - const themes = options?.themes ?? [ - { - id: 'light', - title: 'Light Theme', - variant: 'light', - theme: lightTheme, - }, - { - id: 'dark', - title: 'Dark Theme', - variant: 'dark', - theme: darkTheme, - }, - ]; - - const app = new AppImpl({ apis, icons, plugins, components, themes }); - - app.verify(); - - return app; -} diff --git a/packages/core/src/api/app/AppContext.tsx b/packages/core-api/src/app/AppContext.tsx similarity index 100% rename from packages/core/src/api/app/AppContext.tsx rename to packages/core-api/src/app/AppContext.tsx diff --git a/packages/core/src/api/app/AppThemeProvider.tsx b/packages/core-api/src/app/AppThemeProvider.tsx similarity index 100% rename from packages/core/src/api/app/AppThemeProvider.tsx rename to packages/core-api/src/app/AppThemeProvider.tsx diff --git a/packages/core/src/api/app/FeatureFlags.test.tsx b/packages/core-api/src/app/FeatureFlags.test.tsx similarity index 100% rename from packages/core/src/api/app/FeatureFlags.test.tsx rename to packages/core-api/src/app/FeatureFlags.test.tsx diff --git a/packages/core/src/api/app/FeatureFlags.tsx b/packages/core-api/src/app/FeatureFlags.tsx similarity index 100% rename from packages/core/src/api/app/FeatureFlags.tsx rename to packages/core-api/src/app/FeatureFlags.tsx diff --git a/packages/core/src/api/app/index.ts b/packages/core-api/src/app/index.ts similarity index 95% rename from packages/core/src/api/app/index.ts rename to packages/core-api/src/app/index.ts index c40fa9e9f8..003b71e353 100644 --- a/packages/core/src/api/app/index.ts +++ b/packages/core-api/src/app/index.ts @@ -14,7 +14,6 @@ * limitations under the License. */ -export { createApp } from './App'; export { FeatureFlags } from './FeatureFlags'; export { useApp } from './AppContext'; export * from './types'; diff --git a/packages/core/src/api/app/types.ts b/packages/core-api/src/app/types.ts similarity index 97% rename from packages/core/src/api/app/types.ts rename to packages/core-api/src/app/types.ts index 75a750ce6e..ea3a812557 100644 --- a/packages/core/src/api/app/types.ts +++ b/packages/core-api/src/app/types.ts @@ -15,7 +15,7 @@ */ import { ComponentType } from 'react'; -import { IconComponent, SystemIconKey, SystemIcons } from '../../icons'; +import { IconComponent, SystemIconKey, SystemIcons } from '../icons'; import { BackstagePlugin } from '../plugin'; import { ApiHolder } from '../apis'; import { AppTheme } from '../apis/definitions'; diff --git a/packages/core-api/src/icons/icons.tsx b/packages/core-api/src/icons/icons.tsx new file mode 100644 index 0000000000..488973b664 --- /dev/null +++ b/packages/core-api/src/icons/icons.tsx @@ -0,0 +1,39 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { SvgIconProps } from '@material-ui/core'; +import PeopleIcon from '@material-ui/icons/People'; +import PersonIcon from '@material-ui/icons/Person'; +import React, { FC } from 'react'; +import { useApp } from '../app/AppContext'; +import { IconComponent, SystemIconKey, SystemIcons } from './types'; + +export const defaultSystemIcons: SystemIcons = { + user: PersonIcon, + group: PeopleIcon, +}; + +const overridableSystemIcon = (key: SystemIconKey): IconComponent => { + const Component: FC = props => { + const app = useApp(); + const Icon = app.getSystemIcon(key); + return ; + }; + return Component; +}; + +export const UserIcon = overridableSystemIcon('user'); +export const GroupIcon = overridableSystemIcon('group'); diff --git a/packages/core-api/src/icons/index.ts b/packages/core-api/src/icons/index.ts new file mode 100644 index 0000000000..4c97d27176 --- /dev/null +++ b/packages/core-api/src/icons/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './icons'; +export * from './types'; diff --git a/packages/core-api/src/icons/types.ts b/packages/core-api/src/icons/types.ts new file mode 100644 index 0000000000..30e0ba53b2 --- /dev/null +++ b/packages/core-api/src/icons/types.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { ComponentType } from 'react'; +import { SvgIconProps } from '@material-ui/core'; + +export type IconComponent = ComponentType; +export type SystemIconKey = 'user' | 'group'; +export type SystemIcons = { [key in SystemIconKey]: IconComponent }; diff --git a/packages/core-api/src/index.ts b/packages/core-api/src/index.ts new file mode 100644 index 0000000000..f08e65809e --- /dev/null +++ b/packages/core-api/src/index.ts @@ -0,0 +1,19 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './public'; +import * as privateExports from './private'; +export default privateExports; diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts similarity index 98% rename from packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts rename to packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index d4f95f7907..9dde49aab8 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -16,7 +16,7 @@ import ProviderIcon from '@material-ui/icons/AcUnit'; import { DefaultAuthConnector } from './DefaultAuthConnector'; -import MockOAuthApi from '../../OAuthRequestApi/MockOAuthApi'; +import MockOAuthApi from '../../apis/implementations/OAuthRequestApi/MockOAuthApi'; import * as loginPopup from '../loginPopup'; const anyFetch = fetch as any; diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts similarity index 97% rename from packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts rename to packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts index ca8a9cd2ae..43bca21e90 100644 --- a/packages/core/src/api/apis/implementations/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -14,8 +14,8 @@ * limitations under the License. */ -import { AuthRequester } from '../../..'; -import { OAuthRequestApi, AuthProvider } from '../../../definitions'; +import { AuthRequester } from '../../apis'; +import { OAuthRequestApi, AuthProvider } from '../../apis/definitions'; import { showLoginPopup } from '../loginPopup'; import { AuthConnector } from './types'; diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/MockAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/MockAuthConnector.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthConnector/MockAuthConnector.test.ts rename to packages/core-api/src/lib/AuthConnector/MockAuthConnector.test.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/MockAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/MockAuthConnector.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthConnector/MockAuthConnector.ts rename to packages/core-api/src/lib/AuthConnector/MockAuthConnector.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/index.ts b/packages/core-api/src/lib/AuthConnector/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthConnector/index.ts rename to packages/core-api/src/lib/AuthConnector/index.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts b/packages/core-api/src/lib/AuthConnector/types.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthConnector/types.ts rename to packages/core-api/src/lib/AuthConnector/types.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.test.ts rename to packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/AuthSessionStore.ts rename to packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts rename to packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/RefreshingAuthSessionManager.ts rename to packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.test.ts rename to packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/StaticAuthSessionManager.ts rename to packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/common.ts b/packages/core-api/src/lib/AuthSessionManager/common.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/common.ts rename to packages/core-api/src/lib/AuthSessionManager/common.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/index.ts b/packages/core-api/src/lib/AuthSessionManager/index.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/index.ts rename to packages/core-api/src/lib/AuthSessionManager/index.ts diff --git a/packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/AuthSessionManager/types.ts rename to packages/core-api/src/lib/AuthSessionManager/types.ts diff --git a/packages/core-api/src/lib/index.ts b/packages/core-api/src/lib/index.ts new file mode 100644 index 0000000000..10f213b50f --- /dev/null +++ b/packages/core-api/src/lib/index.ts @@ -0,0 +1,20 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './subjects'; +export * from './loginPopup'; +export * from './AuthConnector'; +export * from './AuthSessionManager'; diff --git a/packages/core/src/api/apis/implementations/lib/loginPopup.test.ts b/packages/core-api/src/lib/loginPopup.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/loginPopup.test.ts rename to packages/core-api/src/lib/loginPopup.test.ts diff --git a/packages/core/src/api/apis/implementations/lib/loginPopup.ts b/packages/core-api/src/lib/loginPopup.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/loginPopup.ts rename to packages/core-api/src/lib/loginPopup.ts diff --git a/packages/core/src/api/apis/implementations/lib/subjects.test.ts b/packages/core-api/src/lib/subjects.test.ts similarity index 100% rename from packages/core/src/api/apis/implementations/lib/subjects.test.ts rename to packages/core-api/src/lib/subjects.test.ts diff --git a/packages/core/src/api/apis/implementations/lib/subjects.ts b/packages/core-api/src/lib/subjects.ts similarity index 99% rename from packages/core/src/api/apis/implementations/lib/subjects.ts rename to packages/core-api/src/lib/subjects.ts index 5b424b7e55..9ebde6ebbc 100644 --- a/packages/core/src/api/apis/implementations/lib/subjects.ts +++ b/packages/core-api/src/lib/subjects.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { Observable } from '../../../types'; +import { Observable } from '../types'; import ObservableImpl from 'zen-observable'; // TODO(Rugvip): These are stopgap and probably incomplete implementations of subjects. diff --git a/packages/core/src/api/plugin/Plugin.tsx b/packages/core-api/src/plugin/Plugin.tsx similarity index 100% rename from packages/core/src/api/plugin/Plugin.tsx rename to packages/core-api/src/plugin/Plugin.tsx diff --git a/packages/core/src/api/plugin/index.ts b/packages/core-api/src/plugin/index.ts similarity index 100% rename from packages/core/src/api/plugin/index.ts rename to packages/core-api/src/plugin/index.ts diff --git a/packages/core/src/api/plugin/types.ts b/packages/core-api/src/plugin/types.ts similarity index 100% rename from packages/core/src/api/plugin/types.ts rename to packages/core-api/src/plugin/types.ts diff --git a/packages/core/src/api/apis/implementations/lib/index.ts b/packages/core-api/src/private.ts similarity index 93% rename from packages/core/src/api/apis/implementations/lib/index.ts rename to packages/core-api/src/private.ts index aa2202858c..65462d8d51 100644 --- a/packages/core/src/api/apis/implementations/lib/index.ts +++ b/packages/core-api/src/private.ts @@ -14,4 +14,4 @@ * limitations under the License. */ -export * from './subjects'; +export { PrivateAppImpl } from './app/App'; diff --git a/packages/core-api/src/public.ts b/packages/core-api/src/public.ts new file mode 100644 index 0000000000..0a30936c52 --- /dev/null +++ b/packages/core-api/src/public.ts @@ -0,0 +1,22 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export * from './apis'; +export * from './app'; +export * from './icons'; +export * from './plugin'; +export * from './routing'; +export * from './types'; diff --git a/packages/core/src/api/routing/RouteRef.ts b/packages/core-api/src/routing/RouteRef.ts similarity index 100% rename from packages/core/src/api/routing/RouteRef.ts rename to packages/core-api/src/routing/RouteRef.ts diff --git a/packages/core/src/api/routing/index.ts b/packages/core-api/src/routing/index.ts similarity index 100% rename from packages/core/src/api/routing/index.ts rename to packages/core-api/src/routing/index.ts diff --git a/packages/core/src/api/routing/types.ts b/packages/core-api/src/routing/types.ts similarity index 95% rename from packages/core/src/api/routing/types.ts rename to packages/core-api/src/routing/types.ts index 0b6d1c711f..491665bbcf 100644 --- a/packages/core/src/api/routing/types.ts +++ b/packages/core-api/src/routing/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { IconComponent } from '../../icons'; +import { IconComponent } from '../icons'; export type RouteRef = { path: string; diff --git a/packages/core-api/src/setupTests.ts b/packages/core-api/src/setupTests.ts new file mode 100644 index 0000000000..e34bc46f4b --- /dev/null +++ b/packages/core-api/src/setupTests.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import '@testing-library/jest-dom'; +require('jest-fetch-mock').enableMocks(); diff --git a/packages/core/src/api/types.ts b/packages/core-api/src/types.ts similarity index 100% rename from packages/core/src/api/types.ts rename to packages/core-api/src/types.ts diff --git a/packages/core/package.json b/packages/core/package.json index 00e80e8e0b..a14c1f9465 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,6 +28,7 @@ "clean": "backstage-cli clean" }, "dependencies": { + "@backstage/core-api": "^0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", diff --git a/packages/core/src/api/createApp.tsx b/packages/core/src/api/createApp.tsx new file mode 100644 index 0000000000..d0deed667a --- /dev/null +++ b/packages/core/src/api/createApp.tsx @@ -0,0 +1,69 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import privateExports, { + AppOptions, + ApiRegistry, + defaultSystemIcons, +} from '@backstage/core-api'; + +import ErrorPage from '../layout/ErrorPage'; +import { lightTheme, darkTheme } from '@backstage/theme'; + +const { PrivateAppImpl } = privateExports; + +// createApp is defined in core, and not core-api, since we need access +// to the components inside core to provide defaults. +// The actual implementation of the app class still lives in core-api, +// as it needs to be used by dev- and test-utils. + +/** + * Creates a new Backstage App. + */ +export function createApp(options?: AppOptions) { + const DefaultNotFoundPage = () => ( + + ); + + const apis = options?.apis ?? ApiRegistry.from([]); + const icons = { ...defaultSystemIcons, ...options?.icons }; + const plugins = options?.plugins ?? []; + const components = { + NotFoundErrorPage: DefaultNotFoundPage, + ...options?.components, + }; + const themes = options?.themes ?? [ + { + id: 'light', + title: 'Light Theme', + variant: 'light', + theme: lightTheme, + }, + { + id: 'dark', + title: 'Dark Theme', + variant: 'dark', + theme: darkTheme, + }, + ]; + + const app = new PrivateAppImpl({ apis, icons, plugins, components, themes }); + + app.verify(); + + return app; +} diff --git a/packages/core/src/api/index.ts b/packages/core/src/api/index.ts index cde010293d..b8136305b0 100644 --- a/packages/core/src/api/index.ts +++ b/packages/core/src/api/index.ts @@ -14,8 +14,4 @@ * limitations under the License. */ -export * from './apis'; -export * from './app'; -export * from './routing'; -export * from './plugin'; -export * from './types'; +export { createApp } from './createApp'; diff --git a/packages/core/src/components/AlertDisplay/AlertDisplay.tsx b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx index 85b41f2e0a..30940f68ac 100644 --- a/packages/core/src/components/AlertDisplay/AlertDisplay.tsx +++ b/packages/core/src/components/AlertDisplay/AlertDisplay.tsx @@ -18,7 +18,7 @@ import React, { FC, useEffect, useState } from 'react'; import { Snackbar, IconButton } from '@material-ui/core'; import CloseIcon from '@material-ui/icons/Close'; import { Alert } from '@material-ui/lab'; -import { AlertMessage, useApi, alertApiRef } from '../../api'; +import { AlertMessage, useApi, alertApiRef } from '@backstage/core-api'; type Props = {}; diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx index 80c11f6b4a..3dc5854ab3 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.stories.tsx @@ -16,7 +16,12 @@ import React from 'react'; import CopyTextButton from '.'; -import { ApiProvider, errorApiRef, ApiRegistry, ErrorApi } from '../../api'; +import { + ApiProvider, + errorApiRef, + ApiRegistry, + ErrorApi, +} from '@backstage/core-api'; export default { title: 'CopyTextButton', diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx index 7bc59d1f48..e0f7271014 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.test.tsx @@ -18,7 +18,12 @@ import React from 'react'; import { render } from '@testing-library/react'; import { wrapInThemedTestApp } from '@backstage/test-utils'; import CopyTextButton from './CopyTextButton'; -import { ApiRegistry, errorApiRef, ApiProvider, ErrorApi } from '../../api'; +import { + ApiRegistry, + errorApiRef, + ApiProvider, + ErrorApi, +} from '@backstage/core-api'; jest.mock('popper.js', () => { const PopperJS = jest.requireActual('popper.js'); diff --git a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx index 5af62f4380..1c5028fa9c 100644 --- a/packages/core/src/components/CopyTextButton/CopyTextButton.tsx +++ b/packages/core/src/components/CopyTextButton/CopyTextButton.tsx @@ -19,9 +19,9 @@ import { IconButton, makeStyles, Tooltip } from '@material-ui/core'; import PropTypes from 'prop-types'; import CopyIcon from '@material-ui/icons/FileCopy'; import { BackstageTheme } from '@backstage/theme'; -import { errorApiRef, useApi } from '../../api'; +import { errorApiRef, useApi } from '@backstage/core-api'; -const useStyles = makeStyles((theme) => ({ +const useStyles = makeStyles(theme => ({ button: { '&:hover': { backgroundColor: theme.palette.highlight, @@ -56,7 +56,7 @@ const defaultProps = { tooltipText: 'Text copied to clipboard', }; -const CopyTextButton: FC = (props) => { +const CopyTextButton: FC = props => { const { text, tooltipDelay, tooltipText } = { ...defaultProps, ...props, @@ -66,7 +66,7 @@ const CopyTextButton: FC = (props) => { const inputRef = useRef(null); const [open, setOpen] = useState(false); - const handleCopyClick: MouseEventHandler = (e) => { + const handleCopyClick: MouseEventHandler = e => { e.stopPropagation(); setOpen(true); diff --git a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx index d97956c1f0..cc7d99660b 100644 --- a/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx +++ b/packages/core/src/components/OAuthRequestDialog/LoginRequestListItem.tsx @@ -23,7 +23,7 @@ import { Theme, } from '@material-ui/core'; import React, { FC, useState } from 'react'; -import { PendingAuthRequest } from '../../api'; +import { PendingAuthRequest } from '@backstage/core-api'; const useItemStyles = makeStyles(theme => ({ root: { diff --git a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx index bde12ccb04..07078c3fa2 100644 --- a/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx +++ b/packages/core/src/components/OAuthRequestDialog/OAuthRequestDialog.tsx @@ -27,7 +27,7 @@ import { import React, { FC, useMemo, useState } from 'react'; import { useObservable } from 'react-use'; import LoginRequestListItem from './LoginRequestListItem'; -import { useApi, oauthRequestApiRef } from '../../api'; +import { useApi, oauthRequestApiRef } from '@backstage/core-api'; const useStyles = makeStyles(theme => ({ dialog: { diff --git a/packages/core/src/icons/icons.tsx b/packages/core/src/icons/icons.tsx index ca3ef32859..e8834a6162 100644 --- a/packages/core/src/icons/icons.tsx +++ b/packages/core/src/icons/icons.tsx @@ -18,7 +18,7 @@ import { SvgIconProps } from '@material-ui/core'; import PeopleIcon from '@material-ui/icons/People'; import PersonIcon from '@material-ui/icons/Person'; import React, { FC } from 'react'; -import { useApp } from '../api/app/AppContext'; +import { useApp } from '@backstage/core-api'; import { IconComponent, SystemIconKey, SystemIcons } from './types'; export const defaultSystemIcons: SystemIcons = { @@ -27,7 +27,7 @@ export const defaultSystemIcons: SystemIcons = { }; const overridableSystemIcon = (key: SystemIconKey): IconComponent => { - const Component: FC = (props) => { + const Component: FC = props => { const app = useApp(); const Icon = app.getSystemIcon(key); return ; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index fc6e2988bc..789c57430e 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -14,6 +14,8 @@ * limitations under the License. */ +export * from '@backstage/core-api'; + export * from './api'; export { default as Page } from './layout/Page'; export { gradients, pageTheme } from './layout/Page'; diff --git a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx b/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx index 7f4c8890f9..32bc9d9632 100644 --- a/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx +++ b/packages/core/src/layout/Sidebar/SidebarThemeToggle.tsx @@ -19,7 +19,7 @@ import { useObservable } from 'react-use'; import LightIcon from '@material-ui/icons/WbSunny'; import DarkIcon from '@material-ui/icons/Brightness2'; import AutoIcon from '@material-ui/icons/BrightnessAuto'; -import { appThemeApiRef, useApi } from '../../api'; +import { appThemeApiRef, useApi } from '@backstage/core-api'; import { SidebarItem } from './Items'; export const SidebarThemeToggle: FC<{}> = () => { From 053fd7d384f7c31988ff8179974c9ae46c362fc1 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 May 2020 02:07:41 +0200 Subject: [PATCH 46/58] packages/core-api: prune imports --- packages/core-api/package.json | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/core-api/package.json b/packages/core-api/package.json index 88d593dcf8..d89d39d532 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -31,34 +31,17 @@ "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", - "@material-ui/lab": "4.0.0-alpha.45", - "@types/classnames": "^2.2.9", - "@types/google-protobuf": "^3.7.2", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/react-helmet": "^5.0.15", - "@types/react-sparklines": "^1.7.0", "@types/zen-observable": "^0.8.0", - "classnames": "^2.2.6", - "clsx": "^1.1.0", - "lodash": "^4.17.15", - "material-table": "^1.58.0", "prop-types": "^15.7.2", - "rc-progress": "^3.0.0", "react": "^16.12.0", - "react-addons-text-content": "0.0.4", - "react-dom": "^16.12.0", - "react-helmet": "5.2.1", - "react-router": "^5.2.0", "react-router-dom": "^5.2.0", - "react-sparklines": "^1.7.0", - "react-syntax-highlighter": "^12.2.1", "react-use": "^14.2.0", "zen-observable": "^0.8.15" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", - "@backstage/test-utils": "^0.1.1-alpha.6", "@backstage/test-utils-core": "^0.1.1-alpha.6", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", From 89d230d2e02861e9f0c847f1328c76addafdc36a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 May 2020 09:48:43 +0200 Subject: [PATCH 47/58] packages/core: prune imports --- packages/core/package.json | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index a14c1f9465..1625284702 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -47,20 +47,17 @@ "prop-types": "^15.7.2", "rc-progress": "^3.0.0", "react": "^16.12.0", - "react-addons-text-content": "0.0.4", "react-dom": "^16.12.0", "react-helmet": "6.0.0", "react-router": "^5.2.0", "react-router-dom": "^5.2.0", "react-sparklines": "^1.7.0", "react-syntax-highlighter": "^12.2.1", - "react-use": "^14.2.0", - "zen-observable": "^0.8.15" + "react-use": "^14.2.0" }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.6", "@backstage/test-utils": "^0.1.1-alpha.6", - "@backstage/test-utils-core": "^0.1.1-alpha.6", "@testing-library/jest-dom": "^5.7.0", "@testing-library/react": "^9.3.2", "@testing-library/user-event": "^10.2.4", From 9b13fa6091812067bc93590df55dabbadacff5b8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 May 2020 09:51:45 +0200 Subject: [PATCH 48/58] packages/core-api: update package json and pin core-api version --- packages/core-api/package.json | 4 ++-- packages/core/package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/core-api/package.json b/packages/core-api/package.json index d89d39d532..73e7a18d33 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -1,6 +1,6 @@ { "name": "@backstage/core-api", - "description": "Core API used by Backstage plugins and apps", + "description": "Internal Core API used by Backstage plugins and apps", "version": "0.1.1-alpha.6", "private": false, "publishConfig": { @@ -10,7 +10,7 @@ "repository": { "type": "git", "url": "https://github.com/spotify/backstage", - "directory": "packages/core" + "directory": "packages/core-api" }, "keywords": [ "backstage" diff --git a/packages/core/package.json b/packages/core/package.json index 1625284702..a89ffc0b92 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -28,7 +28,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core-api": "^0.1.1-alpha.6", + "@backstage/core-api": "0.1.1-alpha.6", "@backstage/theme": "^0.1.1-alpha.6", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", From 11b6ad829e1ad7135fbc06d4fe1fe3876c9fed96 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 May 2020 10:00:32 +0200 Subject: [PATCH 49/58] packages/cli: add core-api as local package for e2e test --- packages/cli/src/lib/tasks.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/cli/src/lib/tasks.ts b/packages/cli/src/lib/tasks.ts index 6b752a36cc..2d592bc549 100644 --- a/packages/cli/src/lib/tasks.ts +++ b/packages/cli/src/lib/tasks.ts @@ -112,6 +112,7 @@ export async function templatingTask( const PATCH_PACKAGES = [ 'cli', 'core', + 'core-api', 'dev-utils', 'test-utils', 'test-utils-core', From 76dc2cc4a086dc11df4e44539cbd24438ed3820a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 May 2020 12:45:43 +0200 Subject: [PATCH 50/58] packages/core-api: properly forward auth options to respect the instantPopup option --- .../core-api/src/apis/definitions/auth.ts | 5 +++- .../implementations/auth/google/GoogleAuth.ts | 19 ++++++++----- .../DefaultAuthConnector.test.ts | 28 +++++++++++++++++-- .../lib/AuthConnector/DefaultAuthConnector.ts | 16 +++++++---- .../core-api/src/lib/AuthConnector/types.ts | 7 ++++- .../AuthSessionManager/AuthSessionStore.ts | 14 ++-------- .../RefreshingAuthSessionManager.test.ts | 17 +++++++++++ .../RefreshingAuthSessionManager.ts | 21 ++++---------- .../StaticAuthSessionManager.test.ts | 2 +- .../StaticAuthSessionManager.ts | 22 ++++----------- .../src/lib/AuthSessionManager/types.ts | 12 ++++---- 11 files changed, 97 insertions(+), 66 deletions(-) diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index 15c9e5d632..c066c83b73 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -85,7 +85,10 @@ export type OAuthApi = { * will be prompted to log in. The returned promise will not resolve until the user has * successfully logged in. The returned promise can be rejected, but only if the user rejects the login request. */ - getAccessToken(scope?: OAuthScope): Promise; + getAccessToken( + scope?: OAuthScope, + options?: AccessTokenOptions, + ): Promise; /** * Log out the user's session. This will reload the page. diff --git a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts index e52e8f0397..582afbcfea 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -21,6 +21,7 @@ import { OAuthApi, OpenIdConnectApi, IdTokenOptions, + AccessTokenOptions, } from '../../../definitions/auth'; import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; @@ -95,19 +96,23 @@ class GoogleAuth implements OAuthApi, OpenIdConnectApi { constructor(private readonly sessionManager: SessionManager) {} - async getAccessToken(scope?: string | string[]) { + async getAccessToken( + scope?: string | string[], + options?: AccessTokenOptions, + ) { const normalizedScopes = GoogleAuth.normalizeScopes(scope); const session = await this.sessionManager.getSession({ - optional: false, + ...options, scopes: normalizedScopes, }); - return session.accessToken; + if (session) { + return session.accessToken; + } + return ''; } - async getIdToken({ optional }: IdTokenOptions = {}) { - const session = await this.sessionManager.getSession({ - optional: optional || false, - }); + async getIdToken(options: IdTokenOptions = {}) { + const session = await this.sessionManager.getSession(options); if (session) { return session.idToken; } diff --git a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts index 9dde49aab8..3d8f7abeb9 100644 --- a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts +++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.test.ts @@ -86,7 +86,7 @@ describe('DefaultAuthConnector', () => { ...defaultOptions, oauthRequestApi: mockOauth, }); - const promise = helper.createSession(new Set(['a', 'b'])); + const promise = helper.createSession({ scopes: new Set(['a', 'b']) }); await mockOauth.rejectAll(); await expect(promise).rejects.toMatchObject({ name: 'RejectedError' }); }); @@ -106,7 +106,9 @@ describe('DefaultAuthConnector', () => { oauthRequestApi: mockOauth, }); - const sessionPromise = helper.createSession(new Set(['a', 'b'])); + const sessionPromise = helper.createSession({ + scopes: new Set(['a', 'b']), + }); await mockOauth.triggerAll(); @@ -123,6 +125,26 @@ describe('DefaultAuthConnector', () => { }); }); + it('should instantly show popup if option is set', async () => { + const popupSpy = jest + .spyOn(loginPopup, 'showLoginPopup') + .mockResolvedValue('my-session'); + const helper = new DefaultAuthConnector({ + ...defaultOptions, + oauthRequestApi: new MockOAuthApi(), + sessionTransform: str => str, + }); + + const sessionPromise = helper.createSession({ + scopes: new Set(), + instantPopup: true, + }); + + expect(popupSpy).toBeCalledTimes(1); + + await expect(sessionPromise).resolves.toBe('my-session'); + }); + it('should use join func to join scopes', async () => { const mockOauth = new MockOAuthApi(); const popupSpy = jest @@ -134,7 +156,7 @@ describe('DefaultAuthConnector', () => { oauthRequestApi: mockOauth, }); - helper.createSession(new Set(['a', 'b'])); + helper.createSession({ scopes: new Set(['a', 'b']) }); await mockOauth.triggerAll(); diff --git a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts index 43bca21e90..21d2530c5a 100644 --- a/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts +++ b/packages/core-api/src/lib/AuthConnector/DefaultAuthConnector.ts @@ -17,7 +17,7 @@ import { AuthRequester } from '../../apis'; import { OAuthRequestApi, AuthProvider } from '../../apis/definitions'; import { showLoginPopup } from '../loginPopup'; -import { AuthConnector } from './types'; +import { AuthConnector, CreateSessionOptions } from './types'; const DEFAULT_BASE_PATH = '/api/auth/'; @@ -68,6 +68,7 @@ export class DefaultAuthConnector private readonly basePath: string; private readonly environment: string; private readonly provider: AuthProvider & { id: string }; + private readonly joinScopesFunc: (scopes: Set) => string; private readonly authRequester: AuthRequester; private readonly sessionTransform: (response: any) => Promise; @@ -84,18 +85,22 @@ export class DefaultAuthConnector this.authRequester = oauthRequestApi.createAuthRequester({ provider, - onAuthRequest: scopes => this.showPopup(joinScopes(scopes)), + onAuthRequest: scopes => this.showPopup(scopes), }); this.apiOrigin = apiOrigin; this.basePath = basePath; this.environment = environment; this.provider = provider; + this.joinScopesFunc = joinScopes; this.sessionTransform = sessionTransform; } - async createSession(scopes: Set): Promise { - return this.authRequester(scopes); + async createSession(options: CreateSessionOptions): Promise { + if (options.instantPopup) { + return this.showPopup(options.scopes); + } + return this.authRequester(options.scopes); } async refreshSession(): Promise { @@ -142,7 +147,8 @@ export class DefaultAuthConnector } } - private async showPopup(scope: string): Promise { + private async showPopup(scopes: Set): Promise { + const scope = this.joinScopesFunc(scopes); const popupUrl = this.buildUrl('/start', { scope }); const payload = await showLoginPopup({ diff --git a/packages/core-api/src/lib/AuthConnector/types.ts b/packages/core-api/src/lib/AuthConnector/types.ts index 146c31cbe1..46175a265f 100644 --- a/packages/core-api/src/lib/AuthConnector/types.ts +++ b/packages/core-api/src/lib/AuthConnector/types.ts @@ -14,12 +14,17 @@ * limitations under the License. */ +export type CreateSessionOptions = { + scopes: Set; + instantPopup?: boolean; +}; + /** * An AuthConnector is responsible for realizing auth session actions * by for example communicating with a backend or interacting with the user. */ export type AuthConnector = { - createSession(scopes: Set): Promise; + createSession(options: CreateSessionOptions): Promise; refreshSession(): Promise; removeSession(): Promise; }; diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts index 14796c6e11..8d318f5bff 100644 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -18,6 +18,7 @@ import { SessionManager, SessionScopesFunc, SessionShouldRefreshFunc, + GetSessionOptions, } from './types'; import { SessionScopeHelper } from './common'; @@ -59,18 +60,7 @@ export class AuthSessionStore implements SessionManager { }); } - async getSession(options: { - optional: false; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise { + async getSession(options: GetSessionOptions): Promise { const { scopes } = options; const session = this.loadSession(); diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts index 8684c932b2..ee7cbab2ae 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts @@ -113,6 +113,23 @@ describe('RefreshingAuthSessionManager', () => { expect(refreshSession).toBeCalledTimes(1); }); + it('should forward option to instantly show auth popup', async () => { + const createSession = jest.fn(); + const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); + const manager = new RefreshingAuthSessionManager({ + connector: { createSession, refreshSession }, + ...defaultOptions, + } as any); + + expect(await manager.getSession({ instantPopup: true })).toBe(undefined); + expect(createSession).toBeCalledTimes(1); + expect(createSession).toHaveBeenCalledWith({ + scopes: new Set(), + instantPopup: true, + }); + expect(refreshSession).toBeCalledTimes(1); + }); + it('should remove session and reload', async () => { // This is a workaround that is used by Facebook and the Jest core team // It is a limitation with the newest versions of JSDOM, and newer browser standards diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index cbd6d5ca46..64df3deca4 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -18,6 +18,7 @@ import { SessionManager, SessionScopesFunc, SessionShouldRefreshFunc, + GetSessionOptions, } from './types'; import { AuthConnector } from '../AuthConnector'; import { SessionScopeHelper, hasScopes } from './common'; @@ -60,18 +61,7 @@ export class RefreshingAuthSessionManager implements SessionManager { this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes }); } - async getSession(options: { - optional: false; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise { + async getSession(options: GetSessionOptions): Promise { if ( this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes) ) { @@ -115,9 +105,10 @@ export class RefreshingAuthSessionManager implements SessionManager { } // We can call authRequester multiple times, the returned session will contain all requested scopes. - this.currentSession = await this.connector.createSession( - this.helper.getExtendedScope(this.currentSession, options.scopes), - ); + this.currentSession = await this.connector.createSession({ + ...options, + scopes: this.helper.getExtendedScope(this.currentSession, options.scopes), + }); return this.currentSession; } diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts index dc13aec0c2..7d47fa91df 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.test.ts @@ -62,7 +62,7 @@ describe('StaticAuthSessionManager', () => { it('should only request auth once for same scopes', async () => { const createSession = jest .fn() - .mockImplementation(scopes => [...scopes].join(' ')); + .mockImplementation(({ scopes }) => [...scopes].join(' ')); const manager = new StaticAuthSessionManager({ connector: { createSession, ...baseConnector }, ...defaultOptions, diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts index 8d057ecc58..6e6db47a99 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { SessionManager } from './types'; +import { SessionManager, GetSessionOptions } from './types'; import { AuthConnector } from '../AuthConnector'; import { SessionScopeHelper } from './common'; @@ -43,18 +43,7 @@ export class StaticAuthSessionManager implements SessionManager { this.helper = new SessionScopeHelper({ sessionScopes, defaultScopes }); } - async getSession(options: { - optional: false; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise; - async getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise { + async getSession(options: GetSessionOptions): Promise { if ( this.helper.sessionExistsAndHasScope(this.currentSession, options.scopes) ) { @@ -67,9 +56,10 @@ export class StaticAuthSessionManager implements SessionManager { } // We can call authRequester multiple times, the returned session will contain all requested scopes. - this.currentSession = await this.connector.createSession( - this.helper.getExtendedScope(this.currentSession, options.scopes), - ); + this.currentSession = await this.connector.createSession({ + ...options, + scopes: this.helper.getExtendedScope(this.currentSession, options.scopes), + }); return this.currentSession; } diff --git a/packages/core-api/src/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts index 5ebc8643e1..d28a751fbf 100644 --- a/packages/core-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-api/src/lib/AuthSessionManager/types.ts @@ -14,17 +14,19 @@ * limitations under the License. */ +export type GetSessionOptions = { + optional?: boolean; + instantPopup?: boolean; + scopes?: Set; +}; + /** * A sessions manager keeps track of the current session and makes sure that * multiple simultaneous requests for sessions with different scope are handled * in a correct way. */ export type SessionManager = { - getSession(options: { optional: false; scopes?: Set }): Promise; - getSession(options: { - optional?: boolean; - scopes?: Set; - }): Promise; + getSession(options: GetSessionOptions): Promise; removeSession(): Promise; }; From 01025bbd01aa2da283b67dcd9be4afa03807d231 Mon Sep 17 00:00:00 2001 From: Bilawal Hameed Date: Wed, 27 May 2020 12:50:09 +0200 Subject: [PATCH 51/58] plugin(tech-radar): set private to false and publishConfig access to public --- plugins/tech-radar/package.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 93285f5214..3a9c75a3f7 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -5,7 +5,10 @@ "main:src": "src/index.ts", "types": "src/index.ts", "license": "Apache-2.0", - "private": true, + "private": false, + "publishConfig": { + "access": "public" + }, "scripts": { "build": "backstage-cli plugin:build", "lint": "backstage-cli lint", From 582f63677678205956dbb6b69f563dc2934cfd65 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Wed, 27 May 2020 18:20:51 +0200 Subject: [PATCH 52/58] docs: add adr004, module export structure --- .../adr004-module-export-structure.md | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 docs/architecture-decisions/adr004-module-export-structure.md diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md new file mode 100644 index 0000000000..56ea7568c6 --- /dev/null +++ b/docs/architecture-decisions/adr004-module-export-structure.md @@ -0,0 +1,59 @@ +# ADR004: Module Export Structure + +| Created | Status | +| ---------- | ------ | +| 2020-05-27 | Open | + +## Context + +With a growing number of exports of packages like `@backstage/core`, it is becoming more and more difficult to answer questions such as + +> Is the export in this module also exported by the package? + +or + +> What is exported from this directory? + +We currently do not use any pattern for how to structure exports. There is a mix of package-level re-exports deep into the directory tree, shallow re-exports for each directory, exports using `*` and explicit lists of each symbol, etc. +The mix and lack of predictability makes it difficult to reason about the boundaries of a module, and for example knowing whether is is safe to export a symbol in a given file. + +## Decision + +We will make each exported symbol traceable through index files all the way down to the root of the package, `src/index.ts`. Each index file will only re-export from its own immediate directory children, and only index files will have re-exports. This gives a file tree similar to this: + +```text +index.ts +components/index.ts + /ComponentX/index.ts + /ComponentX.tsx + /SubComponentY.tsx +lib/index.ts + /UtilityX/index.ts + /UtilityX.ts + /helper.ts +``` + +To check whether for example `SubComponentY` is exported from the package, it should be possible to traverse the index files towards the root, starting at the adjacent one. If there is any index file that doesn't export the previous one, the symbol is not publicly exported. For example, if `components/ComponentX/index.ts` exports `SubComponentY`, but `components/index.ts` does not re-export `./ComponentX`, one should be certain that `SubComponentY` is not exported outside the package. This rule would be broken if for example the root `index.ts` re-exports `./components/ComponentX` + +In addition, index files that are re-exporting other index files should always use wildcard form, that is: + +```ts +// in components/index.ts +export * from './ComponentX'; +``` + +Index files that are re-exporting symbols from non-index files should always enumerate all exports, that is: + +```ts +// in components/ComponentX/index.ts +export { ComponentX } from './ComponentX'; +export type { ComponentXProps } from './ComponentX'; +``` + +Internal cross-directory imports are allowed, but discouraged, and no module should re-export a symbol across directories. + +## Consequences + +We will actively work to rework the export structure in our codebase, prioritizing the library packages such as `@backstage/core` and `@backstage/backend-common`. + +If possible, we will add tools, such as lint rules, to help enforce the export structure. From d37d7b944b38f8f7e6b3c38b56c1dfe1e04855f2 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 00:42:23 +0200 Subject: [PATCH 53/58] docs/adr004: add clarification about internal imports --- .../adr004-module-export-structure.md | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/docs/architecture-decisions/adr004-module-export-structure.md b/docs/architecture-decisions/adr004-module-export-structure.md index 56ea7568c6..56c675f069 100644 --- a/docs/architecture-decisions/adr004-module-export-structure.md +++ b/docs/architecture-decisions/adr004-module-export-structure.md @@ -50,7 +50,19 @@ export { ComponentX } from './ComponentX'; export type { ComponentXProps } from './ComponentX'; ``` -Internal cross-directory imports are allowed, but discouraged, and no module should re-export a symbol across directories. +Internal cross-directory imports are allowed from non-index modules to index modules, for example: + +```ts +// in components/ComponentX/ComponentX.tsx +import { UtilityX } from '../../lib/UtilityX'; +``` + +Imports that bypass an index file are discouraged, but may sometimes be necessary, for example: + +```ts +// in components/ComponentX/ComponentX.tsx +import { helperFunc } from '../../lib/UtilityX/helper'; +``` ## Consequences From eeb69c56177a63e40cb72a29d925c36d4842e699 Mon Sep 17 00:00:00 2001 From: Govind Goel <52847415+govindgoel@users.noreply.github.com> Date: Thu, 28 May 2020 12:17:19 +0530 Subject: [PATCH 54/58] Set the correct page title in /create (#1037) This commit will fix the page title error in /create --- plugins/scaffolder/src/components/ScaffolderPage/index.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx index 922416972a..e6fed50cef 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/index.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/index.tsx @@ -40,6 +40,7 @@ const ScaffolderPage: React.FC<{}> = () => { return (
    Create a new component {' '} From 9daea8f16319aa0cc269aa897081e2cc6669c7b5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Thu, 28 May 2020 11:13:50 +0200 Subject: [PATCH 55/58] packages/cli: more strict rollup version --- packages/cli/package.json | 2 +- yarn.lock | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 299fc5e8d3..a15765c731 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -62,7 +62,7 @@ "react-hot-loader": "^4.12.21", "recursive-readdir": "^2.2.2", "replace-in-file": "^6.0.0", - "rollup": "^2.3.2", + "rollup": "2.10.x", "rollup-plugin-dts": "^1.4.6", "rollup-plugin-esbuild": "^1.4.1", "rollup-plugin-image-files": "^1.4.2", diff --git a/yarn.lock b/yarn.lock index 55255c89d9..65f328a4bc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -18359,6 +18359,13 @@ rollup@0.64.1: "@types/estree" "0.0.39" "@types/node" "*" +rollup@2.10.x: + version "2.10.9" + resolved "https://registry.npmjs.org/rollup/-/rollup-2.10.9.tgz#17dcc6753c619efcc1be2cf61d73a87827eebdf9" + integrity sha512-dY/EbjiWC17ZCUSyk14hkxATAMAShkMsD43XmZGWjLrgFj15M3Dw2kEkA9ns64BiLFm9PKN6vTQw8neHwK74eg== + optionalDependencies: + fsevents "~2.1.2" + rollup@^0.63.4: version "0.63.5" resolved "https://registry.npmjs.org/rollup/-/rollup-0.63.5.tgz#5543eecac9a1b83b7e1be598b5be84c9c0a089db" @@ -18367,13 +18374,6 @@ rollup@^0.63.4: "@types/estree" "0.0.39" "@types/node" "*" -rollup@^2.3.2: - version "2.10.9" - resolved "https://registry.npmjs.org/rollup/-/rollup-2.10.9.tgz#17dcc6753c619efcc1be2cf61d73a87827eebdf9" - integrity sha512-dY/EbjiWC17ZCUSyk14hkxATAMAShkMsD43XmZGWjLrgFj15M3Dw2kEkA9ns64BiLFm9PKN6vTQw8neHwK74eg== - optionalDependencies: - fsevents "~2.1.2" - rsvp@^4.8.4: version "4.8.5" resolved "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz#c8f155311d167f68f21e168df71ec5b083113734" From 3d4334fdb911a95589a9ffcf2550a501530a4b2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20=C3=85lund?= Date: Thu, 28 May 2020 12:46:23 +0200 Subject: [PATCH 56/58] New home: service catalog (#1018) * New home: service catalog * Add banner * Remove extra <> * Re-enable test * Review comments * Update DismissableBanner.stories.tsx --- packages/app/src/components/Root/Root.tsx | 3 - .../DismissableBanner.stories.tsx | 66 +++++++++++++ .../DismissableBanner.test.js | 46 +++++++++ .../DismissableBanner/DismissableBanner.tsx | 96 +++++++++++++++++++ .../src/components/DismissableBanner/index.ts | 17 ++++ packages/core/src/index.ts | 1 + .../components/CatalogPage/CatalogPage.tsx | 19 +++- plugins/catalog/src/plugin.ts | 2 +- plugins/welcome/src/plugin.ts | 2 +- 9 files changed, 246 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx create mode 100644 packages/core/src/components/DismissableBanner/DismissableBanner.test.js create mode 100644 packages/core/src/components/DismissableBanner/DismissableBanner.tsx create mode 100644 packages/core/src/components/DismissableBanner/index.ts diff --git a/packages/app/src/components/Root/Root.tsx b/packages/app/src/components/Root/Root.tsx index 3c5ec4ae3a..20eabbcac5 100644 --- a/packages/app/src/components/Root/Root.tsx +++ b/packages/app/src/components/Root/Root.tsx @@ -20,7 +20,6 @@ import { Link, makeStyles } from '@material-ui/core'; import HomeIcon from '@material-ui/icons/Home'; import ExploreIcon from '@material-ui/icons/Explore'; import CreateComponentIcon from '@material-ui/icons/AddCircleOutline'; -import AccountTreeIcon from '@material-ui/icons/AccountTree'; import LogoFull from './LogoFull'; import LogoIcon from './LogoIcon'; import { @@ -82,8 +81,6 @@ const Root: FC<{}> = ({ children }) => ( {/* End global nav */} - - diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx new file mode 100644 index 0000000000..e19495a099 --- /dev/null +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.stories.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import DismissableBanner from './DismissableBanner'; +import { Link, Typography } from '@material-ui/core'; + +export default { + title: 'DismissableBanner', + component: DismissableBanner, +}; + +const containerStyle = { width: '70%' }; + +export const Default = () => ( +
    + +
    +); + +export const Error = () => ( +
    + +
    +); + +export const EmojisIncluded = () => ( +
    + +
    +); + +export const WithLink = () => ( +
    + + This is a dismissable banner with a link:{' '} + + example.com + + + } + variant="info" + /> +
    +); diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.test.js b/packages/core/src/components/DismissableBanner/DismissableBanner.test.js new file mode 100644 index 0000000000..485b6226d2 --- /dev/null +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.test.js @@ -0,0 +1,46 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +// import { fireEvent, waitForElementToBeRemoved } from '@testing-library/react'; +import { renderWithEffects, wrapInThemedTestApp } from '@backstage/test-utils'; +// import { createSetting } from 'shared/apis/settings'; +import DismissableBanner from './DismissableBanner'; + +describe('', () => { + it('renders the message and the popover', async () => { + /* + const mockSetting = createSetting({ + id: 'mockSetting', + defaultValue: true, + }); + */ + + const rendered = await renderWithEffects( + wrapInThemedTestApp( + , + ), + ); + rendered.getByText('test message'); + + // fireEvent.click(rendered.getByTitle('Permanently dismiss this message')); + // await waitForElementToBeRemoved(rendered.queryByText('test message')); + }); +}); diff --git a/packages/core/src/components/DismissableBanner/DismissableBanner.tsx b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx new file mode 100644 index 0000000000..7190ee8725 --- /dev/null +++ b/packages/core/src/components/DismissableBanner/DismissableBanner.tsx @@ -0,0 +1,96 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { FC, ReactNode, useState } from 'react'; +import classNames from 'classnames'; +import { makeStyles, Theme } from '@material-ui/core'; +import Snackbar from '@material-ui/core/Snackbar'; +import SnackbarContent from '@material-ui/core/SnackbarContent'; +import IconButton from '@material-ui/core/IconButton'; +import Close from '@material-ui/icons/Close'; +// import { useSetting, Setting } from 'shared/apis/settings'; + +const useStyles = makeStyles((theme: Theme) => ({ + root: { + position: 'relative', + padding: theme.spacing(0), + marginBottom: theme.spacing(6), + marginTop: -theme.spacing(3), + display: 'flex', + flexFlow: 'row nowrap', + }, + icon: { + fontSize: 20, + }, + content: { + width: '100%', + maxWidth: 'inherit', + }, + message: { + display: 'flex', + alignItems: 'center', + }, + info: { + backgroundColor: theme.palette.primary.main, + }, + error: { + backgroundColor: theme.palette.error.dark, + }, +})); + +type Props = { + variant: 'info' | 'error'; + // setting: Setting; + message: ReactNode; +}; + +const DismissableBanner: FC = ({ variant, /* setting, */ message }) => { + // const [show, setShown, loading] = useSetting(setting); + const [show, setShown] = useState(true); + const classes = useStyles(); + + const handleClick = () => { + setShown(false); + }; + + return ( + + + + , + ]} + /> + + ); +}; + +export default DismissableBanner; diff --git a/packages/core/src/components/DismissableBanner/index.ts b/packages/core/src/components/DismissableBanner/index.ts new file mode 100644 index 0000000000..28091e858b --- /dev/null +++ b/packages/core/src/components/DismissableBanner/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export { default } from './DismissableBanner'; diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 789c57430e..be9cdc432d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -23,6 +23,7 @@ export type { PageTheme } from './layout/Page'; export { default as CodeSnippet } from './components/CodeSnippet'; export { default as Content } from './layout/Content/Content'; export { default as ContentHeader } from './layout/ContentHeader/ContentHeader'; +export { default as DismissableBanner } from './components/DismissableBanner'; export { default as Header } from './layout/Header/Header'; export { default as HeaderLabel } from './layout/HeaderLabel'; export { default as HomepageTimer } from './layout/HomepageTimer'; diff --git a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx index f5094542a5..610ecc3bb8 100644 --- a/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx +++ b/plugins/catalog/src/components/CatalogPage/CatalogPage.tsx @@ -18,6 +18,7 @@ import React, { FC } from 'react'; import { Content, ContentHeader, + DismissableBanner, Header, HomepageTimer, SupportButton, @@ -31,7 +32,7 @@ import { CatalogFilter, CatalogFilterItem, } from '../CatalogFilter/CatalogFilter'; -import { Button, makeStyles } from '@material-ui/core'; +import { Button, makeStyles, Typography, Link } from '@material-ui/core'; import { filterGroups, defaultFilter } from '../../data/filters'; const useStyles = makeStyles(theme => ({ @@ -65,6 +66,22 @@ const CatalogPage: FC = ({ componentFactory }) => {
    + + + 👋🏼 + {' '} + Welcome to Backstage, we are happy to have you. Start by checking + out our{' '} + + getting started + {' '} + page. + + } + />