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
+35
View File
@@ -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
+3 -10
View File
@@ -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?
+86
View File
@@ -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.
-44
View File
@@ -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.
+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",
@@ -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',
},
});
+21 -19
View File
@@ -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<unknown> = {
...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<unknown> = {
...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,
@@ -57,10 +57,6 @@ export const makeProfileInfo = (
}
}
if (!email) {
throw new Error('No email received in profile info');
}
return {
email,
picture,
@@ -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<OAuthResponse>(
async handler(req: express.Request) {
const { response } = await executeFrameHandlerStrategy<OAuthResponse>(
req,
this._strategy,
);
return {
response: result.response,
};
return { response };
}
}
@@ -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<OAuthResponse> {
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 } };
}
}
+12 -9
View File
@@ -100,14 +100,20 @@ export interface OAuthProviderHandlers {
*/
handler(
req: express.Request,
): Promise<{ response: OAuthResponse; refreshToken?: string }>;
): Promise<{
response: AuthResponse<OAuthProviderInfo>;
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<OAuthResponse>;
refresh?(
refreshToken: string,
scope: string,
): Promise<AuthResponse<OAuthProviderInfo>>;
/**
* (Optional) Sign out of the auth provider.
@@ -192,13 +198,10 @@ export type AuthProviderFactory = (
export type AuthResponse<ProviderInfo> = {
providerInfo: ProviderInfo;
profile: ProfileInfo;
backstageIdentity: BackstageIdentity;
backstageIdentity?: BackstageIdentity;
};
export type OAuthResponse = Omit<
AuthResponse<OAuthProviderInfo>,
'backstageIdentity'
>;
export type OAuthResponse = AuthResponse<OAuthProviderInfo>;
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.
*/
+3 -4
View File
@@ -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",
+3 -4
View File
@@ -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": [
+3 -4
View File
@@ -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": [
+3 -4
View File
@@ -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": [
+3 -4
View File
@@ -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"
},
+3 -4
View File
@@ -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": [
@@ -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();
});
});
+3 -4
View File
@@ -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": [
+1 -1
View File
@@ -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",
+3 -4
View File
@@ -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": [
+3 -4
View File
@@ -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": [
+3 -4
View File
@@ -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": [
+5 -3
View File
@@ -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.
+11 -12
View File
@@ -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
@@ -0,0 +1,2 @@
.tox
*.egg-info
@@ -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.
@@ -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
@@ -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
@@ -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
@@ -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.
+1 -1
View File
@@ -2,7 +2,7 @@ site_name: 'mock-docs'
nav:
- Home: index.md
- SubDocs: '!include ./sub-docs/mkdocs.yml'
plugins:
- techdocs-core
@@ -0,0 +1 @@
### This is an md file in another docs folder using the [MkDocs Monorepo Plugin](https://github.com/spotify/mkdocs-monorepo-plugin)
@@ -0,0 +1,4 @@
site_name: subdocs
nav:
- Home 2: "index.md"
+3 -4
View File
@@ -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": [
+3 -4
View File
@@ -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": [
+57 -86
View File
@@ -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"