diff --git a/.changeset/popular-panthers-hear.md b/.changeset/popular-panthers-hear.md new file mode 100644 index 0000000000..c31586ea82 --- /dev/null +++ b/.changeset/popular-panthers-hear.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +TechDocs redirect feature now includes a notification to the user before they are redirected. diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts index 8fbd63428d..12a552577a 100644 --- a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.test.ts @@ -16,6 +16,7 @@ import { handleMetaRedirects } from './handleMetaRedirects'; import { createTestShadowDom } from '../../test-utils'; +import { screen } from '@testing-library/react'; describe('handleMetaRedirects', () => { const navigate = jest.fn(); @@ -35,15 +36,19 @@ describe('handleMetaRedirects', () => { }, writable: true, }); - return await createTestShadowDom(html, { preTransformers: [], postTransformers: [handleMetaRedirects(navigate, entityName)], }); }; + beforeEach(() => { + jest.useFakeTimers(); + }); afterEach(() => { jest.clearAllMocks(); + jest.useRealTimers(); + document.body.innerHTML = ''; }); it('should navigate to relative URL if meta redirect tag is present', async () => { @@ -52,6 +57,13 @@ describe('handleMetaRedirects', () => { 'http://localhost/docs/default/component/testEntity/subpath', '/docs/default/component/testEntity/subpath', ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); expect(navigate).toHaveBeenCalledWith( 'http://localhost/docs/default/component/testEntity/anotherPage', ); @@ -63,6 +75,13 @@ describe('handleMetaRedirects', () => { 'http://localhost/docs/default/component/testEntity/subpath', '/docs/default/component/testEntity/subpath', ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); expect(navigate).toHaveBeenCalledWith('/docs/default/component/testEntity'); }); @@ -72,6 +91,13 @@ describe('handleMetaRedirects', () => { 'http://localhost/docs/default/component/testEntity/subpath', '/docs/default/component/testEntity/subpath', ); + + expect( + await screen.findByText( + 'This TechDocs page is no longer maintained. Will automatically redirect to the designated replacement.', + ), + ).toBeInTheDocument(); + jest.runAllTimers(); expect(navigate).toHaveBeenCalledWith('http://localhost/test'); }); @@ -81,6 +107,8 @@ describe('handleMetaRedirects', () => { 'http://localhost/docs/default/component/testEntity/subpath', '/docs/default/component/testEntity/subpath', ); + + jest.runAllTimers(); expect(navigate).not.toHaveBeenCalled(); }); }); diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts deleted file mode 100644 index 4c56d4d363..0000000000 --- a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.ts +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2024 The Backstage Authors - * - * 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 './transformer'; -import { normalizeUrl } from './rewriteDocLinks'; - -export const handleMetaRedirects = ( - navigate: (to: string) => void, - entityName: string, -): Transformer => { - return dom => { - for (const elem of Array.from(dom.querySelectorAll('meta'))) { - if (elem.getAttribute('http-equiv') === 'refresh') { - const metaContentParameters = elem - .getAttribute('content') - ?.split('url='); - if (!metaContentParameters || metaContentParameters.length < 2) { - continue; - } - - const metaUrl = metaContentParameters[1]; - const normalizedCurrentUrl = normalizeUrl(window.location.href); - // If metaUrl is relative, it will be resolved with base href. If it is absolute, it will replace the base href when creating URL object. - const absoluteRedirectObj = new URL(metaUrl, normalizedCurrentUrl); - const isExternalRedirect = - absoluteRedirectObj.hostname !== window.location.hostname; - - if (isExternalRedirect) { - // If the redirect is external, navigate to the documentation site home instead of the external url. - const currentTechDocPath = window.location.pathname; - const indexOfSiteHome = currentTechDocPath.indexOf(entityName); - const siteHomePath = currentTechDocPath.slice( - 0, - indexOfSiteHome + entityName.length, - ); - navigate(siteHomePath); - } else { - // The navigate function from dom.tsx is a wrapper around react-router navigate function that helps absolute url redirects. - navigate(absoluteRedirectObj.href); - } - return dom; - } - } - return dom; - }; -}; diff --git a/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx new file mode 100644 index 0000000000..2d74f4cbf5 --- /dev/null +++ b/plugins/techdocs/src/reader/transformers/handleMetaRedirects.tsx @@ -0,0 +1,126 @@ +/* + * Copyright 2024 The Backstage Authors + * + * 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 './transformer'; +import { normalizeUrl } from './rewriteDocLinks'; +import Snackbar from '@material-ui/core/Snackbar'; +import React, { useState } from 'react'; +import { renderReactElement } from './renderReactElement'; +import Button from '@material-ui/core/Button'; +import { makeStyles } from '@material-ui/core/styles'; + +type RedirectNotificationProps = { + handleButtonClick: () => void; + message: string; + autoHideDuration: number; +}; + +const useStyles = makeStyles(theme => ({ + button: { + color: theme.palette.primary.light, + textDecoration: 'underline', + }, +})); +const RedirectNotification = ({ + message, + handleButtonClick, + autoHideDuration, +}: RedirectNotificationProps) => { + const classes = useStyles(); + const [open, setOpen] = useState(true); + const handleClose = () => { + setOpen(prev => !prev); + }; + + return ( + + Redirect now + + } + /> + ); +}; + +export const handleMetaRedirects = ( + navigate: (to: string) => void, + entityName: string, +): Transformer => { + const redirectAfterMs = 4000; + const determineRedirectURL = (metaUrl: string) => { + const normalizedCurrentUrl = normalizeUrl(window.location.href); + // If metaUrl is relative, it will be resolved with base href. If it is absolute, it will replace the base href when creating URL object. + const absoluteRedirectObj = new URL(metaUrl, normalizedCurrentUrl); + const isExternalRedirect = + absoluteRedirectObj.hostname !== window.location.hostname; + + if (isExternalRedirect) { + const currentTechDocPath = window.location.pathname; + const indexOfSiteHome = currentTechDocPath.indexOf(entityName); + const siteHomePath = currentTechDocPath.slice( + 0, + indexOfSiteHome + entityName.length, + ); + return siteHomePath; + } + // The navigate function from dom.tsx is a wrapper around react-router navigate function that helps absolute url redirects. + return absoluteRedirectObj.href; + }; + + return dom => { + for (const elem of Array.from(dom.querySelectorAll('meta'))) { + if (elem.getAttribute('http-equiv') === 'refresh') { + const metaContentParameters = elem + .getAttribute('content') + ?.split('url='); + if (!metaContentParameters || metaContentParameters.length < 2) { + continue; + } + const metaUrl = metaContentParameters[1]; + const redirectURL = determineRedirectURL(metaUrl); + const container = document.createElement('div'); + + renderReactElement( + navigate(redirectURL)} + autoHideDuration={redirectAfterMs} + />, + container, + ); + document.body.appendChild(container); + + setTimeout(() => { + navigate(redirectURL); + }, redirectAfterMs); + + return dom; + } + } + return dom; + }; +};