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;
+};