diff --git a/README.md b/README.md index 867d857fa4..983a8efea4 100644 --- a/README.md +++ b/README.md @@ -16,19 +16,19 @@ For more information go to [backstage.io](https://backstage.io) or join our [Dis ### Features -- Create and manage all of your organization’s software and microservices in one place -- Services catalog keeps track of all software and its ownership -- Visualizations provide information about your backend services and tooling, and help you monitor them -- A unified method for managing microservices offers both visibility and control -- Preset templates allow engineers to quickly create microservices in a standardized way ([coming soon](https://github.com/spotify/backstage/milestone/11)) -- Centralized, full-featured technical documentation with integrated tooling that makes it easy for developers to set up, publish, and maintain alongside their code ([coming soon](https://github.com/spotify/backstage/milestone/15)) +- Create and manage all of your organization’s software and microservices in one place. +- Services catalog keeps track of all software and its ownership. +- Visualizations provide information about your backend services and tooling, and help you monitor them. +- A unified method for managing microservices offers both visibility and control. +- Preset templates allow engineers to quickly create microservices in a standardized way ([coming soon](https://github.com/spotify/backstage/milestone/11)). +- Centralized, full-featured technical documentation with integrated tooling that makes it easy for developers to set up, publish, and maintain alongside their code ([coming soon](https://github.com/spotify/backstage/milestone/15)). ### Benefits -- For engineering managers, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification. -- For end users (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation. -- For platform engineers, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones. -- For everyone, it’s a single, consistent experience that ties all your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place. +- For _engineering managers_, it allows you to maintain standards and best practices across the organization, and can help you manage your whole tech ecosystem, from migrations to test certification. +- For _end users_ (developers), it makes it fast and simple to build software components in a standardized way, and it provides a central place to manage all projects and documentation. +- For _platform engineers_, it enables extensibility and scalability by letting you easily integrate new tools and services (via plugins), as well as extending the functionality of existing ones. +- For _everyone_, it’s a single, consistent experience that ties all your infrastructure tooling, resources, standards, owners, contributors, and administrators together in one place. ## Backstage Service Catalog (alpha) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index ccaeb2828b..3c8054d995 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -31,7 +31,10 @@ const app = createApp({ plugins: Object.values(plugins), components: { SignInPage: props => ( - + ), }, }); diff --git a/packages/core/src/layout/SignInPage/gitlabProvider.tsx b/packages/core/src/layout/SignInPage/gitlabProvider.tsx new file mode 100644 index 0000000000..5e417cb32d --- /dev/null +++ b/packages/core/src/layout/SignInPage/gitlabProvider.tsx @@ -0,0 +1,88 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { Grid, Typography, Button } from '@material-ui/core'; +import { InfoCard } from '../InfoCard/InfoCard'; +import { ProviderComponent, ProviderLoader, SignInProvider } from './types'; +import { useApi, gitlabAuthApiRef, errorApiRef } from '@backstage/core-api'; + +const Component: ProviderComponent = ({ onResult }) => { + const gitlabAuthApi = useApi(gitlabAuthApiRef); + const errorApi = useApi(errorApiRef); + + const handleLogin = async () => { + try { + const identity = await gitlabAuthApi.getBackstageIdentity({ + instantPopup: true, + }); + + const profile = await gitlabAuthApi.getProfile(); + onResult({ + userId: identity!.id, + profile: profile!, + getIdToken: () => + gitlabAuthApi.getBackstageIdentity().then(i => i!.idToken), + logout: async () => { + await gitlabAuthApi.logout(); + }, + }); + } catch (error) { + errorApi.post(error); + } + }; + + return ( + + + Sign In + + } + > + Sign In using Gitlab + + + ); +}; + +const loader: ProviderLoader = async apis => { + const gitlabAuthApi = apis.get(gitlabAuthApiRef)!; + + const identity = await gitlabAuthApi.getBackstageIdentity({ + optional: true, + }); + + if (!identity) { + return undefined; + } + + const profile = await gitlabAuthApi.getProfile(); + + return { + userId: identity.id, + profile: profile!, + getIdToken: () => + gitlabAuthApi.getBackstageIdentity().then(i => i!.idToken), + logout: async () => { + await gitlabAuthApi.logout(); + }, + }; +}; + +export const gitlabProvider: SignInProvider = { Component, loader }; diff --git a/packages/core/src/layout/SignInPage/providers.tsx b/packages/core/src/layout/SignInPage/providers.tsx index a195b2042a..53d6b75eac 100644 --- a/packages/core/src/layout/SignInPage/providers.tsx +++ b/packages/core/src/layout/SignInPage/providers.tsx @@ -18,6 +18,7 @@ import React, { useLayoutEffect, useState, useMemo, useCallback } from 'react'; import { guestProvider } from './guestProvider'; import { googleProvider } from './googleProvider'; import { customProvider } from './customProvider'; +import { gitlabProvider } from './gitlabProvider'; import { oktaProvider } from './oktaProvider'; import { SignInPageProps, @@ -31,11 +32,17 @@ import { SignInProvider } from './types'; const PROVIDER_STORAGE_KEY = '@backstage/core:SignInPage:provider'; // Separate list here to avoid exporting internal types -export type SignInProviderId = 'guest' | 'google' | 'custom' | 'okta'; +export type SignInProviderId = + | 'guest' + | 'google' + | 'gitlab' + | 'custom' + | 'okta'; const signInProviders: { [id in SignInProviderId]: SignInProvider } = { guest: guestProvider, google: googleProvider, + gitlab: gitlabProvider, custom: customProvider, okta: oktaProvider, }; diff --git a/plugins/auth-backend/src/providers/gitlab/provider.test.ts b/plugins/auth-backend/src/providers/gitlab/provider.test.ts index 5eec6c47f9..d4d1de17ec 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.test.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.test.ts @@ -41,6 +41,9 @@ describe('GitlabAuthProvider', () => { }, }, expect: { + backstageIdentity: { + id: 'jimmymarkum', + }, providerInfo: { accessToken: '19xasczxcm9n7gacn9jdgm19me', expiresInSeconds: 100, @@ -74,6 +77,9 @@ describe('GitlabAuthProvider', () => { }, }, expect: { + backstageIdentity: { + id: 'daveboyle', + }, providerInfo: { accessToken: 'ajakljsdoiahoawxbrouawucmbawe.awkxjemaneasdxwe.sodijxqeqwexeqwxe', diff --git a/plugins/auth-backend/src/providers/gitlab/provider.ts b/plugins/auth-backend/src/providers/gitlab/provider.ts index 890fbb3531..950c414750 100644 --- a/plugins/auth-backend/src/providers/gitlab/provider.ts +++ b/plugins/auth-backend/src/providers/gitlab/provider.ts @@ -78,6 +78,14 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { idToken: params.id_token, }; + // gitlab provides an id numeric value (123) + // as a fallback + let id = passportProfile!.id; + + if (profile.email) { + id = profile.email.split('@')[0]; + } + if (params.expires_in) { providerInfo.expiresInSeconds = params.expires_in; } @@ -87,6 +95,9 @@ export class GitlabAuthProvider implements OAuthProviderHandlers { return { providerInfo, profile, + backstageIdentity: { + id, + }, }; } diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index 42be2732d0..1975d350e1 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -27,6 +27,7 @@ import transformer, { addLinkClickListener, removeMkdocsHeader, modifyCss, + onCssReady, } from '../transformers'; import { docStorageURL } from '../../config'; import URLFormatter from '../urlFormatter'; @@ -133,16 +134,25 @@ export const Reader = () => { shadowRoot?.querySelector(parsedUrl.hash)?.scrollIntoView(); }, }), + onCssReady({ + docStorageURL, + onLoading: (dom: Element) => { + (dom as HTMLElement).style.setProperty('opacity', '0'); + }, + onLoaded: (dom: Element) => { + (dom as HTMLElement).style.removeProperty('opacity'); + }, + }), ]); }, [componentId, path, shadowRoot, state]); // eslint-disable-line react-hooks/exhaustive-deps - if (state.value instanceof Error) return ; + if (state.value instanceof Error) { + return ; + } return ( - <> - -
- - + +
+ ); }; diff --git a/plugins/techdocs/src/reader/transformers/index.ts b/plugins/techdocs/src/reader/transformers/index.ts index f35d6aa478..4e12dcae48 100644 --- a/plugins/techdocs/src/reader/transformers/index.ts +++ b/plugins/techdocs/src/reader/transformers/index.ts @@ -19,6 +19,7 @@ export * from './rewriteDocLinks'; export * from './addLinkClickListener'; export * from './removeMkdocsHeader'; export * from './modifyCss'; +export * from './onCssReady'; export type Transformer = (dom: Element) => Element; diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.test.ts b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts new file mode 100644 index 0000000000..10edbbae5e --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/onCssReady.test.ts @@ -0,0 +1,87 @@ +/* + * 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 { + FIXTURES, + createTestShadowDom, + mockStylesheetEventListener, + executeStylesheetEventListeners, + clearStylesheetEventListeners, +} from '../../test-utils'; +import { addBaseUrl, onCssReady } from '../transformers'; + +const docStorageURL: string = + 'https://techdocs-mock-sites.storage.googleapis.com'; + +jest.useFakeTimers(); + +describe('onCssReady', () => { + beforeEach(() => { + mockStylesheetEventListener(100); + }); + + afterEach(() => { + clearStylesheetEventListeners(); + }); + + it('does not call onLoading and onLoaded without the addBaseUrl transformer', () => { + const onLoading = jest.fn(); + const onLoaded = jest.fn(); + + createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + transformers: [ + onCssReady({ + docStorageURL, + onLoading, + onLoaded, + }), + ], + }); + + expect(onLoading).not.toHaveBeenCalled(); + executeStylesheetEventListeners(); + expect(onLoaded).not.toHaveBeenCalled(); + }); + + it('calls the onLoading and onLoaded correctly', () => { + const onLoading = jest.fn(); + const onLoaded = jest.fn(); + + createTestShadowDom(FIXTURES.FIXTURE_STANDARD_PAGE, { + transformers: [ + addBaseUrl({ + docStorageURL, + componentId: 'mkdocs', + path: '', + }), + onCssReady({ + docStorageURL, + onLoading, + onLoaded, + }), + ], + }); + + expect(onLoading).toHaveBeenCalledTimes(1); + expect(onLoading).toHaveBeenCalledWith(expect.any(Element)); + expect(onLoaded).not.toHaveBeenCalled(); + + executeStylesheetEventListeners(); + + expect(onLoaded).toHaveBeenCalledTimes(1); + expect(onLoaded).toHaveBeenCalledWith(expect.any(Element)); + }); +}); diff --git a/plugins/techdocs/src/reader/transformers/onCssReady.ts b/plugins/techdocs/src/reader/transformers/onCssReady.ts new file mode 100644 index 0000000000..2d355574b6 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/onCssReady.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import type { Transformer } from './index'; + +type OnCssReadyOptions = { + docStorageURL: string; + onLoading: (dom: Element) => void; + onLoaded: (dom: Element) => void; +}; + +export const onCssReady = ({ + docStorageURL, + onLoading, + onLoaded, +}: OnCssReadyOptions): Transformer => { + return dom => { + const cssPages = Array.from( + dom.querySelectorAll('head > link[rel="stylesheet"]'), + ).filter(elem => elem.getAttribute('href')?.startsWith(docStorageURL)); + + let count = cssPages.length; + + if (count > 0) { + onLoading(dom); + } + + cssPages.forEach(cssPage => + cssPage.addEventListener('load', () => { + count -= 1; + + if (count === 0) { + onLoaded(dom); + } + }), + ); + + return dom; + }; +}; diff --git a/plugins/techdocs/src/test-utils/index.ts b/plugins/techdocs/src/test-utils/index.ts index f9ced5f930..d782f225d5 100644 --- a/plugins/techdocs/src/test-utils/index.ts +++ b/plugins/techdocs/src/test-utils/index.ts @@ -15,49 +15,10 @@ */ import FIXTURE_STANDARD_PAGE from './fixtures/mkdocs-index'; -import transformer from '../reader/transformers'; -import type { Transformer } from '../reader/transformers'; export const FIXTURES = { FIXTURE_STANDARD_PAGE, }; -export type CreateTestShadowDomOptions = { - transformers: Transformer[]; -}; - -export const createTestShadowDom = ( - fixture: string, - opts: CreateTestShadowDomOptions = { transformers: [] }, -): ShadowRoot => { - const divElement = document.createElement('div'); - divElement.attachShadow({ mode: 'open' }); - document.body.appendChild(divElement); - - const domParser = new DOMParser().parseFromString(fixture, 'text/html'); - divElement.shadowRoot?.appendChild(domParser.documentElement); - - if (opts.transformers) { - transformer(divElement.shadowRoot!.children[0], opts.transformers); - } - - return divElement.shadowRoot!; -}; - -export const getSample = ( - shadowDom: ShadowRoot, - elementName: string, - elementAttribute: string, - sampleSize = 2, -) => { - const rootElement = shadowDom.children[0]; - - return Array.from(rootElement.getElementsByTagName(elementName)) - .filter(elem => { - return elem.hasAttribute(elementAttribute); - }) - .slice(0, sampleSize) - .map(elem => { - return elem.getAttribute(elementAttribute); - }); -}; +export * from './stylesheets'; +export * from './shadowDom'; diff --git a/plugins/techdocs/src/test-utils/shadowDom.ts b/plugins/techdocs/src/test-utils/shadowDom.ts new file mode 100644 index 0000000000..ded6b6a860 --- /dev/null +++ b/plugins/techdocs/src/test-utils/shadowDom.ts @@ -0,0 +1,58 @@ +/* + * 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 transformer from '../reader/transformers'; +import type { Transformer } from '../reader/transformers'; + +export type CreateTestShadowDomOptions = { + transformers: Transformer[]; +}; + +export const createTestShadowDom = ( + fixture: string, + opts: CreateTestShadowDomOptions = { transformers: [] }, +): ShadowRoot => { + const divElement = document.createElement('div'); + divElement.attachShadow({ mode: 'open' }); + document.body.appendChild(divElement); + + const domParser = new DOMParser().parseFromString(fixture, 'text/html'); + divElement.shadowRoot?.appendChild(domParser.documentElement); + + if (opts.transformers) { + transformer(divElement.shadowRoot!.children[0], opts.transformers); + } + + return divElement.shadowRoot!; +}; + +export const getSample = ( + shadowDom: ShadowRoot, + elementName: string, + elementAttribute: string, + sampleSize = 2, +) => { + const rootElement = shadowDom.children[0]; + + return Array.from(rootElement.getElementsByTagName(elementName)) + .filter(elem => { + return elem.hasAttribute(elementAttribute); + }) + .slice(0, sampleSize) + .map(elem => { + return elem.getAttribute(elementAttribute); + }); +}; diff --git a/plugins/techdocs/src/test-utils/stylesheets.ts b/plugins/techdocs/src/test-utils/stylesheets.ts new file mode 100644 index 0000000000..0532c71272 --- /dev/null +++ b/plugins/techdocs/src/test-utils/stylesheets.ts @@ -0,0 +1,35 @@ +/* + * Copyright 2020 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +export const mockStylesheetEventListener = (timeToCallbackMs: number): void => { + HTMLLinkElement.prototype.addEventListener = ( + _eventName: string, + eventCallback: any, + ) => { + setTimeout(() => { + eventCallback(); + }, timeToCallbackMs); + }; +}; + +export const executeStylesheetEventListeners = (): void => { + jest.runOnlyPendingTimers(); +}; + +export const clearStylesheetEventListeners = (): void => { + HTMLLinkElement.prototype.addEventListener = + Element.prototype.addEventListener; +};