[TechDocs] Experimental useTechDocsReaderDom hook (#7476)

* WIP!

Co-authored-by: Camila Belo <camilaibs@gmail.com>
Signed-off-by: Eric Peterson <ericpeterson@spotify.com>

* fix: use dom when updating sidebar

Signed-off-by: Camila Belo <camilaibs@gmail.com>

* fix: extract small components and transformers

Signed-off-by: Camila Belo <camilaibs@gmail.com>

* docs: describe the useTechDocsReaderDom

Signed-off-by: Camila Belo <camilaibs@gmail.com>

* docs: update api report

Signed-off-by: Camila Belo <camilaibs@gmail.com>

* docs: make the hook internal

Signed-off-by: Camila Belo <camilaibs@gmail.com>

* fix: remove circular progress from the alert

Signed-off-by: Camila Belo <camilaibs@gmail.com>

* Fix test

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>

* Mark new exports as @internal, add a text string to make it easy to find in the future

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>

* Changeset

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>

* Do not show progress bar during checking; prevent jumpy site on navigation

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>

* Combine progress/alert into a state indicator component and clarify vars.

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>

* fix: documentation state updates

Co-authored-by: Eric Peterson <ericpeterson@spotify.com>
Signed-off-by: Camila Belo <camilaibs@gmail.com>

* fix: use circular progress again

Co-authored-by: Eric Peterson <ericpeterson@spotify.com>
Signed-off-by: Camila Belo <camilaibs@gmail.com>

* docs: update reader exports

Co-authored-by: Eric Peterson <ericpeterson@spotify.com>
Signed-off-by: Camila Belo <camilaibs@gmail.com>

* refactor: using provider instead of HOC

Co-authored-by: Eric Peterson <ericpeterson@spotify.com>
Signed-off-by: Camila Belo <camilaibs@gmail.com>

* refactor: update the changeset file

Co-authored-by: Eric Peterson <ericpeterson@spotify.com>
Signed-off-by: Camila Belo <camilaibs@gmail.com>

Co-authored-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Camila Belo
2021-10-13 16:15:27 +02:00
committed by GitHub
parent 7c00725471
commit 76fef740fe
8 changed files with 478 additions and 260 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-techdocs': patch
---
Refactored `<Reader />` component internals to support future extensibility.
+237 -257
View File
@@ -14,22 +14,26 @@
* limitations under the License.
*/
import React, {
PropsWithChildren,
ComponentType,
createContext,
useContext,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { Grid, makeStyles, useTheme } from '@material-ui/core';
import { EntityName } from '@backstage/catalog-model';
import { Progress } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { BackstageTheme } from '@backstage/theme';
import {
Button,
CircularProgress,
Grid,
makeStyles,
useTheme,
} from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { techdocsStorageApiRef } from '../../api';
import {
addBaseUrl,
addGitFeedbackLink,
@@ -40,26 +44,21 @@ import {
rewriteDocLinks,
sanitizeDOM,
simplifyMkdocsFooter,
scrollIntoAnchor,
transform as transformer,
} from '../transformers';
import { TechDocsBuildLogs } from './TechDocsBuildLogs';
import { TechDocsNotFound } from './TechDocsNotFound';
import { TechDocsSearch } from './TechDocsSearch';
import { TechDocsStateIndicator } from './TechDocsStateIndicator';
import { useReaderState } from './useReaderState';
type Props = {
entityRef: EntityName;
onReady?: () => void;
withSearch?: boolean;
onReady?: () => void;
};
const useStyles = makeStyles<BackstageTheme>(theme => ({
message: {
// `word-break: break-word` is deprecated, but gives legacy support to browsers not supporting `overflow-wrap` yet
// https://developer.mozilla.org/en-US/docs/Web/CSS/word-break
wordBreak: 'break-word',
overflowWrap: 'anywhere',
},
searchBar: {
marginLeft: '20rem',
maxWidth: 'calc(100% - 20rem * 2 - 3rem)',
@@ -71,44 +70,86 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
const { kind, namespace, name } = entityRef;
const theme = useTheme<BackstageTheme>();
const classes = useStyles();
type TechDocsReaderValue = ReturnType<typeof useReaderState>;
const {
state,
path,
contentReload,
content: rawPage,
contentErrorMessage,
syncErrorMessage,
buildLog,
} = useReaderState(kind, namespace, name, useParams()['*']);
const TechDocsReaderContext = createContext<TechDocsReaderValue>(
{} as TechDocsReaderValue,
);
const techdocsStorageApi = useApi(techdocsStorageApiRef);
const [sidebars, setSidebars] = useState<HTMLElement[]>();
const TechDocsReaderProvider = ({ children }: PropsWithChildren<{}>) => {
const { namespace = '', kind = '', name = '', '*': path } = useParams();
const value = useReaderState(kind, namespace, name, path);
return (
<TechDocsReaderContext.Provider value={value}>
{children}
</TechDocsReaderContext.Provider>
);
};
/**
* Note: this HOC is currently being exported so that we can rapidly
* iterate on alternative <Reader /> implementations that extend core
* functionality. There is no guarantee that this HOC will continue to be
* exported by the package in the future!
*
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
* @internal
*/
export const withTechDocsReaderProvider =
<T extends {}>(Component: ComponentType<T>) =>
(props: T) =>
(
<TechDocsReaderProvider>
<Component {...props} />
</TechDocsReaderProvider>
);
/**
* Note: this hook is currently being exported so that we can rapidly
* iterate on alternative <Reader /> implementations that extend core
* functionality. There is no guarantee that this hook will continue to be
* exported by the package in the future!
*
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
* @internal
*/
export const useTechDocsReader = () => useContext(TechDocsReaderContext);
/**
* Hook that encapsulates the behavior of getting raw HTML and applying
* transforms to it in order to make it function at a basic level in the
* Backstage UI.
*
* Note: this hook is currently being exported so that we can rapidly iterate
* on alternative <Reader /> implementations that extend core functionality.
* There is no guarantee that this hook will continue to be exported by the
* package in the future!
*
* todo: Make public or stop exporting (see others: "altReaderExperiments")
* @internal
*/
export const useTechDocsReaderDom = (): Element | null => {
const navigate = useNavigate();
const shadowDomRef = useRef<HTMLDivElement>(null);
const theme = useTheme<BackstageTheme>();
const techdocsStorageApi = useApi(techdocsStorageApiRef);
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
const { namespace = '', kind = '', name = '' } = useParams();
const { state, path, content: rawPage } = useTechDocsReader();
const [sidebars, setSidebars] = useState<HTMLElement[]>();
const [dom, setDom] = useState<HTMLElement | null>(null);
const updateSidebarPosition = useCallback(() => {
if (!!shadowDomRef.current && !!sidebars) {
const shadowDiv: HTMLElement = shadowDomRef.current!;
const shadowRoot =
shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' });
const mdTabs = shadowRoot.querySelector('.md-container > .md-tabs');
sidebars!.forEach(sidebar => {
const newTop = Math.max(
shadowDomRef.current!.getBoundingClientRect().top,
0,
);
sidebar.style.top = mdTabs
? `${newTop + mdTabs.getBoundingClientRect().height}px`
: `${newTop}px`;
});
}
}, [shadowDomRef, sidebars]);
if (!dom || !sidebars) return;
// set sidebar height so they don't initially render in wrong position
const mdTabs = dom.querySelector('.md-container > .md-tabs');
sidebars.forEach(sidebar => {
const newTop = Math.max(dom.getBoundingClientRect().top, 0);
sidebar.style.top = mdTabs
? `${newTop + mdTabs.getBoundingClientRect().height}px`
: `${newTop}px`;
});
}, [dom, sidebars]);
useEffect(() => {
updateSidebarPosition();
@@ -141,62 +182,62 @@ export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
addGitFeedbackLink(scmIntegrationsApi),
injectCss({
css: `
body {
font-family: ${theme.typography.fontFamily};
--md-text-color: ${theme.palette.text.primary};
--md-text-link-color: ${theme.palette.primary.main};
body {
font-family: ${theme.typography.fontFamily};
--md-text-color: ${theme.palette.text.primary};
--md-text-link-color: ${theme.palette.primary.main};
--md-code-fg-color: ${theme.palette.text.primary};
--md-code-bg-color: ${theme.palette.background.paper};
}
.md-main__inner { margin-top: 0; }
.md-sidebar { position: fixed; bottom: 100px; width: 20rem; }
.md-sidebar--secondary { right: 2rem; }
.md-content { margin-bottom: 50px }
.md-footer { position: fixed; bottom: 0px; width: 100vw; }
.md-footer-nav__link { width: 20rem;}
.md-content { margin-left: 20rem; max-width: calc(100% - 20rem * 2 - 3rem); }
.md-typeset { font-size: 1rem; }
.md-nav { font-size: 1rem; }
.md-grid { max-width: 90vw; margin: 0 }
.md-typeset table:not([class]) {
font-size: 1rem;
border: 1px solid ${theme.palette.text.primary};
border-bottom: none;
border-collapse: collapse;
}
.md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
border-bottom: 1px solid ${theme.palette.text.primary};
}
.md-typeset table:not([class]) th { font-weight: bold; }
.md-typeset .admonition, .md-typeset details {
font-size: 1rem;
}
@media screen and (max-width: 76.1875em) {
.md-nav {
background-color: ${theme.palette.background.default};
transition: none !important
--md-code-fg-color: ${theme.palette.text.primary};
--md-code-bg-color: ${theme.palette.background.paper};
}
.md-sidebar--secondary { display: none; }
.md-sidebar--primary { left: 72px; width: 10rem }
.md-content { margin-left: 10rem; max-width: calc(100% - 10rem); }
.md-content__inner { font-size: 0.9rem }
.md-footer {
position: static;
margin-left: 10rem;
width: calc(100% - 10rem);
.md-main__inner { margin-top: 0; }
.md-sidebar { position: fixed; bottom: 100px; width: 20rem; }
.md-sidebar--secondary { right: 2rem; }
.md-content { margin-bottom: 50px }
.md-footer { position: fixed; bottom: 0px; width: 100vw; }
.md-footer-nav__link { width: 20rem;}
.md-content { margin-left: 20rem; max-width: calc(100% - 20rem * 2 - 3rem); }
.md-typeset { font-size: 1rem; }
.md-nav { font-size: 1rem; }
.md-grid { max-width: 90vw; margin: 0 }
.md-typeset table:not([class]) {
font-size: 1rem;
border: 1px solid ${theme.palette.text.primary};
border-bottom: none;
border-collapse: collapse;
}
.md-nav--primary .md-nav__title {
white-space: normal;
height: auto;
line-height: 1rem;
cursor: auto;
.md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
border-bottom: 1px solid ${theme.palette.text.primary};
}
.md-nav--primary > .md-nav__title [for="none"] {
padding-top: 0;
.md-typeset table:not([class]) th { font-weight: bold; }
.md-typeset .admonition, .md-typeset details {
font-size: 1rem;
}
}
`,
@media screen and (max-width: 76.1875em) {
.md-nav {
background-color: ${theme.palette.background.default};
transition: none !important
}
.md-sidebar--secondary { display: none; }
.md-sidebar--primary { left: 72px; width: 10rem }
.md-content { margin-left: 10rem; max-width: calc(100% - 10rem); }
.md-content__inner { font-size: 0.9rem }
.md-footer {
position: static;
margin-left: 10rem;
width: calc(100% - 10rem);
}
.md-nav--primary .md-nav__title {
white-space: normal;
height: auto;
line-height: 1rem;
cursor: auto;
}
.md-nav--primary > .md-nav__title [for="none"] {
padding-top: 0;
}
}
`,
}),
injectCss({
// Disable CSS animations on link colors as they lead to issues in dark
@@ -204,20 +245,20 @@ export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
// is always an animation from light to dark mode when navigation
// between pages.
css: `
.md-nav__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
transition: none;
}
`,
.md-nav__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
transition: none;
}
`,
}),
injectCss({
// Properly style code blocks.
css: `
.md-typeset pre > code::-webkit-scrollbar-thumb {
background-color: hsla(0, 0%, 0%, 0.32);
}
.md-typeset pre > code::-webkit-scrollbar-thumb:hover {
background-color: hsla(0, 0%, 0%, 0.87);
}
.md-typeset pre > code::-webkit-scrollbar-thumb {
background-color: hsla(0, 0%, 0%, 0.32);
}
.md-typeset pre > code::-webkit-scrollbar-thumb:hover {
background-color: hsla(0, 0%, 0%, 0.87);
}
`,
}),
injectCss({
@@ -230,30 +271,30 @@ export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
// example in the backend), we have to copy from main*.css and modify
// them.
css: `
:host {
--md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z"/></svg>');
--md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 5h16v2H4V5m0 4h16v2H4V9m0 4h16v2H4v-2m0 4h10v2H4v-2z"/></svg>');
--md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z"/></svg>');
--md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.55 11.2c-.23-.3-.5-.56-.76-.82-.65-.6-1.4-1.03-2.03-1.66C13.3 7.26 13 4.85 13.91 3c-.91.23-1.75.75-2.45 1.32-2.54 2.08-3.54 5.75-2.34 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.22.1-.46.04-.64-.12a.83.83 0 01-.15-.17c-1.1-1.43-1.28-3.48-.53-5.12C5.89 10 5 12.3 5.14 14.47c.04.5.1 1 .27 1.5.14.6.4 1.2.72 1.73 1.04 1.73 2.87 2.97 4.84 3.22 2.1.27 4.35-.12 5.96-1.6 1.8-1.66 2.45-4.32 1.5-6.6l-.13-.26c-.2-.46-.47-.87-.8-1.25l.05-.01m-3.1 6.3c-.28.24-.73.5-1.08.6-1.1.4-2.2-.16-2.87-.82 1.19-.28 1.89-1.16 2.09-2.05.17-.8-.14-1.46-.27-2.23-.12-.74-.1-1.37.18-2.06.17.38.37.76.6 1.06.76 1 1.95 1.44 2.2 2.8.04.14.06.28.06.43.03.82-.32 1.72-.92 2.27h.01z"/></svg>');
--md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2m-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
--md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.07 11.25l-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 00-2-2 2 2 0 00-2 2H8a4 4 0 014-4 4 4 0 014 4 3.2 3.2 0 01-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10c0-5.53-4.5-10-10-10z"/></svg>');
--md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 14h-2v-4h2m0 8h-2v-2h2M1 21h22L12 2 1 21z"/></svg>');
--md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2c5.53 0 10 4.47 10 10s-4.47 10-10 10S2 17.53 2 12 6.47 2 12 2m3.59 5L12 10.59 8.41 7 7 8.41 10.59 12 7 15.59 8.41 17 12 13.41 15.59 17 17 15.59 13.41 12 17 8.41 15.59 7z"/></svg>');
--md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.5 20l4.86-9.73H13V4l-5 9.73h3.5V20M12 2c2.75 0 5.1 1 7.05 2.95C21 6.9 22 9.25 22 12s-1 5.1-2.95 7.05C17.1 21 14.75 22 12 22s-5.1-1-7.05-2.95C3 17.1 2 14.75 2 12s1-5.1 2.95-7.05C6.9 3 9.25 2 12 2z"/></svg>');
--md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 00-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 00-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z"/></svg>');
--md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 01.75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z"/></svg>');
--md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z"/></svg>');
}
:host {
--md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.42L5.83 13H21V7h-2z"/></svg>');
}
:host {
--md-details-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
}
:host {
--md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z"/></svg>');
--md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2m-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
}
:host {
--md-admonition-icon--note: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20.71 7.04c.39-.39.39-1.04 0-1.41l-2.34-2.34c-.37-.39-1.02-.39-1.41 0l-1.84 1.83 3.75 3.75M3 17.25V21h3.75L17.81 9.93l-3.75-3.75L3 17.25z"/></svg>');
--md-admonition-icon--abstract: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M4 5h16v2H4V5m0 4h16v2H4V9m0 4h16v2H4v-2m0 4h10v2H4v-2z"/></svg>');
--md-admonition-icon--info: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 9h-2V7h2m0 10h-2v-6h2m-1-9A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z"/></svg>');
--md-admonition-icon--tip: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.55 11.2c-.23-.3-.5-.56-.76-.82-.65-.6-1.4-1.03-2.03-1.66C13.3 7.26 13 4.85 13.91 3c-.91.23-1.75.75-2.45 1.32-2.54 2.08-3.54 5.75-2.34 8.9.04.1.08.2.08.33 0 .22-.15.42-.35.5-.22.1-.46.04-.64-.12a.83.83 0 01-.15-.17c-1.1-1.43-1.28-3.48-.53-5.12C5.89 10 5 12.3 5.14 14.47c.04.5.1 1 .27 1.5.14.6.4 1.2.72 1.73 1.04 1.73 2.87 2.97 4.84 3.22 2.1.27 4.35-.12 5.96-1.6 1.8-1.66 2.45-4.32 1.5-6.6l-.13-.26c-.2-.46-.47-.87-.8-1.25l.05-.01m-3.1 6.3c-.28.24-.73.5-1.08.6-1.1.4-2.2-.16-2.87-.82 1.19-.28 1.89-1.16 2.09-2.05.17-.8-.14-1.46-.27-2.23-.12-.74-.1-1.37.18-2.06.17.38.37.76.6 1.06.76 1 1.95 1.44 2.2 2.8.04.14.06.28.06.43.03.82-.32 1.72-.92 2.27h.01z"/></svg>');
--md-admonition-icon--success: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2m-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
--md-admonition-icon--question: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15.07 11.25l-.9.92C13.45 12.89 13 13.5 13 15h-2v-.5c0-1.11.45-2.11 1.17-2.83l1.24-1.26c.37-.36.59-.86.59-1.41a2 2 0 00-2-2 2 2 0 00-2 2H8a4 4 0 014-4 4 4 0 014 4 3.2 3.2 0 01-.93 2.25M13 19h-2v-2h2M12 2A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10c0-5.53-4.5-10-10-10z"/></svg>');
--md-admonition-icon--warning: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 14h-2v-4h2m0 8h-2v-2h2M1 21h22L12 2 1 21z"/></svg>');
--md-admonition-icon--failure: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2c5.53 0 10 4.47 10 10s-4.47 10-10 10S2 17.53 2 12 6.47 2 12 2m3.59 5L12 10.59 8.41 7 7 8.41 10.59 12 7 15.59 8.41 17 12 13.41 15.59 17 17 15.59 13.41 12 17 8.41 15.59 7z"/></svg>');
--md-admonition-icon--danger: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M11.5 20l4.86-9.73H13V4l-5 9.73h3.5V20M12 2c2.75 0 5.1 1 7.05 2.95C21 6.9 22 9.25 22 12s-1 5.1-2.95 7.05C17.1 21 14.75 22 12 22s-5.1-1-7.05-2.95C3 17.1 2 14.75 2 12s1-5.1 2.95-7.05C6.9 3 9.25 2 12 2z"/></svg>');
--md-admonition-icon--bug: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 12h-4v-2h4m0 6h-4v-2h4m6-6h-2.81a5.985 5.985 0 00-1.82-1.96L17 4.41 15.59 3l-2.17 2.17a6.002 6.002 0 00-2.83 0L8.41 3 7 4.41l1.62 1.63C7.88 6.55 7.26 7.22 6.81 8H4v2h2.09c-.05.33-.09.66-.09 1v1H4v2h2v1c0 .34.04.67.09 1H4v2h2.81c1.04 1.79 2.97 3 5.19 3s4.15-1.21 5.19-3H20v-2h-2.09c.05-.33.09-.66.09-1v-1h2v-2h-2v-1c0-.34-.04-.67-.09-1H20V8z"/></svg>');
--md-admonition-icon--example: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M7 13v-2h14v2H7m0 6v-2h14v2H7M7 7V5h14v2H7M3 8V5H2V4h2v4H3m-1 9v-1h3v4H2v-1h2v-.5H3v-1h1V17H2m2.25-7a.75.75 0 01.75.75c0 .2-.08.39-.21.52L3.12 13H5v1H2v-.92L4 11H2v-1h2.25z"/></svg>');
--md-admonition-icon--quote: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 17h3l2-4V7h-6v6h3M6 17h3l2-4V7H5v6h3l-2 4z"/></svg>');
}
:host {
--md-footnotes-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 7v4H5.83l3.58-3.59L8 6l-6 6 6 6 1.41-1.42L5.83 13H21V7h-2z"/></svg>');
}
:host {
--md-details-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8.59 16.58L13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
}
:host {
--md-tasklist-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2A10 10 0 002 12a10 10 0 0010 10 10 10 0 0010-10A10 10 0 0012 2z"/></svg>');
--md-tasklist-icon--checked: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 2C6.5 2 2 6.5 2 12s4.5 10 10 10 10-4.5 10-10S17.5 2 12 2m-2 15l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
}
`,
}),
]),
@@ -273,29 +314,18 @@ export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
// a function that performs transformations that are executed after adding it to the DOM
const postRender = useCallback(
async (shadowRoot: ShadowRoot) =>
transformer(shadowRoot.children[0], [
dom => {
setTimeout(() => {
// Scoll to the desired anchor on initial navigation
if (window.location.hash) {
const hash = window.location.hash.slice(1);
shadowRoot?.getElementById(hash)?.scrollIntoView();
}
}, 200);
return dom;
},
async (transformedElement: Element) =>
transformer(transformedElement, [
scrollIntoAnchor(),
addLinkClickListener({
baseUrl: window.location.origin,
onClick: (_: MouseEvent, url: string) => {
const parsedUrl = new URL(url);
if (parsedUrl.hash) {
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
// Scroll to hash if it's on the current page
shadowRoot
?.getElementById(parsedUrl.hash.slice(1))
transformedElement
?.querySelector(`#${parsedUrl.hash.slice(1)}`)
?.scrollIntoView();
} else {
navigate(parsedUrl.pathname);
@@ -304,28 +334,18 @@ export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
}),
onCssReady({
docStorageUrl: await techdocsStorageApi.getApiOrigin(),
onLoading: (dom: Element) => {
(dom as HTMLElement).style.setProperty('opacity', '0');
onLoading: (renderedElement: Element) => {
(renderedElement as HTMLElement).style.setProperty('opacity', '0');
},
onLoaded: (dom: Element) => {
(dom as HTMLElement).style.removeProperty('opacity');
onLoaded: (renderedElement: Element) => {
(renderedElement as HTMLElement).style.removeProperty('opacity');
// disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
(dom as HTMLElement)
renderedElement
.querySelector('.md-nav__title')
?.removeAttribute('for');
const sideDivs: HTMLElement[] = Array.from(
shadowRoot!.querySelectorAll('.md-sidebar'),
setSidebars(
Array.from(renderedElement.querySelectorAll('.md-sidebar')),
);
setSidebars(sideDivs);
// set sidebar height so they don't initially render in wrong position
const docTopPosition = (dom as HTMLElement).getBoundingClientRect()
.top;
const mdTabs = dom.querySelector('.md-container > .md-tabs');
sideDivs!.forEach(sidebar => {
sidebar.style.top = mdTabs
? `${docTopPosition + mdTabs.getBoundingClientRect().height}px`
: `${docTopPosition}px`;
});
},
}),
]),
@@ -333,23 +353,14 @@ export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
);
useEffect(() => {
if (!rawPage || !shadowDomRef.current) {
// clear the shadow dom if no content is available
if (shadowDomRef.current?.shadowRoot) {
shadowDomRef.current.shadowRoot.innerHTML = '';
}
return () => {};
}
if (onReady) {
onReady();
}
if (!rawPage) return () => {};
// if false, there is already a newer execution of this effect
let shouldReplaceContent = true;
// Pre-render
preRender(rawPage, path).then(async transformedElement => {
if (!transformedElement?.innerHTML) {
preRender(rawPage, path).then(async preTransformedDomElement => {
if (!preTransformedDomElement?.innerHTML) {
return; // An unexpected error occurred
}
@@ -358,94 +369,49 @@ export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
return;
}
const shadowDiv: HTMLElement = shadowDomRef.current!;
const shadowRoot =
shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' });
Array.from(shadowRoot.children).forEach(child =>
shadowRoot.removeChild(child),
);
shadowRoot.appendChild(transformedElement);
// Scroll to top after render
window.scroll({ top: 0 });
// Post-render
await postRender(shadowRoot);
const postTransformedDomElement = await postRender(
preTransformedDomElement,
);
setDom(postTransformedDomElement as HTMLElement);
});
// cancel this execution
return () => {
shouldReplaceContent = false;
};
}, [onReady, path, postRender, preRender, rawPage]);
}, [rawPage, path, preRender, postRender]);
return dom;
};
const TheReader = ({
entityRef,
onReady = () => {},
withSearch = true,
}: Props) => {
const classes = useStyles();
const dom = useTechDocsReaderDom();
const shadowDomRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!dom || !shadowDomRef.current) return;
const shadowDiv = shadowDomRef.current;
const shadowRoot =
shadowDiv.shadowRoot || shadowDiv.attachShadow({ mode: 'open' });
Array.from(shadowRoot.children).forEach(child =>
shadowRoot.removeChild(child),
);
shadowRoot.appendChild(dom);
onReady();
}, [dom, onReady]);
return (
<>
{state === 'CHECKING' && <Progress />}
{state === 'INITIAL_BUILD' && (
<Alert
variant="outlined"
severity="info"
icon={<CircularProgress size="24px" />}
action={<TechDocsBuildLogs buildLog={buildLog} />}
>
Documentation is accessed for the first time and is being prepared.
The subsequent loads are much faster.
</Alert>
)}
{state === 'CONTENT_STALE_REFRESHING' && (
<Alert
variant="outlined"
severity="info"
icon={<CircularProgress size="24px" />}
action={<TechDocsBuildLogs buildLog={buildLog} />}
>
A newer version of this documentation is being prepared and will be
available shortly.
</Alert>
)}
{state === 'CONTENT_STALE_READY' && (
<Alert
variant="outlined"
severity="success"
action={
<Button color="inherit" onClick={() => contentReload()}>
Refresh
</Button>
}
>
A newer version of this documentation is now available, please refresh
to view.
</Alert>
)}
{state === 'CONTENT_STALE_ERROR' && (
<Alert
variant="outlined"
severity="error"
action={<TechDocsBuildLogs buildLog={buildLog} />}
classes={{ message: classes.message }}
>
Building a newer version of this documentation failed.{' '}
{syncErrorMessage}
</Alert>
)}
{state === 'CONTENT_NOT_FOUND' && (
<>
{syncErrorMessage && (
<Alert
variant="outlined"
severity="error"
action={<TechDocsBuildLogs buildLog={buildLog} />}
classes={{ message: classes.message }}
>
Building a newer version of this documentation failed.{' '}
{syncErrorMessage}
</Alert>
)}
<TechDocsNotFound errorMessage={contentErrorMessage} />
</>
)}
<TechDocsStateIndicator />
{withSearch && shadowDomRef?.current?.shadowRoot?.innerHTML && (
<Grid container className={classes.searchBar}>
<TechDocsSearch entityId={entityRef} />
@@ -455,3 +421,17 @@ export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
</>
);
};
export const Reader = ({
entityRef,
onReady = () => {},
withSearch = true,
}: Props) => (
<TechDocsReaderProvider>
<TheReader
entityRef={entityRef}
onReady={onReady}
withSearch={withSearch}
/>
</TechDocsReaderProvider>
);
@@ -61,7 +61,7 @@ export const buildInitialFilters = (
return legacyPaths
? entityId
: Object.entries(entityId).reduce((acc, [key, value]) => {
return { ...acc, [key]: value.toLocaleLowerCase('en-US') };
return { ...acc, [key]: value?.toLocaleLowerCase('en-US') };
}, {});
};
@@ -175,7 +175,7 @@ const TechDocsSearchBar = ({
);
};
const TechDocsSearch = (props: TechDocsSearchProps) => {
export const TechDocsSearch = (props: TechDocsSearchProps) => {
const configApi = useApi(configApiRef);
const legacyPaths = configApi.getOptionalBoolean(
'techdocs.legacyUseCaseSensitiveTripletPaths',
@@ -192,4 +192,3 @@ const TechDocsSearch = (props: TechDocsSearchProps) => {
</SearchContextProvider>
);
};
export { TechDocsSearch };
@@ -0,0 +1,142 @@
/*
* Copyright 2021 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 React from 'react';
import { Progress } from '@backstage/core-components';
import { CircularProgress, Button, makeStyles } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { TechDocsBuildLogs } from './TechDocsBuildLogs';
import { TechDocsNotFound } from './TechDocsNotFound';
import { useTechDocsReader } from './Reader';
const useStyles = makeStyles(() => ({
message: {
// `word-break: break-word` is deprecated, but gives legacy support to browsers not supporting `overflow-wrap` yet
// https://developer.mozilla.org/en-US/docs/Web/CSS/word-break
wordBreak: 'break-word',
overflowWrap: 'anywhere',
},
}));
/**
* Note: this component is currently being exported so that we can rapidly
* iterate on alternative <Reader /> implementations that extend core
* functionality. There is no guarantee that this component will continue to be
* exported by the package in the future!
*
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
* @internal
*/
export const TechDocsStateIndicator = () => {
let StateAlert: JSX.Element | null = null;
const classes = useStyles();
const {
state,
contentReload,
contentErrorMessage,
syncErrorMessage,
buildLog,
} = useTechDocsReader();
const ReaderProgress = state === 'CHECKING' ? <Progress /> : null;
if (state === 'INITIAL_BUILD') {
StateAlert = (
<Alert
variant="outlined"
severity="info"
icon={<CircularProgress size="24px" />}
action={<TechDocsBuildLogs buildLog={buildLog} />}
>
Documentation is accessed for the first time and is being prepared. The
subsequent loads are much faster.
</Alert>
);
}
if (state === 'CONTENT_STALE_REFRESHING') {
StateAlert = (
<Alert
variant="outlined"
severity="info"
icon={<CircularProgress size="24px" />}
action={<TechDocsBuildLogs buildLog={buildLog} />}
>
A newer version of this documentation is being prepared and will be
available shortly.
</Alert>
);
}
if (state === 'CONTENT_STALE_READY') {
StateAlert = (
<Alert
variant="outlined"
severity="success"
action={
<Button color="inherit" onClick={() => contentReload()}>
Refresh
</Button>
}
>
A newer version of this documentation is now available, please refresh
to view.
</Alert>
);
}
if (state === 'CONTENT_STALE_ERROR') {
StateAlert = (
<Alert
variant="outlined"
severity="error"
action={<TechDocsBuildLogs buildLog={buildLog} />}
classes={{ message: classes.message }}
>
Building a newer version of this documentation failed.{' '}
{syncErrorMessage}
</Alert>
);
}
if (state === 'CONTENT_NOT_FOUND') {
StateAlert = (
<>
{syncErrorMessage && (
<Alert
variant="outlined"
severity="error"
action={<TechDocsBuildLogs buildLog={buildLog} />}
classes={{ message: classes.message }}
>
Building a newer version of this documentation failed.{' '}
{syncErrorMessage}
</Alert>
)}
<TechDocsNotFound errorMessage={contentErrorMessage} />
</>
);
}
return (
<>
{ReaderProgress}
{StateAlert}
</>
);
};
@@ -17,3 +17,18 @@
export * from './Reader';
export * from './TechDocsPage';
export * from './TechDocsPageHeader';
export * from './TechDocsStateIndicator';
/**
* Note: this component is currently being exported so that we can rapidly
* iterate on alternative <Reader /> implementations that extend core
* functionality. There is no guarantee that this component will continue to be
* exported by the package in the future!
*
* Why is this comment here instead of above the component itself? It's a
* workaround for some kind of bug in @microsoft/api-extractor.
*
* todo: Make public or stop exporting (ctrl+f "altReaderExperiments")
* @internal
*/
export { TechDocsSearch } from './TechDocsSearch';
@@ -23,4 +23,5 @@ export * from './simplifyMkdocsFooter';
export * from './onCssReady';
export * from './sanitizeDOM';
export * from './injectCss';
export * from './scrollIntoAnchor';
export * from './transformer';
@@ -0,0 +1,46 @@
/*
* Copyright 2021 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 { scrollIntoAnchor } from '../transformers';
jest.useFakeTimers();
describe('scrollIntoAnchor', () => {
const transformer = scrollIntoAnchor();
const dom = { querySelector: jest.fn() };
afterEach(() => {
jest.clearAllMocks();
});
it('does nothing if there is no anchor element', async () => {
transformer(dom as unknown as Element);
jest.advanceTimersByTime(200);
expect(dom.querySelector).not.toHaveBeenCalled();
});
it('scroll to the hash anchor element', async () => {
const scrollIntoView = jest.fn();
dom.querySelector.mockReturnValue({ scrollIntoView });
const hash = '#hash';
window.location.hash = hash;
transformer(dom as unknown as Element);
jest.advanceTimersByTime(200);
expect(dom.querySelector).toHaveBeenCalledWith(`#${hash.slice(1)}`);
expect(scrollIntoView).toHaveBeenCalledWith();
window.location.hash = '';
});
});
@@ -0,0 +1,30 @@
/*
* Copyright 2021 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 type { Transformer } from './transformer';
export const scrollIntoAnchor = (): Transformer => {
return dom => {
setTimeout(() => {
// Scroll to the desired anchor on initial navigation
if (window.location.hash) {
const hash = window.location.hash.slice(1);
dom?.querySelector(`#${hash}`)?.scrollIntoView();
}
}, 200);
return dom;
};
};