diff --git a/.changeset/four-hounds-own.md b/.changeset/four-hounds-own.md new file mode 100644 index 0000000000..28eeb89f83 --- /dev/null +++ b/.changeset/four-hounds-own.md @@ -0,0 +1,8 @@ +--- +'@backstage/plugin-techdocs': minor +'@backstage/plugin-techdocs-backend': minor +--- + +When newer documentation available but not built, show older documentation while async building newer +TechDocs backend: /sync endpoint added to support above, returns immediate success if docs don't need a build, returns delayed success after build if needed +TechDocs backend: /docs endpoint removed as frontend can directly request to techdocs.storageUrl or /static/docs diff --git a/plugins/techdocs-backend/src/DocsBuilder/builder.ts b/plugins/techdocs-backend/src/DocsBuilder/builder.ts index 5d8ee3612d..9dd53f159e 100644 --- a/plugins/techdocs-backend/src/DocsBuilder/builder.ts +++ b/plugins/techdocs-backend/src/DocsBuilder/builder.ts @@ -101,7 +101,7 @@ export class DocsBuilder { } catch (err) { // Proceed with a fresh build this.logger.warn( - `Unable to read techdocs_metadata.json, error ${err}.`, + `Unable to read techdocs_metadata.json, proceeding with fresh build, error ${err}.`, ); } } diff --git a/plugins/techdocs-backend/src/service/router.ts b/plugins/techdocs-backend/src/service/router.ts index 4b10063e03..9456e8a277 100644 --- a/plugins/techdocs-backend/src/service/router.ts +++ b/plugins/techdocs-backend/src/service/router.ts @@ -31,6 +31,7 @@ import { Logger } from 'winston'; import { DocsBuilder } from '../DocsBuilder'; import { shouldCheckForUpdate } from '../DocsBuilder/BuildMetadataStorage'; import { getEntityNameFromUrlPath } from './helpers'; +import { NotFoundError } from '@backstage/errors'; type RouterOptions = { preparers: PreparerBuilder; @@ -105,12 +106,11 @@ export async function createRouter({ } }); - router.get('/docs/:namespace/:kind/:name/*', async (req, res) => { + // Check if docs are the latest version and trigger rebuilds if not + // Responds with immediate success if rebuild not needed + // If a build is required, responds with a success when finished + router.get('/sync/:namespace/:kind/:name', async (req, res) => { const { kind, namespace, name } = req.params; - const storageUrl = - config.getOptionalString('techdocs.storageUrl') ?? - `${await discovery.getExternalBaseUrl('techdocs')}/static/docs`; - const catalogUrl = await discovery.getBaseUrl('catalog'); const triple = [kind, namespace, name].map(encodeURIComponent).join('/'); @@ -127,6 +127,16 @@ export async function createRouter({ const entity: Entity = await catalogRes.json(); + if (!entity.metadata.uid) { + throw new NotFoundError('Entity metadata UID missing'); + } + if (!shouldCheckForUpdate(entity.metadata.uid)) { + res.status(200).json({ + message: `Last check for documentation update is recent, did not retry.`, + }); + return; + } + let publisherType = ''; try { publisherType = config.getString('techdocs.publisher.type'); @@ -137,63 +147,61 @@ export async function createRouter({ 'https://backstage.io/docs/features/techdocs/architecture', ); } - // techdocs-backend will only try to build documentation for an entity if techdocs.builder is set to 'local' - // If set to 'external', it will only try to fetch and assume that an external process (e.g. CI/CD pipeline - // of the repository) is responsible for building and publishing documentation to the storage provider. - if ( - config.getString('techdocs.builder') === 'local' && - entity.metadata.uid && - shouldCheckForUpdate(entity.metadata.uid) - ) { - const docsBuilder = new DocsBuilder({ - preparers, - generators, - publisher, - dockerClient, - logger, - entity, + // If set to 'external', it will assume that an external process (e.g. CI/CD pipeline + // of the repository) is responsible for building and publishing documentation to the storage provider + if (config.getString('techdocs.builder') !== 'local') { + res.status(200).json({ + message: + '`techdocs.builder` app config is not set to `local`, so docs will not be generated locally and sync is not required.', }); - let foundDocs = false; - switch (publisherType) { - case 'local': - case 'awsS3': - case 'azureBlobStorage': - case 'openStackSwift': - case 'googleGcs': - // This block should be valid for all storage implementations. So no need to duplicate in future, - // add the publisher type in the list here. - await docsBuilder.build(); - // With a maximum of ~5 seconds wait, check if the files got published and if docs will be fetched - // on the user's page. If not, respond with a message asking them to check back later. - // The delay here is to make sure GCS/AWS/etc. registers newly uploaded files which is usually <1 second - for (let attempt = 0; attempt < 5; attempt++) { - if (await publisher.hasDocsBeenGenerated(entity)) { - foundDocs = true; - break; - } - await new Promise(r => setTimeout(r, 1000)); - } - if (!foundDocs) { - logger.error( - 'Published files are taking longer to show up in storage. Something went wrong.', - ); - res.status(408).json({ - error: - 'Sorry! It took too long for the generated docs to show up in storage. Check back later.', - }); - return; - } - break; - default: - res.status(400).json({ - error: `Publisher type ${publisherType} is not supported by techdocs-backend docs builder.`, - }); - break; - } + return; + } + const docsBuilder = new DocsBuilder({ + preparers, + generators, + publisher, + dockerClient, + logger, + entity, + }); + let foundDocs = false; + switch (publisherType) { + case 'local': + case 'awsS3': + case 'azureBlobStorage': + case 'openStackSwift': + case 'googleGcs': + // This block should be valid for all storage implementations. So no need to duplicate in future, + // add the publisher type in the list here. + await docsBuilder.build(); + // With a maximum of ~5 seconds wait, check if the files got published and if docs will be fetched + // on the user's page. If not, respond with a message asking them to check back later. + // The delay here is to make sure GCS/AWS/etc. registers newly uploaded files which is usually <1 second + for (let attempt = 0; attempt < 5; attempt++) { + if (await publisher.hasDocsBeenGenerated(entity)) { + foundDocs = true; + break; + } + await new Promise(r => setTimeout(r, 1000)); + } + if (!foundDocs) { + logger.error( + 'Published files are taking longer to show up in storage. Something went wrong.', + ); + throw new NotFoundError( + 'Sorry! It took too long for the generated docs to show up in storage. Check back later.', + ); + } + res + .status(201) + .json({ message: 'Docs updated or did not need updating' }); + break; + default: + throw new NotFoundError( + `Publisher type ${publisherType} is not supported by techdocs-backend docs builder.`, + ); } - - res.redirect(`${storageUrl}${req.path.replace('/docs', '')}`); }); // Route middleware which serves files from the storage set in the publisher. diff --git a/plugins/techdocs/dev/api.ts b/plugins/techdocs/dev/api.ts index 5fa494a973..7acf3c7032 100644 --- a/plugins/techdocs/dev/api.ts +++ b/plugins/techdocs/dev/api.ts @@ -17,6 +17,7 @@ import { DiscoveryApi, IdentityApi } from '@backstage/core'; import { Config } from '@backstage/config'; import { EntityName } from '@backstage/catalog-model'; import { TechDocsStorage } from '../src/api'; +import { NotFoundError } from '@backstage/errors'; export class TechDocsDevStorageApi implements TechDocsStorage { public configApi: Config; @@ -44,11 +45,29 @@ export class TechDocsDevStorageApi implements TechDocsStorage { ); } - async getEntityDocs(entityId: EntityName, path: string) { - const { name } = entityId; + async getStorageUrl() { + return ( + this.configApi.getOptionalString('techdocs.storageUrl') ?? + `${await this.discoveryApi.getBaseUrl('techdocs')}/static/docs` + ); + } - const apiOrigin = await this.getApiOrigin(); - const url = `${apiOrigin}/${name}/${path}`; + async getBuilder() { + return this.configApi.getString('techdocs.builder'); + } + + async fetchUrl(url: string) { + const token = await this.identityApi.getIdToken(); + return fetch(url, { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + } + + async getEntityDocs(entityId: EntityName, path: string) { + const { kind, namespace, name } = entityId; + + const storageUrl = await this.getStorageUrl(); + const url = `${storageUrl}/${namespace}/${kind}/${name}/${path}`; const token = await this.identityApi.getIdToken(); const request = await fetch( @@ -58,13 +77,66 @@ export class TechDocsDevStorageApi implements TechDocsStorage { }, ); - if (request.status === 404) { - throw new Error('Page not found'); + let errorMessage = ''; + switch (request.status) { + case 404: + errorMessage = 'Page not found. '; + // path is empty for the home page of an entity's docs site + if (!path) { + errorMessage += + 'This could be because there is no index.md file in the root of the docs directory of this repository.'; + } + throw new NotFoundError(errorMessage); + case 500: + errorMessage = + 'Could not generate documentation or an error in the TechDocs backend. '; + throw new Error(errorMessage); + default: + // Do nothing + break; } return request.text(); } + /** + * Check if docs are the latest version and trigger rebuilds if not + * + * @param {EntityName} entityId Object containing entity data like name, namespace, etc. + * @returns {boolean} Whether documents are currently synchronized to newest version + * @throws {Error} Throws error on error from sync endpoint + */ + async syncEntityDocs(entityId: EntityName) { + const { kind, namespace, name } = entityId; + + const apiOrigin = await this.getApiOrigin(); + const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`; + let request; + let attempts: number = 0; + // retry if request times out, up to 5 times + // can happen due to docs taking too long to generate + while (!request || (request.status === 408 && attempts < 5)) { + attempts++; + request = await this.fetchUrl( + `${url.endsWith('/') ? url : `${url}/`}index.html`, + ); + } + + switch (request.status) { + case 404: + throw (await request.json()).error; + case 200: + case 201: + return true; + // for timeout and misc errors, handle without error to allow viewing older docs + // if older docs not available, + // Reader will show 404 error coming from getEntityDocs + case 408: + default: + return false; + } + } + async getBaseUrl( oldBaseUrl: string, entityId: EntityName, diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index 9951dccded..42e2ac0b6e 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -38,10 +38,10 @@ "@backstage/test-utils": "^0.1.9", "@backstage/theme": "^0.2.4", "@backstage/techdocs-common": "^0.4.5", + "@backstage/errors": "^0.1.1", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", - "@types/react": "^16.9", "react": "^16.13.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", @@ -56,8 +56,10 @@ "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", + "@types/react": "^16.9", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", + "@types/react": "^16.9", "canvas": "^2.6.1", "cross-fetch": "^3.0.6", "msw": "^0.21.2" diff --git a/plugins/techdocs/src/api.test.ts b/plugins/techdocs/src/api.test.ts index 734650d7c8..2260cc7d39 100644 --- a/plugins/techdocs/src/api.test.ts +++ b/plugins/techdocs/src/api.test.ts @@ -37,7 +37,7 @@ describe('TechDocsStorageApi', () => { await expect( storageApi.getBaseUrl('test.js', mockEntity, ''), ).resolves.toEqual( - `${mockBaseUrl}/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test.js`, + `${mockBaseUrl}/static/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test.js`, ); }); @@ -48,7 +48,7 @@ describe('TechDocsStorageApi', () => { await expect( storageApi.getBaseUrl('test/', mockEntity, ''), ).resolves.toEqual( - `${mockBaseUrl}/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test/`, + `${mockBaseUrl}/static/docs/${mockEntity.namespace}/${mockEntity.kind}/${mockEntity.name}/test/`, ); }); }); diff --git a/plugins/techdocs/src/api.ts b/plugins/techdocs/src/api.ts index 6de94b63bb..0be530cc08 100644 --- a/plugins/techdocs/src/api.ts +++ b/plugins/techdocs/src/api.ts @@ -18,6 +18,7 @@ import { createApiRef, DiscoveryApi, IdentityApi } from '@backstage/core'; import { Config } from '@backstage/config'; import { EntityName } from '@backstage/catalog-model'; import { TechDocsMetadata } from './types'; +import { NotFoundError } from '@backstage/errors'; export const techdocsStorageApiRef = createApiRef({ id: 'plugin.techdocs.storageservice', @@ -31,6 +32,7 @@ export const techdocsApiRef = createApiRef({ export interface TechDocsStorage { getEntityDocs(entityId: EntityName, path: string): Promise; + syncEntityDocs(entityId: EntityName): Promise; getBaseUrl( oldBaseUrl: string, entityId: EntityName, @@ -153,6 +155,17 @@ export class TechDocsStorageApi implements TechDocsStorage { ); } + async getStorageUrl() { + return ( + this.configApi.getOptionalString('techdocs.storageUrl') ?? + `${await this.discoveryApi.getBaseUrl('techdocs')}/static/docs` + ); + } + + async getBuilder() { + return this.configApi.getString('techdocs.builder'); + } + /** * Fetch HTML content as text for an individual docs page in an entity's docs site. * @@ -164,8 +177,8 @@ export class TechDocsStorageApi implements TechDocsStorage { async getEntityDocs(entityId: EntityName, path: string) { const { kind, namespace, name } = entityId; - const apiOrigin = await this.getApiOrigin(); - const url = `${apiOrigin}/docs/${namespace}/${kind}/${name}/${path}`; + const storageUrl = await this.getStorageUrl(); + const url = `${storageUrl}/${namespace}/${kind}/${name}/${path}`; const token = await this.identityApi.getIdToken(); const request = await fetch( @@ -184,7 +197,7 @@ export class TechDocsStorageApi implements TechDocsStorage { errorMessage += 'This could be because there is no index.md file in the root of the docs directory of this repository.'; } - throw new Error(errorMessage); + throw new NotFoundError(errorMessage); case 500: errorMessage = 'Could not generate documentation or an error in the TechDocs backend. '; @@ -197,6 +210,46 @@ export class TechDocsStorageApi implements TechDocsStorage { return request.text(); } + /** + * Check if docs are on the latest version and trigger rebuild if not + * + * @param {EntityName} entityId Object containing entity data like name, namespace, etc. + * @returns {boolean} Whether documents are currently synchronized to newest version + * @throws {Error} Throws error on error from sync endpoint in Techdocs Backend + */ + async syncEntityDocs(entityId: EntityName) { + const { kind, namespace, name } = entityId; + + const apiOrigin = await this.getApiOrigin(); + const url = `${apiOrigin}/sync/${namespace}/${kind}/${name}`; + const token = await this.identityApi.getIdToken(); + let request; + let attempts: number = 0; + // retry if request times out, up to 5 times + // can happen due to docs taking too long to generate + while (!request || (request.status === 408 && attempts < 5)) { + attempts++; + request = await fetch(url, { + headers: token ? { Authorization: `Bearer ${token}` } : {}, + }); + } + + switch (request.status) { + case 404: + throw new NotFoundError((await request.json()).error); + case 200: + case 201: + case 304: + return true; + // for timeout and misc errors, handle without error to allow viewing older docs + // if older docs not available, + // Reader will show 404 error coming from getEntityDocs + case 408: + default: + return false; + } + } + async getBaseUrl( oldBaseUrl: string, entityId: EntityName, @@ -205,10 +258,9 @@ export class TechDocsStorageApi implements TechDocsStorage { const { kind, namespace, name } = entityId; const apiOrigin = await this.getApiOrigin(); - return new URL( oldBaseUrl, - `${apiOrigin}/docs/${namespace}/${kind}/${name}/${path}`, + `${apiOrigin}/static/docs/${namespace}/${kind}/${name}/${path}`, ).toString(); } } diff --git a/plugins/techdocs/src/reader/components/Reader.tsx b/plugins/techdocs/src/reader/components/Reader.tsx index dc76aa3fdd..b5186f70b2 100644 --- a/plugins/techdocs/src/reader/components/Reader.tsx +++ b/plugins/techdocs/src/reader/components/Reader.tsx @@ -17,11 +17,15 @@ import { EntityName } from '@backstage/catalog-model'; import { useApi } from '@backstage/core'; import { BackstageTheme } from '@backstage/theme'; import { useTheme } from '@material-ui/core'; +<<<<<<< HEAD import React, { useEffect, useState } from 'react'; +======= +>>>>>>> 3ab0f7d4f (TechDocs: show outdated docs and asnyc build new) import { useNavigate, useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; +import React, { useEffect, useRef, useState } from 'react'; +import { Alert } from '@material-ui/lab'; import { techdocsStorageApiRef } from '../../api'; -import { useShadowDom } from '../hooks'; import transformer, { addBaseUrl, addLinkClickListener, @@ -46,13 +50,45 @@ export const Reader = ({ entityId, onReady }: Props) => { const theme = useTheme(); const techdocsStorageApi = useApi(techdocsStorageApiRef); +<<<<<<< HEAD const [shadowDomRef, shadowRoot] = useShadowDom(); const [sidebars, setSidebars] = useState(); +======= +>>>>>>> 3ab0f7d4f (TechDocs: show outdated docs and asnyc build new) const navigate = useNavigate(); + const shadowDomRef = useRef(null); + const [loadedPath, setLoadedPath] = useState(''); + const [atInitialLoad, setAtInitialLoad] = useState(true); + const [newerDocsExist, setNewerDocsExist] = useState(false); - const { value, loading, error } = useAsync(async () => { + const { + value: isSynced, + loading: syncInProgress, + error: syncError, + } = useAsync(async () => { + // Attempt to sync only if `techdocs.builder` in app config is set to 'local' + if ((await techdocsStorageApi.getBuilder()) !== 'local') { + return Promise.resolve({ + value: true, + loading: null, + error: null, + }); + } + return techdocsStorageApi.syncEntityDocs({ kind, namespace, name }); + }); + + const { + value: rawPage, + loading: docLoading, + error: docLoadError, + } = useAsync(async () => { + // do not automatically load same page again if URL has not changed, + // happens when generating new docs finishes + if (newerDocsExist && path === loadedPath) { + return null; + } return techdocsStorageApi.getEntityDocs({ kind, namespace, name }, path); - }, [techdocsStorageApi, kind, namespace, name, path]); + }, [techdocsStorageApi, kind, namespace, name, path, isSynced]); useEffect(() => { const updateSidebarPosition = () => { @@ -75,15 +111,36 @@ export const Reader = ({ entityId, onReady }: Props) => { }; }, [shadowDomRef, shadowRoot, sidebars]); - React.useEffect(() => { - if (!shadowRoot || loading || error) { - return; // Shadow DOM isn't ready / It's not ready / Docs was not found + useEffect(() => { + if (rawPage) { + setLoadedPath(path); + } + }, [rawPage, path]); + + useEffect(() => { + if (atInitialLoad === false) { + return; + } + setTimeout(() => { + setAtInitialLoad(false); + }, 5000); + }); + + useEffect(() => { + if (!atInitialLoad && !!rawPage && syncInProgress) { + setNewerDocsExist(true); + } + }, [atInitialLoad, rawPage, syncInProgress]); + + useEffect(() => { + if (!rawPage || !shadowDomRef.current) { + return; } if (onReady) { onReady(); } // Pre-render - const transformedElement = transformer(value as string, [ + const transformedElement = transformer(rawPage as string, [ sanitizeDOM(), addBaseUrl({ techdocsStorageApi, @@ -145,6 +202,9 @@ export const Reader = ({ entityId, onReady }: Props) => { return; // An unexpected error occurred } + const shadowDiv: HTMLElement = shadowDomRef.current!; + const shadowRoot = + shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' }); Array.from(shadowRoot.children).forEach(child => shadowRoot.removeChild(child), ); @@ -166,17 +226,15 @@ export const Reader = ({ entityId, onReady }: Props) => { onClick: (_: MouseEvent, url: string) => { window.scroll({ top: 0 }); const parsedUrl = new URL(url); + if (newerDocsExist && isSynced) { + // link navigation will load newer docs + setNewerDocsExist(false); + } if (parsedUrl.hash) { - history.pushState( - null, - '', - `${parsedUrl.pathname}${parsedUrl.hash}`, - ); + navigate(`${parsedUrl.pathname}${parsedUrl.hash}`); } else { navigate(parsedUrl.pathname); } - - shadowRoot?.querySelector(parsedUrl.hash)?.scrollIntoView(); }, }), onCssReady({ @@ -204,30 +262,49 @@ export const Reader = ({ entityId, onReady }: Props) => { }), ]); }, [ - name, - path, - shadowRoot, - value, - error, - loading, - namespace, - kind, + rawPage, entityId, navigate, + onReady, + shadowDomRef, + path, techdocsStorageApi, theme, - onReady, + kind, + namespace, + name, + newerDocsExist, + isSynced, ]); - if (error) { - // TODO Enhance API call to return customize error objects so we can identify which we ran into - // For now this defaults to display error code 404 - return ; + // docLoadError not considered an error state if sync request is still ongoing + // or sync just completed and doc is loading again + if ((docLoadError && !syncInProgress && !docLoading) || syncError) { + let errMessage = ''; + if (docLoadError) { + errMessage += ` Load error: ${docLoadError}`; + } + if (syncError) errMessage += ` Build error: ${syncError}`; + return ; } return ( <> - {loading ? : null} + {newerDocsExist && !isSynced ? ( + + A newer version of this documentation is being prepared and will be + available shortly. + + ) : null} + {newerDocsExist && isSynced ? ( + + A newer version of this documentation is now available, please refresh + to view. + + ) : null} + {docLoading || (docLoadError && syncInProgress) ? ( + + ) : null}
); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx index c6562ec8ad..841ccfcc66 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.test.tsx @@ -43,17 +43,14 @@ describe('', ( }); describe('', () => { - it('should render with a custom status code, custom error message and go back link', () => { + it('should render with a 404 code, custom error message and go back link', () => { const rendered = render( wrapInTestApp( - , + , ), ); rendered.getByText(/This is a custom error message/i); - rendered.getByText(/500/i); + rendered.getByText(/404/i); rendered.getByText(/Looks like someone dropped the mic!/i); expect(rendered.getByTestId('go-back-link')).toBeDefined(); }); diff --git a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx index 04f2106161..cdacc1cb7e 100644 --- a/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx +++ b/plugins/techdocs/src/reader/components/TechDocsNotFound.tsx @@ -19,10 +19,9 @@ import { ErrorPage, useApi, configApiRef } from '@backstage/core'; type Props = { errorMessage?: string; - statusCode?: number; }; -export const TechDocsNotFound = ({ errorMessage, statusCode }: Props) => { +export const TechDocsNotFound = ({ errorMessage }: Props) => { const techdocsBuilder = useApi(configApiRef).getOptionalString( 'techdocs.builder', ); @@ -38,7 +37,7 @@ export const TechDocsNotFound = ({ errorMessage, statusCode }: Props) => { return ( diff --git a/plugins/techdocs/src/reader/hooks/index.ts b/plugins/techdocs/src/reader/hooks/index.ts deleted file mode 100644 index f66c5303ed..0000000000 --- a/plugins/techdocs/src/reader/hooks/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -/* - * 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 { useShadowDom } from './shadowDom'; diff --git a/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx b/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx deleted file mode 100644 index c4fab0dbc6..0000000000 --- a/plugins/techdocs/src/reader/hooks/shadowDom.test.tsx +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 { renderWithEffects } from '@backstage/test-utils'; -import { useShadowDom } from './shadowDom'; - -const ComponentWithoutHook = () => { - return
; -}; - -const ComponentWithHook = () => { - const [ref] = useShadowDom(); - return
; -}; - -describe('useShadowDom', () => { - it('does not create a Shadow DOM instance', async () => { - const rendered = await renderWithEffects(); - - const divElement = rendered.getByTestId('shadow-dom'); - expect(divElement.shadowRoot).not.toBeInstanceOf(ShadowRoot); - }); - - it('create a Shadow DOM instance', async () => { - const rendered = await renderWithEffects(); - - const divElement = rendered.getByTestId('shadow-dom'); - expect(divElement.shadowRoot).toBeInstanceOf(ShadowRoot); - }); -}); diff --git a/plugins/techdocs/src/reader/hooks/shadowDom.ts b/plugins/techdocs/src/reader/hooks/shadowDom.ts deleted file mode 100644 index 93e4c1d375..0000000000 --- a/plugins/techdocs/src/reader/hooks/shadowDom.ts +++ /dev/null @@ -1,31 +0,0 @@ -/* - * 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 { useEffect, useRef } from 'react'; -import type { RefObject } from 'react'; - -type IUseShadowDOM = () => [RefObject, ShadowRoot?]; - -export const useShadowDom: IUseShadowDOM = () => { - const ref = useRef(null); - - useEffect(() => { - const divElement = ref.current; - divElement?.attachShadow({ mode: 'open' }); - }, []); - - return [ref, ref.current?.shadowRoot || undefined]; -}; diff --git a/plugins/techdocs/src/reader/index.tsx b/plugins/techdocs/src/reader/index.tsx index a3ca8f394b..6b66289fa5 100644 --- a/plugins/techdocs/src/reader/index.tsx +++ b/plugins/techdocs/src/reader/index.tsx @@ -14,5 +14,4 @@ * limitations under the License. */ -export * from './hooks'; export * from './components'; diff --git a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts index 10c7191b63..5ba86e42ca 100644 --- a/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts +++ b/plugins/techdocs/src/reader/transformers/addBaseUrl.test.ts @@ -23,6 +23,7 @@ const DOC_STORAGE_URL = 'https://example-host.storage.googleapis.com'; const techdocsStorageApi: TechDocsStorage = { getBaseUrl: jest.fn(() => Promise.resolve(DOC_STORAGE_URL)), getEntityDocs: () => new Promise(resolve => resolve('yes!')), + syncEntityDocs: () => new Promise(resolve => resolve(true)), }; const fixture = `