Merge branch 'master' of github.com:spotify/backstage into mob/job-processor

* 'master' of github.com:spotify/backstage: (53 commits)
  Updating FAQ.md related to issue #1441 (#1443)
  docs/auth: add some more information about Identities
  plugins/lighthouse: fix CreateAudit test selecting child of button
  app,cli/templates: install @types/react-dom in app for @testing-library/react
  packages: bump @testing-library packages to latest versions
  docs/auth: rename overview to README
  rollback package install
  fixes build errors
  update plugin template to not trigger the notice header warning
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  fixup
  ...
This commit is contained in:
blam
2020-06-25 04:54:24 +02:00
62 changed files with 591 additions and 369 deletions
+4 -3
View File
@@ -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",
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: experimental
owner: tools@example.com
owner: artists@example.com
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
owner: tools@example.com
owner: guest
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: experimental
owner: tools@example.com
owner: players@example.com
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
owner: tools@example.com
owner: guest
@@ -6,4 +6,4 @@ metadata:
spec:
type: service
lifecycle: production
owner: tools@example.com
owner: guest
@@ -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",
@@ -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": [
@@ -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": [
@@ -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';
+3 -3
View File
@@ -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",
@@ -173,7 +173,7 @@ export type ProfileInfo = {
/**
* Email ID.
*/
email: string;
email?: string;
/**
* Display name that can be presented to the user.
@@ -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<SessionState> {
return this.sessionStateTracker.observable;
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<GithubSession>) {}
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<BackstageIdentity | undefined> {
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<string> {
@@ -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<SessionState> {
return this.sessionStateTracker.observable;
return this.sessionManager.sessionState$();
}
constructor(private readonly sessionManager: SessionManager<GoogleSession>) {}
@@ -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<BackstageIdentity | undefined> {
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;
}
@@ -38,6 +38,7 @@ class LocalStorage {
class MockManager implements SessionManager<string> {
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();
});
});
@@ -82,6 +82,10 @@ export class AuthSessionStore<T> implements SessionManager<T> {
await this.manager.removeSession();
}
sessionState$() {
return this.manager.sessionState$();
}
private loadSession(): T | undefined {
try {
const sessionJson = localStorage.getItem(this.storageKey);
@@ -15,6 +15,7 @@
*/
import { RefreshingAuthSessionManager } from './RefreshingAuthSessionManager';
import { SessionState } from '../../apis';
const defaultOptions = {
sessionScopes: (session: { scopes: Set<string> }) => 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 },
@@ -22,6 +22,7 @@ import {
} from './types';
import { AuthConnector } from '../AuthConnector';
import { SessionScopeHelper, hasScopes } from './common';
import { SessionStateTracker } from './SessionStateTracker';
type Options<T> = {
/** The connector used for acting on the auth session */
@@ -43,6 +44,7 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
private readonly helper: SessionScopeHelper<T>;
private readonly sessionScopesFunc: SessionScopesFunc<T>;
private readonly sessionShouldRefreshFunc: SessionShouldRefreshFunc<T>;
private readonly stateTracker = new SessionStateTracker();
private refreshPromise?: Promise<T>;
private currentSession: T | undefined;
@@ -109,16 +111,18 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
...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<T> {
@@ -129,7 +133,9 @@ export class RefreshingAuthSessionManager<T> implements SessionManager<T> {
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;
}
@@ -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>(SessionState.SignedOut);
private readonly subject = new BehaviorSubject<SessionState>(
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<SessionState> {
return this.subject;
}
}
@@ -17,6 +17,7 @@
import { SessionManager, GetSessionOptions } from './types';
import { AuthConnector } from '../AuthConnector';
import { SessionScopeHelper } from './common';
import { SessionStateTracker } from './SessionStateTracker';
type Options<T> = {
/** The connector used for acting on the auth session */
@@ -33,6 +34,7 @@ type Options<T> = {
export class StaticAuthSessionManager<T> implements SessionManager<T> {
private readonly connector: AuthConnector<T>;
private readonly helper: SessionScopeHelper<T>;
private readonly stateTracker = new SessionStateTracker();
private currentSession: T | undefined;
@@ -60,11 +62,17 @@ export class StaticAuthSessionManager<T> implements SessionManager<T> {
...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$();
}
}
@@ -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<T> = {
getSession(options: GetSessionOptions): Promise<T | undefined>;
removeSession(): Promise<void>;
sessionState$(): Observable<SessionState>;
};
/**
+3 -3
View File
@@ -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",
@@ -34,14 +34,16 @@ export const UserProfile: FC<{ open: boolean; setOpen: Function }> = ({
}) => {
const ref = useRef<Element>(); // 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 = () => (
<Avatar src={profile.picture} className={classes.avatar}>
{displayName[0]}
+3 -3
View File
@@ -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",
+2 -2
View File
@@ -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"
+3 -3
View File
@@ -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",