diff --git a/.github/workflows/techdocs.yml b/.github/workflows/techdocs.yml new file mode 100644 index 0000000000..6d6a6e904e --- /dev/null +++ b/.github/workflows/techdocs.yml @@ -0,0 +1,35 @@ +name: TechDocs + +on: + pull_request: + paths: + - '.github/workflows/techdocs.yml' + - 'packages/techdocs-cli/**' + - 'plugins/techdocs/**' + +jobs: + build: + runs-on: ${{ matrix.os }} + + strategy: + matrix: + os: [ubuntu-latest] + python-version: [3.7] + + env: + TECHDOCS_CORE_PATH: ./plugins/techdocs/mkdocs/container/techdocs-core + + name: Python ${{ matrix.node-version }} on ${{ matrix.os }} + steps: + - uses: actions/checkout@v2 + + # Lint Python code for techdocs-core package + - name: prepare python environment + run: | + python3 -m pip install --index-url https://pypi.org/simple/ setuptools + python3 -m pip install --upgrade pip + python3 -m pip install --index-url https://pypi.org/simple/ -r $TECHDOCS_CORE_PATH/requirements.txt + + - name: lint techdocs-core package + run: | + python3 -m black --check $TECHDOCS_CORE_PATH/src diff --git a/docs/FAQ.md b/docs/FAQ.md index 5167139d45..e38fa84e73 100644 --- a/docs/FAQ.md +++ b/docs/FAQ.md @@ -97,17 +97,10 @@ app and configures which plugins are available to use in the app. ​ The **software engineer** uses the app's functionality and interacts with its plugins. ​ -### What is a "plugin" in Backstage? +### What is the use of a "plugin" in Backstage? + +​ A Backstage Plugin adds functionality to Backstage. ​ -By far, our most-used plugin is our TechDocs plugin, which we use for creating -technical documentation. Our philosophy at Spotify is to treat "docs like code", -where you write documentation using the same workflow as you write your code. -This makes it easier to create, find, and update documentation. We hope to -release -[the open source version](https://github.com/spotify/backstage/issues/687) in -the future. (See also: -"[Will Spotify's internal plugins be open sourced, too?](https://github.com/spotify/backstage/blob/master/docs/FAQ.md#what-is-a-plugin-in-backstage)" -above) ### Do I have to write plugins in TypeScript? diff --git a/docs/auth/README.md b/docs/auth/README.md new file mode 100644 index 0000000000..081d10add6 --- /dev/null +++ b/docs/auth/README.md @@ -0,0 +1,86 @@ +# User Authentication and Authorization in Backstage + +## Summary + +The purpose of the Auth APIs in Backstage are to identify the user, and to +provide a way for plugins to request access to 3rd party services on behalf of +the user (OAuth). This documentation focuses on the implementation of that +solution and how to extend it. For documentation on how to consume the Auth APIs +in a plugin, see [TODO](#TODO). + +### Accessing Third Party Services + +The main pattern for talking to third party services in Backstage is +user-to-server requests, where short-lived OAuth Access Tokens are requested by +plugins to authenticate calls to external services. These calls can be made +either directly to the services or through a backend plugin or service. + +By relying on user-to-server calls we keep the coupling between the frontend and +backend low, and provide a much lower barrier for plugins to make use of third +party services. This is in comparison to for example a session-based system, +where access tokens are stored server-side. Such a solution would require a much +deeper coupling between the auth backend plugin, its session storage, and other +backend plugins or separate services. A goal of Backstage is to make it as easy +as possible to create new plugins, and an auth solution based on user-to-server +OAuth helps in that regard. + +The method with which frontend plugins request access to third party services is +through [Utility APIs](../getting-started/utility-apis.md) for each service +provider. For a full list of providers, see [TODO](#TODO). + +### Identity - WIP + +Identity management is still work in progress, but there are already a couple of +pieces in place that can be used. + +#### Identity for Plugin Developers + +As a plugin developer, there are two main touchpoints for identities: the +`IdentityApi` exported by `@backstage/core` via the `identityApiRef`, and a not +yet existing middleware exported by `@backstage/backend-common`. + +The `IdentityApi` gives access to the signed-in user's identity in the frontend. +It provides access to the user's ID, lightweight profile information, and an ID +token used to make authenticated calls within Backstage. + +The middleware that will be provided by `@backstage/backend-common` allows +verification of Backstage ID tokens, and optionally loading additional +information about the user. The progress is tracked in +https://github.com/spotify/backstage/issues/1435. + +#### Identity for App Developers + +If you're setting up your own Backstage app, or want to add a new identity +provider, there are three touchpoints: the frontend auth APIs in +`@backstage/core-api`, the backend auth providers in `auth-backend`, and the +`SignInPage` component configured in the Backstage app via `createApp`. + +The frontend APIs and backend providers are tightly coupled together for each +auth provider, and together they implement an e2e auth flow. Only some auth +providers also act as identity providers though. For example, at the moment of +writing, the Google Auth provider is able to act as a Backstage identity +provider, but the GitHub one can not. For an auth provider to also act as an +identity provider, it needs to implement the `BackstageIdentityApi` in the +frontend, and in the backend it needs to return a `BackstageIdentity` structure. + +It is up to each provider to implement the mapping between a provider identity +and the corresponding Backstage identity. That is currently still work in +progress, and as a stop-gap for example the Google provider returns the local +part of the user's email as the user ID. + +The final piece of the puzzle is the `SignInPage` component that can be +configured as part of the app. Without a sign-in page, Backstage will fall back +to a `guest` identity for all users, without any ID token. To enable sign-in, a +`SignInPage` needs to be configured, which in turn has to supply a user to the +app. The `@backstage/core` package provides a basic sign-in page that allows +both the user and the app developer to choose between a couple of different +sign-in methods. + +## Further Reading + +More details are provided in dedicated sections of the documentation. + +- [OAuth](./oauth): Description of the generic OAuth flow implemented by the + [auth-backend](../../plugins/auth-backend). +- [Glossary](./glossary): Glossary of some common terms related to the auth + flows. diff --git a/docs/auth/overview.md b/docs/auth/overview.md deleted file mode 100644 index 2806b2dccf..0000000000 --- a/docs/auth/overview.md +++ /dev/null @@ -1,44 +0,0 @@ -# User Authentication and Authorization in Backstage - -## Summary - -The purpose of the Auth APIs in Backstage are to identify the user, and to -provide a way for plugins to request access to 3rd party services on behalf of -the user (OAuth). This documentation focuses on the implementation of that -solution and how to extend it. For documentation on how to consume the Auth APIs -in a plugin, see [TODO](#TODO). - -### Accessing Third Party Services - -The main pattern for talking to third party services in Backstage is -user-to-server requests, where short-lived OAuth Access Tokens are requested by -plugins to authenticate calls to external services. These calls can be made -either directly to the services or through a backend plugin or service. - -By relying on user-to-server calls we keep the coupling between the frontend and -backend low, and provide a much lower barrier for plugins to make use of third -party services. This is in comparison to for example a session-based system, -where access tokens are stored server-side. Such a solution would require a much -deeper coupling between the auth backend plugin, its session storage, and other -backend plugins or separate services. A goal of Backstage is to make it as easy -as possible to create new plugins, and an auth solution based on user-to-server -OAuth helps in that regard. - -The method with which frontend plugins request access to third party services is -through [Utility APIs](../getting-started/utility-apis.md) for each service -provider. For a full list of providers, see [TODO](#TODO). - -### Identity - TODO - -This documentation currently only covers the OAuth use-case, as identity -management is not settled yet and part of an -[upcoming milestone](https://github.com/spotify/backstage/milestone/12). - -## Further Reading - -More details are provided in dedicated sections of the documentation. - -- [OAuth](./oauth): Description of the generic OAuth flow implemented by the - [auth-backend](../../plugins/auth-backend). -- [Glossary](./glossary): Glossary of some common terms related to the auth - flows. diff --git a/packages/app/package.json b/packages/app/package.json index 6ccf48584f..118b4c15ff 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -32,12 +32,13 @@ }, "devDependencies": { "@testing-library/cypress": "^6.0.0", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/jquery": "^3.3.34", "@types/node": "^12.0.0", + "@types/react-dom": "^16.9.8", "@types/zen-observable": "^0.8.0", "cross-env": "^7.0.0", "cypress": "^4.2.0", diff --git a/packages/catalog-model/examples/artist-lookup-component.yaml b/packages/catalog-model/examples/artist-lookup-component.yaml index bb7d8d5767..448dd13fa3 100644 --- a/packages/catalog-model/examples/artist-lookup-component.yaml +++ b/packages/catalog-model/examples/artist-lookup-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: experimental - owner: tools@example.com + owner: artists@example.com diff --git a/packages/catalog-model/examples/playback-order-component.yaml b/packages/catalog-model/examples/playback-order-component.yaml index fa9f68e77a..4f93b65edf 100644 --- a/packages/catalog-model/examples/playback-order-component.yaml +++ b/packages/catalog-model/examples/playback-order-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: production - owner: tools@example.com + owner: guest diff --git a/packages/catalog-model/examples/podcast-api-component.yaml b/packages/catalog-model/examples/podcast-api-component.yaml index 2a27a0a2c5..19f9a3d4de 100644 --- a/packages/catalog-model/examples/podcast-api-component.yaml +++ b/packages/catalog-model/examples/podcast-api-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: experimental - owner: tools@example.com + owner: players@example.com diff --git a/packages/catalog-model/examples/searcher-component.yaml b/packages/catalog-model/examples/searcher-component.yaml index d82f6e42c7..33d765117c 100644 --- a/packages/catalog-model/examples/searcher-component.yaml +++ b/packages/catalog-model/examples/searcher-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: production - owner: tools@example.com + owner: guest diff --git a/packages/catalog-model/examples/shuffle-api-component.yaml b/packages/catalog-model/examples/shuffle-api-component.yaml index ed0517835c..275c48d6ba 100644 --- a/packages/catalog-model/examples/shuffle-api-component.yaml +++ b/packages/catalog-model/examples/shuffle-api-component.yaml @@ -6,4 +6,4 @@ metadata: spec: type: service lifecycle: production - owner: tools@example.com + owner: guest diff --git a/packages/cli/templates/default-app/packages/app/package.json.hbs b/packages/cli/templates/default-app/packages/app/package.json.hbs index 79120e0d7e..31e6baa63e 100644 --- a/packages/cli/templates/default-app/packages/app/package.json.hbs +++ b/packages/cli/templates/default-app/packages/app/package.json.hbs @@ -18,12 +18,12 @@ "react-use": "^14.2.0" }, "devDependencies": { - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", + "@types/react-dom": "^16.9.8", "cross-env": "^7.0.0", "cypress": "^4.2.0", "eslint-plugin-cypress": "^2.10.3", diff --git a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs index 904186ac60..b686abab9a 100644 --- a/packages/cli/templates/default-app/plugins/welcome/package.json.hbs +++ b/packages/cli/templates/default-app/plugins/welcome/package.json.hbs @@ -31,9 +31,8 @@ "devDependencies": { "@backstage/cli": "^{{version}}", "@backstage/dev-utils": "^{{version}}", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@types/testing-library__jest-dom": "^5.0.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/packages/cli/templates/default-plugin/package.json.hbs b/packages/cli/templates/default-plugin/package.json.hbs index d5728213e9..da611a13b7 100644 --- a/packages/cli/templates/default-plugin/package.json.hbs +++ b/packages/cli/templates/default-plugin/package.json.hbs @@ -33,12 +33,11 @@ "devDependencies": { "@backstage/cli": "^{{version}}", "@backstage/dev-utils": "^{{version}}", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/packages/cli/templates/default-plugin/src/plugin.ts.hbs b/packages/cli/templates/default-plugin/src/plugin.ts.hbs index 01df20a06a..52cce54002 100644 --- a/packages/cli/templates/default-plugin/src/plugin.ts.hbs +++ b/packages/cli/templates/default-plugin/src/plugin.ts.hbs @@ -1,18 +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. -*/ + * 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 { createPlugin, createRouteRef } from '@backstage/core'; import ExampleComponent from './components/ExampleComponent'; diff --git a/packages/core-api/package.json b/packages/core-api/package.json index c6b25fc0f7..f9f206daf5 100644 --- a/packages/core-api/package.json +++ b/packages/core-api/package.json @@ -43,9 +43,9 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/test-utils-core": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", "@types/zen-observable": "^0.8.0", diff --git a/packages/core-api/src/apis/definitions/auth.ts b/packages/core-api/src/apis/definitions/auth.ts index ccd40e66ce..d5c0e9c0a4 100644 --- a/packages/core-api/src/apis/definitions/auth.ts +++ b/packages/core-api/src/apis/definitions/auth.ts @@ -173,7 +173,7 @@ export type ProfileInfo = { /** * Email ID. */ - email: string; + email?: string; /** * Display name that can be presented to the user. diff --git a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts index 4641dd7b50..ddf03034dc 100644 --- a/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/github/GithubAuth.ts @@ -29,7 +29,6 @@ import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { StaticAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { Observable } from '../../../../types'; -import { SessionStateTracker } from '../../../../lib/AuthSessionManager/SessionStateTracker'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GithubAuth @@ -95,44 +94,34 @@ class GithubAuth implements OAuthApi, SessionStateApi { return new GithubAuth(sessionManager); } - private readonly sessionStateTracker = new SessionStateTracker(); - sessionState$(): Observable { - return this.sessionStateTracker.observable; + return this.sessionManager.sessionState$(); } constructor(private readonly sessionManager: SessionManager) {} async getAccessToken(scope?: string, options?: AuthRequestOptions) { - const normalizedScopes = GithubAuth.normalizeScope(scope); const session = await this.sessionManager.getSession({ ...options, - scopes: normalizedScopes, + scopes: GithubAuth.normalizeScope(scope), }); - this.sessionStateTracker.setIsSignedId(!!session); - if (session) { - return session.providerInfo.accessToken; - } - return ''; + return session?.providerInfo.accessToken ?? ''; } async getBackstageIdentity( options: AuthRequestOptions = {}, ): Promise { const session = await this.sessionManager.getSession(options); - this.sessionStateTracker.setIsSignedId(!!session); return session?.backstageIdentity; } async getProfile(options: AuthRequestOptions = {}) { const session = await this.sessionManager.getSession(options); - this.sessionStateTracker.setIsSignedId(!!session); return session?.profile; } async logout() { await this.sessionManager.removeSession(); - this.sessionStateTracker.setIsSignedId(false); } static normalizeScope(scope?: string): Set { 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 db26d2d9a4..594116fc1a 100644 --- a/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts +++ b/packages/core-api/src/apis/implementations/auth/google/GoogleAuth.ts @@ -32,7 +32,6 @@ import { OAuthRequestApi, AuthProvider } from '../../../definitions'; import { SessionManager } from '../../../../lib/AuthSessionManager/types'; import { RefreshingAuthSessionManager } from '../../../../lib/AuthSessionManager'; import { Observable } from '../../../../types'; -import { SessionStateTracker } from '../../../../lib/AuthSessionManager/SessionStateTracker'; type CreateOptions = { // TODO(Rugvip): These two should be grabbed from global config when available, they're not unique to GoogleAuth @@ -117,10 +116,8 @@ class GoogleAuth return new GoogleAuth(sessionManager); } - private readonly sessionStateTracker = new SessionStateTracker(); - sessionState$(): Observable { - return this.sessionStateTracker.observable; + return this.sessionManager.sessionState$(); } constructor(private readonly sessionManager: SessionManager) {} @@ -129,43 +126,31 @@ class GoogleAuth scope?: string | string[], options?: AuthRequestOptions, ) { - const normalizedScopes = GoogleAuth.normalizeScopes(scope); const session = await this.sessionManager.getSession({ ...options, - scopes: normalizedScopes, + scopes: GoogleAuth.normalizeScopes(scope), }); - this.sessionStateTracker.setIsSignedId(!!session); - if (session) { - return session.providerInfo.accessToken; - } - return ''; + return session?.providerInfo.accessToken ?? ''; } async getIdToken(options: AuthRequestOptions = {}) { const session = await this.sessionManager.getSession(options); - this.sessionStateTracker.setIsSignedId(!!session); - if (session) { - return session.providerInfo.idToken; - } - return ''; + return session?.providerInfo.idToken ?? ''; } async logout() { await this.sessionManager.removeSession(); - this.sessionStateTracker.setIsSignedId(false); } async getBackstageIdentity( options: AuthRequestOptions = {}, ): Promise { const session = await this.sessionManager.getSession(options); - this.sessionStateTracker.setIsSignedId(!!session); return session?.backstageIdentity; } async getProfile(options: AuthRequestOptions = {}) { const session = await this.sessionManager.getSession(options); - this.sessionStateTracker.setIsSignedId(!!session); return session?.profile; } diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts index 91e55c3a48..4572b923dd 100644 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.test.ts @@ -38,6 +38,7 @@ class LocalStorage { class MockManager implements SessionManager { getSession = jest.fn(); removeSession = jest.fn(); + sessionState$ = jest.fn(); } describe('GheAuth AuthSessionStore', () => { @@ -119,4 +120,11 @@ describe('GheAuth AuthSessionStore', () => { expect(localStorage.getItem('my-key')).toBe(null); }); + + it('should forward sessionState calls', () => { + const manager = new MockManager(); + const store = new AuthSessionStore({ manager, ...defaultOptions }); + store.sessionState$(); + expect(manager.sessionState$).toHaveBeenCalled(); + }); }); diff --git a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts index 8d318f5bff..3e3a40d02a 100644 --- a/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts +++ b/packages/core-api/src/lib/AuthSessionManager/AuthSessionStore.ts @@ -82,6 +82,10 @@ export class AuthSessionStore implements SessionManager { await this.manager.removeSession(); } + sessionState$() { + return this.manager.sessionState$(); + } + private loadSession(): T | undefined { try { const sessionJson = localStorage.getItem(this.storageKey); diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts index 793c6f1708..12e7c0a978 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.test.ts @@ -15,6 +15,7 @@ */ import { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager'; +import { SessionState } from '../../apis'; const defaultOptions = { sessionScopes: (session: { scopes: Set }) => session.scopes, @@ -22,21 +23,44 @@ const defaultOptions = { }; describe('RefreshingAuthSessionManager', () => { - it('should save result form createSession', async () => { + it('should save result from createSession', async () => { const createSession = jest.fn().mockResolvedValue({ expired: false }); const refreshSession = jest.fn().mockRejectedValue(new Error('NOPE')); + const removeSession = jest.fn(); const manager = new RefreshingAuthSessionManager({ - connector: { createSession, refreshSession }, + connector: { createSession, refreshSession, removeSession }, ...defaultOptions, } as any); + const stateSubscriber = jest.fn(); + manager.sessionState$().subscribe(stateSubscriber); + await Promise.resolve(); // Wait a tick for observer to post a value + + expect(stateSubscriber.mock.calls).toEqual([[SessionState.SignedOut]]); await manager.getSession({}); expect(createSession).toBeCalledTimes(1); + expect(stateSubscriber.mock.calls).toEqual([ + [SessionState.SignedOut], + [SessionState.SignedIn], + ]); await manager.getSession({}); expect(createSession).toBeCalledTimes(1); expect(refreshSession).toBeCalledTimes(1); + expect(stateSubscriber.mock.calls).toEqual([ + [SessionState.SignedOut], + [SessionState.SignedIn], + ]); + + expect(removeSession).toHaveBeenCalledTimes(0); + await manager.removeSession(); + expect(removeSession).toHaveBeenCalledTimes(1); + expect(stateSubscriber.mock.calls).toEqual([ + [SessionState.SignedOut], + [SessionState.SignedIn], + [SessionState.SignedOut], + ]); }); it('should ask consent only if scopes have changed', async () => { @@ -130,7 +154,7 @@ describe('RefreshingAuthSessionManager', () => { expect(refreshSession).toBeCalledTimes(1); }); - it('should remove session and reload', async () => { + it('should remove session straight away', async () => { const removeSession = jest.fn(); const manager = new RefreshingAuthSessionManager({ connector: { removeSession }, diff --git a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts index f7d5bcf7ca..e46c505c12 100644 --- a/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/RefreshingAuthSessionManager.ts @@ -22,6 +22,7 @@ import { } from './types'; import { AuthConnector } from '../AuthConnector'; import { SessionScopeHelper, hasScopes } from './common'; +import { SessionStateTracker } from './SessionStateTracker'; type Options = { /** The connector used for acting on the auth session */ @@ -43,6 +44,7 @@ export class RefreshingAuthSessionManager implements SessionManager { private readonly helper: SessionScopeHelper; private readonly sessionScopesFunc: SessionScopesFunc; private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc; + private readonly stateTracker = new SessionStateTracker(); private refreshPromise?: Promise; private currentSession: T | undefined; @@ -109,16 +111,18 @@ export class RefreshingAuthSessionManager implements SessionManager { ...options, scopes: this.helper.getExtendedScope(this.currentSession, options.scopes), }); + this.stateTracker.setIsSignedIn(true); return this.currentSession; } async removeSession() { this.currentSession = undefined; await this.connector.removeSession(); + this.stateTracker.setIsSignedIn(false); } - async getCurrentSession() { - return this.currentSession; + sessionState$() { + return this.stateTracker.sessionState$(); } private async collapsedSessionRefresh(): Promise { @@ -129,7 +133,9 @@ export class RefreshingAuthSessionManager implements SessionManager { this.refreshPromise = this.connector.refreshSession(); try { - return await this.refreshPromise; + const session = await this.refreshPromise; + this.stateTracker.setIsSignedIn(true); + return session; } finally { delete this.refreshPromise; } diff --git a/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts index de308acb0c..c4260bb31d 100644 --- a/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts +++ b/packages/core-api/src/lib/AuthSessionManager/SessionStateTracker.ts @@ -16,17 +16,25 @@ import { BehaviorSubject } from '..'; import { SessionState } from '../../apis'; +import { Observable } from '../../types'; export class SessionStateTracker { - private signedIn: boolean = false; - observable = new BehaviorSubject(SessionState.SignedOut); + private readonly subject = new BehaviorSubject( + SessionState.SignedOut, + ); - setIsSignedId(isSignedIn: boolean) { + private signedIn: boolean = false; + + setIsSignedIn(isSignedIn: boolean) { if (this.signedIn !== isSignedIn) { this.signedIn = isSignedIn; - this.observable.next( + this.subject.next( this.signedIn ? SessionState.SignedIn : SessionState.SignedOut, ); } } + + sessionState$(): Observable { + return this.subject; + } } diff --git a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts index 5ecbbc0c4e..e59c11421c 100644 --- a/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts +++ b/packages/core-api/src/lib/AuthSessionManager/StaticAuthSessionManager.ts @@ -17,6 +17,7 @@ import { SessionManager, GetSessionOptions } from './types'; import { AuthConnector } from '../AuthConnector'; import { SessionScopeHelper } from './common'; +import { SessionStateTracker } from './SessionStateTracker'; type Options = { /** The connector used for acting on the auth session */ @@ -33,6 +34,7 @@ type Options = { export class StaticAuthSessionManager implements SessionManager { private readonly connector: AuthConnector; private readonly helper: SessionScopeHelper; + private readonly stateTracker = new SessionStateTracker(); private currentSession: T | undefined; @@ -60,11 +62,17 @@ export class StaticAuthSessionManager implements SessionManager { ...options, scopes: this.helper.getExtendedScope(this.currentSession, options.scopes), }); + this.stateTracker.setIsSignedIn(true); return this.currentSession; } async removeSession() { this.currentSession = undefined; await this.connector.removeSession(); + this.stateTracker.setIsSignedIn(false); + } + + sessionState$() { + return this.stateTracker.sessionState$(); } } diff --git a/packages/core-api/src/lib/AuthSessionManager/types.ts b/packages/core-api/src/lib/AuthSessionManager/types.ts index d28a751fbf..804c7121e1 100644 --- a/packages/core-api/src/lib/AuthSessionManager/types.ts +++ b/packages/core-api/src/lib/AuthSessionManager/types.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { Observable } from '../../types'; +import { SessionState } from '../../apis'; + export type GetSessionOptions = { optional?: boolean; instantPopup?: boolean; @@ -29,6 +32,8 @@ export type SessionManager = { getSession(options: GetSessionOptions): Promise; removeSession(): Promise; + + sessionState$(): Observable; }; /** diff --git a/packages/core/package.json b/packages/core/package.json index 82ea259ba1..10a6c5d045 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -56,9 +56,9 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/test-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/classnames": "^2.2.9", "@types/google-protobuf": "^3.7.2", "@types/jest": "^25.2.2", diff --git a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx index 101ae7010b..3b854801c9 100644 --- a/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx +++ b/packages/core/src/layout/Sidebar/Settings/UserProfile.tsx @@ -34,14 +34,16 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({ }) => { const ref = useRef(); // for scrolling down when collapse item opens const classes = useStyles(); - const profile = useApi(identityApiRef).getProfile(); + const identityApi = useApi(identityApiRef); const handleClick = () => { setOpen(!open); setTimeout(() => ref.current?.scrollIntoView({ behavior: 'smooth' }), 300); }; - const displayName = profile.displayName ?? profile.email; + const userId = identityApi.getUserId(); + const profile = identityApi.getProfile(); + const displayName = profile.displayName ?? userId; const SignInAvatar = () => ( {displayName[0]} diff --git a/packages/dev-utils/package.json b/packages/dev-utils/package.json index 65f92242da..fce1340386 100644 --- a/packages/dev-utils/package.json +++ b/packages/dev-utils/package.json @@ -35,9 +35,9 @@ "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", "@material-ui/icons": "^4.9.1", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/react": "^16.9", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/packages/test-utils-core/package.json b/packages/test-utils-core/package.json index 60e0a6bdbe..8252d396ff 100644 --- a/packages/test-utils-core/package.json +++ b/packages/test-utils-core/package.json @@ -29,8 +29,8 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", "@types/react": "^16.9", "react": "^16.12.0", "react-dom": "^16.12.0" diff --git a/packages/test-utils/package.json b/packages/test-utils/package.json index 51ab99fcb1..501e288754 100644 --- a/packages/test-utils/package.json +++ b/packages/test-utils/package.json @@ -34,9 +34,9 @@ "@backstage/test-utils-core": "^0.1.1-alpha.12", "@backstage/theme": "^0.1.1-alpha.12", "@material-ui/core": "^4.9.1", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/react": "^16.9", "react": "^16.12.0", "react-dom": "^16.12.0", diff --git a/plugins/auth-backend/src/lib/OAuthProvider.test.ts b/plugins/auth-backend/src/lib/OAuthProvider.test.ts index f92875fd48..f67a91dfcd 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.test.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.test.ts @@ -23,13 +23,9 @@ import { verifyNonce, OAuthProvider, } from './OAuthProvider'; -import { - WebMessageResponse, - OAuthProviderHandlers, - OAuthResponse, -} from '../providers/types'; +import { WebMessageResponse, OAuthProviderHandlers } from '../providers/types'; -const mockResponseData: OAuthResponse = { +const mockResponseData = { providerInfo: { accessToken: 'ACCESS_TOKEN', idToken: 'ID_TOKEN', @@ -39,6 +35,9 @@ const mockResponseData: OAuthResponse = { profile: { email: 'foo@bar.com', }, + backstageIdentity: { + id: 'foo', + }, }; describe('OAuthProvider Utils', () => { @@ -350,7 +349,7 @@ describe('OAuthProvider', () => { expect(mockResponse.send).toHaveBeenCalledWith({ ...mockResponseData, backstageIdentity: { - id: mockResponseData.profile.email, + id: mockResponseData.backstageIdentity.id, idToken: 'my-id-token', }, }); diff --git a/plugins/auth-backend/src/lib/OAuthProvider.ts b/plugins/auth-backend/src/lib/OAuthProvider.ts index dac31af451..b7179659f9 100644 --- a/plugins/auth-backend/src/lib/OAuthProvider.ts +++ b/plugins/auth-backend/src/lib/OAuthProvider.ts @@ -18,10 +18,10 @@ import express from 'express'; import crypto from 'crypto'; import { URL } from 'url'; import { - AuthResponse, AuthProviderRouteHandlers, OAuthProviderHandlers, WebMessageResponse, + BackstageIdentity, } from '../providers/types'; import { InputError } from '@backstage/backend-common'; import { TokenIssuer } from '../identity'; @@ -147,19 +147,12 @@ export class OAuthProvider implements AuthProviderRouteHandlers { this.setRefreshTokenCookie(res, refreshToken); } - const id = response.profile.email; - const idToken = await this.options.tokenIssuer.issueToken({ - claims: { sub: id }, - }); - const fullResponse: AuthResponse = { - ...response, - backstageIdentity: { id, idToken }, - }; + await this.populateIdentity(response.backstageIdentity); // post message back to popup if successful return postMessageResponse(res, this.options.appOrigin, { type: 'authorization_response', - response: fullResponse, + response, }); } catch (error) { // post error message back to popup if failure @@ -213,21 +206,30 @@ export class OAuthProvider implements AuthProviderRouteHandlers { // get new access_token const response = await this.providerHandlers.refresh(refreshToken, scope); - const id = response.profile.email; - const idToken = await this.options.tokenIssuer.issueToken({ - claims: { sub: id }, - }); - const fullResponse: AuthResponse = { - ...response, - backstageIdentity: { id, idToken }, - }; + await this.populateIdentity(response.backstageIdentity); - res.send(fullResponse); + res.send(response); } catch (error) { res.status(401).send(`${error.message}`); } } + /** + * If the response from the OAuth provider includes a Backstage identity, we + * make sure it's populated with all the information we can derive from the user ID. + */ + private async populateIdentity(identity?: BackstageIdentity) { + if (!identity) { + return; + } + + if (!identity.idToken) { + identity.idToken = await this.options.tokenIssuer.issueToken({ + claims: { sub: identity.id }, + }); + } + } + private setNonceCookie = (res: express.Response, nonce: string) => { res.cookie(`${this.options.providerId}-nonce`, nonce, { maxAge: TEN_MINUTES_MS, diff --git a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts index 46a386f419..1167946560 100644 --- a/plugins/auth-backend/src/lib/PassportStrategyHelper.ts +++ b/plugins/auth-backend/src/lib/PassportStrategyHelper.ts @@ -57,10 +57,6 @@ export const makeProfileInfo = ( } } - if (!email) { - throw new Error('No email received in profile info'); - } - return { email, picture, diff --git a/plugins/auth-backend/src/providers/github/provider.ts b/plugins/auth-backend/src/providers/github/provider.ts index 3ae9858bb1..01652a6d7e 100644 --- a/plugins/auth-backend/src/providers/github/provider.ts +++ b/plugins/auth-backend/src/providers/github/provider.ts @@ -72,15 +72,13 @@ export class GithubAuthProvider implements OAuthProviderHandlers { return await executeRedirectStrategy(req, this._strategy, options); } - async handler(req: express.Request): Promise<{ response: OAuthResponse }> { - const result = await executeFrameHandlerStrategy( + async handler(req: express.Request) { + const { response } = await executeFrameHandlerStrategy( req, this._strategy, ); - return { - response: result.response, - }; + return { response }; } } diff --git a/plugins/auth-backend/src/providers/google/provider.ts b/plugins/auth-backend/src/providers/google/provider.ts index f12b248021..04705b66c1 100644 --- a/plugins/auth-backend/src/providers/google/provider.ts +++ b/plugins/auth-backend/src/providers/google/provider.ts @@ -97,14 +97,14 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { async handler( req: express.Request, ): Promise<{ response: OAuthResponse; refreshToken: string }> { - const result = await executeFrameHandlerStrategy< + const { response, privateInfo } = await executeFrameHandlerStrategy< OAuthResponse, PrivateInfo >(req, this._strategy); return { - response: result.response, - refreshToken: result.privateInfo.refreshToken, + response: await this.populateIdentity(response), + refreshToken: privateInfo.refreshToken, }; } @@ -121,7 +121,7 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { params.id_token, ); - return { + return this.populateIdentity({ providerInfo: { accessToken, idToken: params.id_token, @@ -129,7 +129,22 @@ export class GoogleAuthProvider implements OAuthProviderHandlers { scope: params.scope, }, profile, - }; + }); + } + + private async populateIdentity( + response: OAuthResponse, + ): Promise { + const { profile } = response; + + if (!profile.email) { + throw new Error('Google profile contained no email'); + } + + // TODO(Rugvip): Hardcoded to the local part of the email for now + const id = profile.email.split('@')[0]; + + return { ...response, backstageIdentity: { id } }; } } diff --git a/plugins/auth-backend/src/providers/types.ts b/plugins/auth-backend/src/providers/types.ts index 81773bf9dc..56d4db79fa 100644 --- a/plugins/auth-backend/src/providers/types.ts +++ b/plugins/auth-backend/src/providers/types.ts @@ -100,14 +100,20 @@ export interface OAuthProviderHandlers { */ handler( req: express.Request, - ): Promise<{ response: OAuthResponse; refreshToken?: string }>; + ): Promise<{ + response: AuthResponse; + refreshToken?: string; + }>; /** * (Optional) Given a refresh token and scope fetches a new access token from the auth provider. * @param {string} refreshToken * @param {string} scope */ - refresh?(refreshToken: string, scope: string): Promise; + refresh?( + refreshToken: string, + scope: string, + ): Promise>; /** * (Optional) Sign out of the auth provider. @@ -192,13 +198,10 @@ export type AuthProviderFactory = ( export type AuthResponse = { providerInfo: ProviderInfo; profile: ProfileInfo; - backstageIdentity: BackstageIdentity; + backstageIdentity?: BackstageIdentity; }; -export type OAuthResponse = Omit< - AuthResponse, - 'backstageIdentity' ->; +export type OAuthResponse = AuthResponse; export type BackstageIdentity = { /** @@ -209,7 +212,7 @@ export type BackstageIdentity = { /** * An ID token that can be used to authenticate the user within Backstage. */ - idToken: string; + idToken?: string; }; export type OAuthProviderInfo = { @@ -279,7 +282,7 @@ export type ProfileInfo = { /** * Email ID of the signed in user. */ - email: string; + email?: string; /** * Display name that can be presented to the signed in user. */ diff --git a/plugins/catalog/package.json b/plugins/catalog/package.json index c841b9dc7a..818a8c4bc6 100644 --- a/plugins/catalog/package.json +++ b/plugins/catalog/package.json @@ -41,13 +41,12 @@ "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", "@backstage/test-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", "@testing-library/react-hooks": "^3.3.0", - "@testing-library/user-event": "^10.2.4", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3", "msw": "^0.19.0", "react-test-renderer": "^16.13.1", diff --git a/plugins/circleci/package.json b/plugins/circleci/package.json index a938b9728b..8eede54580 100644 --- a/plugins/circleci/package.json +++ b/plugins/circleci/package.json @@ -47,13 +47,12 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", "@types/react-lazylog": "^4.5.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/explore/package.json b/plugins/explore/package.json index 755f0e50c3..dea3ffe87b 100644 --- a/plugins/explore/package.json +++ b/plugins/explore/package.json @@ -35,12 +35,11 @@ "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", "@backstage/test-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/gitops-profiles/package.json b/plugins/gitops-profiles/package.json index bbf9b9c5d8..bfb0328909 100644 --- a/plugins/gitops-profiles/package.json +++ b/plugins/gitops-profiles/package.json @@ -34,12 +34,11 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/graphiql/package.json b/plugins/graphiql/package.json index 0277363ad1..af971f9160 100644 --- a/plugins/graphiql/package.json +++ b/plugins/graphiql/package.json @@ -46,13 +46,12 @@ "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", "@backstage/test-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/codemirror": "^0.0.96", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3", "react-router-dom": "6.0.0-alpha.5" }, diff --git a/plugins/lighthouse/package.json b/plugins/lighthouse/package.json index 67d8bf143e..bb79e57179 100644 --- a/plugins/lighthouse/package.json +++ b/plugins/lighthouse/package.json @@ -36,12 +36,11 @@ "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", "@backstage/test-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx index ee0404fc22..aaf376923f 100644 --- a/plugins/lighthouse/src/components/CreateAudit/index.test.tsx +++ b/plugins/lighthouse/src/components/CreateAudit/index.test.tsx @@ -106,7 +106,7 @@ describe('CreateAudit', () => { fireEvent.click(rendered.getByText(/Create Audit/)); expect(rendered.getByLabelText(/URL/)).toBeDisabled(); - expect(rendered.getByText(/Create Audit/)).toBeDisabled(); + expect(rendered.getByText(/Create Audit/).parentElement).toBeDisabled(); }); }); diff --git a/plugins/register-component/package.json b/plugins/register-component/package.json index 5c879ff8a1..738cabea4b 100644 --- a/plugins/register-component/package.json +++ b/plugins/register-component/package.json @@ -38,12 +38,11 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/scaffolder-backend/package.json b/plugins/scaffolder-backend/package.json index 5a8ebddce5..47f5a31158 100644 --- a/plugins/scaffolder-backend/package.json +++ b/plugins/scaffolder-backend/package.json @@ -25,6 +25,7 @@ "@backstage/catalog-model": "^0.1.1-alpha.12", "@backstage/config": "^0.1.1-alpha.12", "@types/express": "^4.17.6", + "@types/dockerode": "^2.5.32", "compression": "^1.7.4", "cors": "^2.8.5", "dockerode": "^3.2.0", @@ -41,7 +42,6 @@ }, "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", - "@types/dockerode": "^2.5.32", "@types/fs-extra": "^9.0.1", "@types/git-url-parse": "^9.0.0", "@types/nodegit": "0.26.5", diff --git a/plugins/scaffolder/package.json b/plugins/scaffolder/package.json index 313242f2c0..65322c31d6 100644 --- a/plugins/scaffolder/package.json +++ b/plugins/scaffolder/package.json @@ -34,12 +34,11 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/sentry/package.json b/plugins/sentry/package.json index 969445c1ec..2af4badc07 100644 --- a/plugins/sentry/package.json +++ b/plugins/sentry/package.json @@ -36,12 +36,11 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/tech-radar/package.json b/plugins/tech-radar/package.json index 660ad735bc..a288268670 100644 --- a/plugins/tech-radar/package.json +++ b/plugins/tech-radar/package.json @@ -38,14 +38,13 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/color": "^3.0.1", "@types/d3-force": "^1.2.1", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/techdocs/mkdocs/README.md b/plugins/techdocs/mkdocs/README.md index 4d1c11d7b0..5c2a7725aa 100644 --- a/plugins/techdocs/mkdocs/README.md +++ b/plugins/techdocs/mkdocs/README.md @@ -6,8 +6,10 @@ Welcome to MkDocs. This is the TechDocs implementation of MkDocs. ## Getting started -``` - docker build ./container -t mkdocs-container +```bash +docker build ./container -t mkdocs-container - docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs-container serve -a 0.0.0.0:8000 +docker run -w /content -v $(pwd)/mock-docs:/content -p 8000:8000 -it mkdocs-container serve -a 0.0.0.0:8000 ``` + +Then open up `http://localhost:8000` on your local machine. diff --git a/plugins/techdocs/mkdocs/container/Dockerfile b/plugins/techdocs/mkdocs/container/Dockerfile index 1ae66589b6..f2a92287e4 100644 --- a/plugins/techdocs/mkdocs/container/Dockerfile +++ b/plugins/techdocs/mkdocs/container/Dockerfile @@ -1,22 +1,21 @@ - -# Copyright 2020 Spotify AB +# 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 +# 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 +# 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. +# 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. FROM python:3.7.7-alpine3.12 RUN apk update && apk --no-cache add gcc musl-dev -RUN pip install mkdocs==1.1.2 mkdocs-material==5.3.2 +RUN pip install --upgrade pip && pip install mkdocs==1.1.2 mkdocs-material==5.3.2 mkdocs-monorepo-plugin==0.4.5 pymdown-extensions==7.1 ADD ./techdocs-core /techdocs-core RUN pip install --no-index /techdocs-core diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/.gitignore b/plugins/techdocs/mkdocs/container/techdocs-core/.gitignore new file mode 100644 index 0000000000..cd5c4a44f3 --- /dev/null +++ b/plugins/techdocs/mkdocs/container/techdocs-core/.gitignore @@ -0,0 +1,2 @@ +.tox +*.egg-info diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/README.md b/plugins/techdocs/mkdocs/container/techdocs-core/README.md new file mode 100644 index 0000000000..143acf3c3b --- /dev/null +++ b/plugins/techdocs/mkdocs/container/techdocs-core/README.md @@ -0,0 +1,45 @@ +# techdocs-core + +This is the base [Mkdocs](https://mkdocs.org) plugin used when using Mkdocs with Spotify's TechDocs. It is written in Python and packages all of our Mkdocs defaults, such as theming, plugins, etc in a single plugin. + +## Usage + +**Installation instructions TBD.** We haven't published it to a Python registry yet. + +Once you have installed the `mkdocs-techdocs-core` plugin, you'll need to add it to your `mkdocs.yml`. + +```yaml +site_name: Backstage Docs + +nav: + - Home: index.md + - Developing a Plugin: developing-a-plugin.md + +plugins: + - techdocs-core +``` + +## Running Locally + +You can install this package locally using `pip` and the `--editable` flag used for making developing Python packages. + +```bash +pip install --editable . +``` + +You'll then have the `techdocs-core` package available to use in Mkdocs and `pip` will point the dependency to this folder. + +## Running with Docker + +In the parent `Dockerfile` we add this folder to the build and install the package locally in the container. In the future, we'll probably move away from this approach and have it download directly from a Python registry (and this folder will publish to one). + +See the `README.md` located in the `mkdocs/` folder for more details on how to build and run the Docker container. + +## Linting + +```bash +pip install -r requirements.txt +python -m black src/ +``` + +**Note:** This will write to all Python files in `src/` with the formatted code. If you would like to only check to see if it passes, simply append the `--check` flag. diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/requirements.txt b/plugins/techdocs/mkdocs/container/techdocs-core/requirements.txt index b3667f94ec..2a13d1a9da 100644 --- a/plugins/techdocs/mkdocs/container/techdocs-core/requirements.txt +++ b/plugins/techdocs/mkdocs/container/techdocs-core/requirements.txt @@ -1 +1,9 @@ +# The "base" version of the Mkdocs project. +# Note: if you update this, also update `install_requires` in setup.py +# https://github.com/mkdocs/mkdocs mkdocs==1.1.2 + +# The linter using for Python +# Note: This requires Python 3.6+ to run, but can format Python 2 code too. +# https://github.com/psf/black +black==19.10b0 diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/setup.py b/plugins/techdocs/mkdocs/container/techdocs-core/setup.py index faa8b8c46e..a259ae0615 100644 --- a/plugins/techdocs/mkdocs/container/techdocs-core/setup.py +++ b/plugins/techdocs/mkdocs/container/techdocs-core/setup.py @@ -1,17 +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. +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. """ from setuptools import setup, find_packages diff --git a/plugins/techdocs/mkdocs/container/techdocs-core/src/core.py b/plugins/techdocs/mkdocs/container/techdocs-core/src/core.py index e42a60d28b..18c22c40e3 100644 --- a/plugins/techdocs/mkdocs/container/techdocs-core/src/core.py +++ b/plugins/techdocs/mkdocs/container/techdocs-core/src/core.py @@ -19,17 +19,67 @@ from mkdocs.theme import Theme from mkdocs.contrib.search import SearchPlugin +from mkdocs_monorepo_plugin.plugin import MonorepoPlugin + + class TechDocsCore(BasePlugin): def on_config(self, config): - # Theme - config['theme'] = Theme(name="material") + # Theme + config["theme"] = Theme(name="material") - # Plugins - del config['plugins']['techdocs-core'] + # Plugins + del config["plugins"]["techdocs-core"] - search_plugin = SearchPlugin() - search_plugin.load_config({}) - config['plugins']['search'] = search_plugin + search_plugin = SearchPlugin() + search_plugin.load_config({}) - return config + monorepo_plugin = MonorepoPlugin() + monorepo_plugin.load_config({}) + config["plugins"]["search"] = search_plugin + config["plugins"]["monorepo"] = monorepo_plugin + + search_plugin = SearchPlugin() + search_plugin.load_config({}) + config["plugins"]["search"] = search_plugin + + # Markdown Extensions + config['markdown_extensions'].append('admonition') + config['markdown_extensions'].append('abbr') + config['markdown_extensions'].append('attr_list') + config['markdown_extensions'].append('def_list') + config['markdown_extensions'].append('codehilite') + config['mdx_configs']['codehilite'] = { + 'linenums': True, + 'guess_lang': False, + 'pygments_style': 'friendly', + } + config['markdown_extensions'].append('toc') + config['mdx_configs']['toc'] = { + 'permalink': True, + } + config['markdown_extensions'].append('footnotes') + config['markdown_extensions'].append('markdown.extensions.tables') + config['markdown_extensions'].append('pymdownx.betterem') + config['mdx_configs']['pymdownx.betterem'] = { + 'smart_enable': 'all', + } + config['markdown_extensions'].append('pymdownx.caret') + config['markdown_extensions'].append('pymdownx.critic') + config['markdown_extensions'].append('pymdownx.details') + config['markdown_extensions'].append('pymdownx.emoji') + config['mdx_configs']['pymdownx.emoji'] = { + 'emoji_generator': '!!python/name:pymdownx.emoji.to_svg', + } + config['markdown_extensions'].append('pymdownx.inlinehilite') + config['markdown_extensions'].append('pymdownx.magiclink') + config['markdown_extensions'].append('pymdownx.mark') + config['markdown_extensions'].append('pymdownx.smartsymbols') + config['markdown_extensions'].append('pymdownx.superfences') + config['markdown_extensions'].append('pymdownx.tasklist') + config['mdx_configs']['pymdownx.tasklist'] = { + 'custom_checkbox': True, + } + config['markdown_extensions'].append('pymdownx.tilde') + + return config diff --git a/plugins/techdocs/mkdocs/mock-docs/docs/index.md b/plugins/techdocs/mkdocs/mock-docs/docs/index.md index bc437e6180..e8ba4e2d6d 100644 --- a/plugins/techdocs/mkdocs/mock-docs/docs/index.md +++ b/plugins/techdocs/mkdocs/mock-docs/docs/index.md @@ -1 +1,32 @@ ## hello mock docs + +!!! test +Testing somethin + +Some text about MOCDOC + +\*[MOCDOC]: Mock Documentation + +This is a paragraph. +{: #test_id .test_class } + +Apple +: Pomaceous fruit of plants of the genus Malus in +the family Rosaceae. + +```javascript +import { test } from 'something'; + +const addThingToThing = (a, b) a + b; +``` + +- [abc](#abc) +- [xyz](#xyz) + +## abc + +This is a b c. + +## xyz + +This is x y z. diff --git a/plugins/techdocs/mkdocs/mock-docs/mkdocs.yml b/plugins/techdocs/mkdocs/mock-docs/mkdocs.yml index b14d82c4de..fca8418d12 100644 --- a/plugins/techdocs/mkdocs/mock-docs/mkdocs.yml +++ b/plugins/techdocs/mkdocs/mock-docs/mkdocs.yml @@ -2,7 +2,7 @@ site_name: 'mock-docs' nav: - Home: index.md + - SubDocs: '!include ./sub-docs/mkdocs.yml' plugins: - techdocs-core - diff --git a/plugins/techdocs/mkdocs/mock-docs/sub-docs/docs/index.md b/plugins/techdocs/mkdocs/mock-docs/sub-docs/docs/index.md new file mode 100644 index 0000000000..65c6644ef8 --- /dev/null +++ b/plugins/techdocs/mkdocs/mock-docs/sub-docs/docs/index.md @@ -0,0 +1 @@ +### This is an md file in another docs folder using the [MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin) diff --git a/plugins/techdocs/mkdocs/mock-docs/sub-docs/mkdocs.yml b/plugins/techdocs/mkdocs/mock-docs/sub-docs/mkdocs.yml new file mode 100644 index 0000000000..09504c1b31 --- /dev/null +++ b/plugins/techdocs/mkdocs/mock-docs/sub-docs/mkdocs.yml @@ -0,0 +1,4 @@ +site_name: subdocs + +nav: + - Home 2: "index.md" diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 068eb1ee22..1f83d6290f 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -33,12 +33,11 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/plugins/welcome/package.json b/plugins/welcome/package.json index 3fb10b64e3..c4b9b5e5bd 100644 --- a/plugins/welcome/package.json +++ b/plugins/welcome/package.json @@ -34,12 +34,11 @@ "devDependencies": { "@backstage/cli": "^0.1.1-alpha.12", "@backstage/dev-utils": "^0.1.1-alpha.12", - "@testing-library/jest-dom": "^5.7.0", - "@testing-library/react": "^9.3.2", - "@testing-library/user-event": "^10.2.4", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^10.4.1", + "@testing-library/user-event": "^12.0.7", "@types/jest": "^25.2.2", "@types/node": "^12.0.0", - "@types/testing-library__jest-dom": "^5.0.4", "jest-fetch-mock": "^3.0.3" }, "files": [ diff --git a/yarn.lock b/yarn.lock index ba53ee7513..04388bb623 100644 --- a/yarn.lock +++ b/yarn.lock @@ -876,7 +876,15 @@ core-js "^2.6.5" regenerator-runtime "^0.13.4" -"@babel/runtime-corejs3@^7.7.4", "@babel/runtime-corejs3@^7.8.3": +"@babel/runtime-corejs3@^7.10.2": + version "7.10.3" + resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.10.3.tgz#931ed6941d3954924a7aa967ee440e60c507b91a" + integrity sha512-HA7RPj5xvJxQl429r5Cxr2trJwOfPjKiqhCXcdQPSqO2G0RHPZpXu4fkYmBaTKCp2c/jRaMK9GB/lN+7zvvFPw== + dependencies: + core-js-pure "^3.0.0" + regenerator-runtime "^0.13.4" + +"@babel/runtime-corejs3@^7.8.3": version "7.9.2" resolved "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.9.2.tgz#26fe4aa77e9f1ecef9b776559bbb8e84d34284b7" integrity sha512-HHxmgxbIzOfFlZ+tdeRKtaxWOMUoCG5Mu3wKeUmOxjYrwb3AAHgnmtCUbPPK11/raIWLIBK250t8E2BPO0p7jA== @@ -891,6 +899,13 @@ dependencies: regenerator-runtime "^0.13.4" +"@babel/runtime@^7.10.2", "@babel/runtime@^7.10.3": + version "7.10.3" + resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.3.tgz#670d002655a7c366540c67f6fd3342cd09500364" + integrity sha512-RzGO0RLSdokm9Ipe/YD+7ww8X2Ro79qiXZF3HU9ljrM+qnJmH1Vqth+hbiQZy761LnMJTMitHDuKVYTk3k4dLw== + dependencies: + regenerator-runtime "^0.13.4" + "@babel/template@^7.3.3", "@babel/template@^7.7.4", "@babel/template@^7.8.3", "@babel/template@^7.8.6": version "7.8.6" resolved "https://registry.npmjs.org/@babel/template/-/template-7.8.6.tgz#86b22af15f828dfb086474f964dcc3e39c43ce2b" @@ -2468,11 +2483,6 @@ dependencies: any-observable "^0.3.0" -"@sheerun/mutationobserver-shim@^0.3.2": - version "0.3.3" - resolved "https://registry.npmjs.org/@sheerun/mutationobserver-shim/-/mutationobserver-shim-0.3.3.tgz#5405ee8e444ed212db44e79351f0c70a582aae25" - integrity sha512-DetpxZw1fzPD5xUBrIAoplLChO2VB8DlL5Gg+I1IR9b2wPqYIca2WSUxL5g1vLeR4MsQq1NeWriXAVffV+U1Fw== - "@sindresorhus/is@^0.14.0": version "0.14.0" resolved "https://registry.npmjs.org/@sindresorhus/is/-/is-0.14.0.tgz#9fb3a3cf3132328151f353de4632e01e52102bea" @@ -3173,37 +3183,23 @@ "@testing-library/dom" "^7.0.2" "@types/testing-library__cypress" "^5.0.3" -"@testing-library/dom@^6.15.0": - version "6.16.0" - resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-6.16.0.tgz#04ada27ed74ad4c0f0d984a1245bb29b1fd90ba9" - integrity sha512-lBD88ssxqEfz0wFL6MeUyyWZfV/2cjEZZV3YRpb2IoJRej/4f1jB0TzqIOznTpfR1r34CNesrubxwIlAQ8zgPA== +"@testing-library/dom@^7.0.2", "@testing-library/dom@^7.17.1": + version "7.17.2" + resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.17.2.tgz#d062e41336b885107bca8ffc7022eaf37cccaee1" + integrity sha512-TQAoIzoI9g64oNVJ+13i4cCh/DQp/n7fJOyMqlFB1oQVusPtSgiPImyb52CwtvPO7J0Im0+/4YcmxU9a+cVxxw== dependencies: - "@babel/runtime" "^7.8.4" - "@sheerun/mutationobserver-shim" "^0.3.2" - "@types/testing-library__dom" "^6.12.1" - aria-query "^4.0.2" - dom-accessibility-api "^0.3.0" - pretty-format "^25.1.0" - wait-for-expect "^3.0.2" + "@babel/runtime" "^7.10.3" + aria-query "^4.2.2" + dom-accessibility-api "^0.4.5" + pretty-format "^25.5.0" -"@testing-library/dom@^7.0.2": - version "7.1.2" - resolved "https://registry.npmjs.org/@testing-library/dom/-/dom-7.1.2.tgz#0942e3751beeea9820e14dd4bf685f1f1767353a" - integrity sha512-U0wLMbND1NUMUB65E9VmfuehT1GUSIHnT2zK7rjpDIdFNDbMtjDzbdaZXBYDp5lWzHJwUdPQozMd1GHp3O9gow== +"@testing-library/jest-dom@^5.10.1": + version "5.10.1" + resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.10.1.tgz#6508a9f007bd74e5d3c0b3135b668027ab663989" + integrity sha512-uv9lLAnEFRzwUTN/y9lVVXVXlEzazDkelJtM5u92PsGkEasmdI+sfzhZHxSDzlhZVTrlLfuMh2safMr8YmzXLg== dependencies: "@babel/runtime" "^7.9.2" - "@types/testing-library__dom" "^7.0.0" - aria-query "^4.0.2" - dom-accessibility-api "^0.4.2" - pretty-format "^25.1.0" - -"@testing-library/jest-dom@^5.7.0": - version "5.9.0" - resolved "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.9.0.tgz#86464c66cbe75e632b8adb636f539bfd0efc2c9c" - integrity sha512-uZ68dyILuM2VL13lGz4ehFEAgxzvLKRu8wQxyAZfejWnyMhmipJ60w4eG81NQikJHBfaYXx+Or8EaPQTDwGfPA== - dependencies: - "@babel/runtime" "^7.9.2" - "@types/testing-library__jest-dom" "^5.0.2" + "@types/testing-library__jest-dom" "^5.9.1" chalk "^3.0.0" css "^2.2.4" css.escape "^1.5.1" @@ -3220,19 +3216,20 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.0.0" -"@testing-library/react@^9.3.2": - version "9.5.0" - resolved "https://registry.npmjs.org/@testing-library/react/-/react-9.5.0.tgz#71531655a7890b61e77a1b39452fbedf0472ca5e" - integrity sha512-di1b+D0p+rfeboHO5W7gTVeZDIK5+maEgstrZbWZSSvxDyfDRkkyBE1AJR5Psd6doNldluXlCWqXriUfqu/9Qg== +"@testing-library/react@^10.4.1": + version "10.4.1" + resolved "https://registry.npmjs.org/@testing-library/react/-/react-10.4.1.tgz#d38dee4abab172c06f6cf8894c51190e6c503f61" + integrity sha512-QX31fRDGLnOdBYoQ95VEOYgRahaPfsI+toOaYhlvuGNFQrcagZv/KLWCIctRGB0h1PTsQt3JpLBbbLGM63yy5Q== dependencies: - "@babel/runtime" "^7.8.4" - "@testing-library/dom" "^6.15.0" - "@types/testing-library__react" "^9.1.2" + "@babel/runtime" "^7.10.3" + "@testing-library/dom" "^7.17.1" -"@testing-library/user-event@^10.2.4": - version "10.3.1" - resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-10.3.1.tgz#8ed6fbfa40fbb866fa4666c9a62d7f7ff151f93b" - integrity sha512-HozvWlr/xHmkoJrrDdZBbUOXAC/STAeXGyNU+g5KgEwWBpLJi1RPCHJKbTjwz8hp2jDFsk/jjfD0530eZjcFEg== +"@testing-library/user-event@^12.0.7": + version "12.0.7" + resolved "https://registry.npmjs.org/@testing-library/user-event/-/user-event-12.0.7.tgz#6028d8de294ebd1cbf63812bc40f99e61b82f318" + integrity sha512-v/1DS6V6P5zJVq5HCC/NU93CQaqML+exslOOYRmE71ULvtOCS4Fv6A2wgBjIsEEecjZtdS2LF+KlrOK3KvVZBA== + dependencies: + "@babel/runtime" "^7.10.2" "@theme-ui/color-modes@^0.3.1": version "0.3.1" @@ -3822,10 +3819,10 @@ "@types/webpack" "*" "@types/webpack-dev-server" "*" -"@types/react-dom@*": - version "16.9.5" - resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.5.tgz#5de610b04a35d07ffd8f44edad93a71032d9aaa7" - integrity sha512-BX6RQ8s9D+2/gDhxrj8OW+YD4R+8hj7FEM/OJHGNR0KipE1h1mSsf39YeyC81qafkq+N3rU3h3RFbLSwE5VqUg== +"@types/react-dom@^16.9.8": + version "16.9.8" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" + integrity sha512-ykkPQ+5nFknnlU6lDd947WbQ6TE3NNzbQAkInC2EKY1qeYdTKp7onFusmYZb+ityzx2YviqT6BXSu+LyWWJwcA== dependencies: "@types/react" "*" @@ -4015,21 +4012,14 @@ "@types/testing-library__dom" "*" cypress "*" -"@types/testing-library__dom@*", "@types/testing-library__dom@^6.12.1": +"@types/testing-library__dom@*": version "6.14.0" resolved "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-6.14.0.tgz#1aede831cb4ed4a398448df5a2c54b54a365644e" integrity sha512-sMl7OSv0AvMOqn1UJ6j1unPMIHRXen0Ita1ujnMX912rrOcawe4f7wu0Zt9GIQhBhJvH2BaibqFgQ3lP+Pj2hA== dependencies: pretty-format "^24.3.0" -"@types/testing-library__dom@^7.0.0": - version "7.0.0" - resolved "https://registry.npmjs.org/@types/testing-library__dom/-/testing-library__dom-7.0.0.tgz#c0fb7d1c2495a3d26f19342102142d47500f0319" - integrity sha512-1TEPWyqQ6IQ7R1hCegZmFSA3KrBQjdzJW7yC9ybpRcFst5XuPOqBGNr0mTAKbxwI/TrTyc1skeyLJrpcvAf93w== - dependencies: - pretty-format "^25.1.0" - -"@types/testing-library__jest-dom@^5.0.2", "@types/testing-library__jest-dom@^5.0.4": +"@types/testing-library__jest-dom@^5.9.1": version "5.9.1" resolved "https://registry.npmjs.org/@types/testing-library__jest-dom/-/testing-library__jest-dom-5.9.1.tgz#aba5ee062b7880f69c212ef769389f30752806e5" integrity sha512-yYn5EKHO3MPEMSOrcAb1dLWY+68CG29LiXKsWmmpVHqoP5+ZRiAVLyUHvPNrO2dABDdUGZvavMsaGpWNjM6N2g== @@ -4044,15 +4034,6 @@ "@types/react" "*" "@types/react-test-renderer" "*" -"@types/testing-library__react@^9.1.2": - version "9.1.3" - resolved "https://registry.npmjs.org/@types/testing-library__react/-/testing-library__react-9.1.3.tgz#35eca61cc6ea923543796f16034882a1603d7302" - integrity sha512-iCdNPKU3IsYwRK9JieSYAiX0+aYDXOGAmrC/3/M7AqqSDKnWWVv07X+Zk1uFSL7cMTUYzv4lQRfohucEocn5/w== - dependencies: - "@types/react-dom" "*" - "@types/testing-library__dom" "*" - pretty-format "^25.1.0" - "@types/through@*": version "0.0.30" resolved "https://registry.npmjs.org/@types/through/-/through-0.0.30.tgz#e0e42ce77e897bd6aead6f6ea62aeb135b8a3895" @@ -4729,13 +4710,13 @@ aria-query@^3.0.0: ast-types-flow "0.0.7" commander "^2.11.0" -aria-query@^4.0.2: - version "4.0.2" - resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.0.2.tgz#250687b4ccde1ab86d127da0432ae3552fc7b145" - integrity sha512-S1G1V790fTaigUSM/Gd0NngzEfiMy9uTUfMyHhKhVyy4cH5O/eTuR01ydhGL0z4Za1PXFTRGH3qL8VhUQuEO5w== +aria-query@^4.2.2: + version "4.2.2" + resolved "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz#0d2ca6c9aceb56b8977e9fed6aed7e15bbd2f83b" + integrity sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA== dependencies: - "@babel/runtime" "^7.7.4" - "@babel/runtime-corejs3" "^7.7.4" + "@babel/runtime" "^7.10.2" + "@babel/runtime-corejs3" "^7.10.2" arr-diff@^4.0.0: version "4.0.0" @@ -7650,15 +7631,10 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" -dom-accessibility-api@^0.3.0: - version "0.3.0" - resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.3.0.tgz#511e5993dd673b97c87ea47dba0e3892f7e0c983" - integrity sha512-PzwHEmsRP3IGY4gv/Ug+rMeaTIyTJvadCb+ujYXYeIylbHJezIyNToe8KfEgHTCEYyC+/bUghYOGg8yMGlZ6vA== - -dom-accessibility-api@^0.4.2: - version "0.4.3" - resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.3.tgz#93ca9002eb222fd5a343b6e5e6b9cf5929411c4c" - integrity sha512-JZ8iPuEHDQzq6q0k7PKMGbrIdsgBB7TRrtVOUm4nSMCExlg5qQG4KXWTH2k90yggjM4tTumRGwTKJSldMzKyLA== +dom-accessibility-api@^0.4.5: + version "0.4.5" + resolved "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.4.5.tgz#d9c1cefa89f509d8cf132ab5d250004d755e76e3" + integrity sha512-HcPDilI95nKztbVikaN2vzwvmv0sE8Y2ZJFODy/m15n7mGXLeOKGiys9qWVbFbh+aq/KYj2lqMLybBOkYAEXqg== dom-converter@^0.2: version "0.2.0" @@ -19034,11 +19010,6 @@ w3c-xmlserializer@^2.0.0: dependencies: xml-name-validator "^3.0.0" -wait-for-expect@^3.0.2: - version "3.0.2" - resolved "https://registry.npmjs.org/wait-for-expect/-/wait-for-expect-3.0.2.tgz#d2f14b2f7b778c9b82144109c8fa89ceaadaa463" - integrity sha512-cfS1+DZxuav1aBYbaO/kE06EOS8yRw7qOFoD3XtjTkYvCvh3zUvNST8DXK/nPaeqIzIv3P3kL3lRJn8iwOiSag== - wait-on@4.0.0: version "4.0.0" resolved "https://registry.npmjs.org/wait-on/-/wait-on-4.0.0.tgz#4d7e4485ca759968897fd3b0cc50720c0b4ca959"