feat(techdocs): merge reader components

Co-authored-by: Emma Indal <emma.indahl@gmail.com>
Signed-off-by: Camila Belo <camilaibs@gmail.com>
This commit is contained in:
Camila Belo
2022-03-29 20:05:46 +02:00
committed by Emma Indal
parent 948d6425de
commit fbc5c68d91
24 changed files with 1738 additions and 1484 deletions
+15 -73
View File
@@ -14,65 +14,20 @@
* limitations under the License.
*/
import React, { PropsWithChildren } from 'react';
import {
CompoundEntityRef,
DEFAULT_NAMESPACE,
Entity,
} from '@backstage/catalog-model';
import {
Reader,
useTechDocsReaderDom,
withTechDocsReaderProvider,
} from './reader';
import React from 'react';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
import { DEFAULT_NAMESPACE, Entity } from '@backstage/catalog-model';
import { toLowerMaybe } from './helpers';
import {
configApiRef,
getComponentData,
useApi,
} from '@backstage/core-plugin-api';
import {
TechDocsReaderPage as AddonAwareReaderPage,
TECHDOCS_ADDONS_WRAPPER_KEY,
} from '@backstage/plugin-techdocs-addons';
import { AsyncState } from 'react-use/lib/useAsyncFn';
import { TechDocsEntityMetadata } from './types';
import { techdocsApiRef } from '.';
import useAsync from 'react-use/lib/useAsync';
import { TechDocsReaderPage } from './plugin';
import { TechDocsReaderLayout } from './reader';
type SpecialReaderPageProps = {
entityName: CompoundEntityRef;
asyncEntityMetadata: AsyncState<TechDocsEntityMetadata>;
addonConfig?: React.ReactNode;
};
type EntityPageDocsProps = { entity: Entity };
// todo(backstage/techdocs-core): Combine with <SpecialReaderPage> and simplify
// with the version in TechDocsReaderPage.tsx
const SpecialReaderPage = (props: SpecialReaderPageProps) => {
const techdocsApi = useApi(techdocsApiRef);
const dom = useTechDocsReaderDom(props.entityName);
const { kind, namespace, name } = props.entityName;
const asyncTechDocsMetadata = useAsync(() => {
return techdocsApi.getTechDocsMetadata({ kind, namespace, name });
}, [kind, namespace, name, techdocsApi]);
return (
<AddonAwareReaderPage
asyncEntityMetadata={props.asyncEntityMetadata}
asyncTechDocsMetadata={asyncTechDocsMetadata}
addonConfig={props.addonConfig}
dom={dom}
hideHeader
/>
);
};
export const EntityPageDocs = ({
children,
entity,
}: PropsWithChildren<{ entity: Entity }>) => {
export const EntityPageDocs = ({ entity }: EntityPageDocsProps) => {
const config = useApi(configApiRef);
const entityName = {
namespace: toLowerMaybe(
entity.metadata.namespace ?? DEFAULT_NAMESPACE,
@@ -82,22 +37,9 @@ export const EntityPageDocs = ({
name: toLowerMaybe(entity.metadata.name, config),
};
// Check if we were given a set of TechDocs addons.
if (children && getComponentData(children, TECHDOCS_ADDONS_WRAPPER_KEY)) {
const Component = withTechDocsReaderProvider(SpecialReaderPage, entityName);
return (
<Component
entityName={entityName}
asyncEntityMetadata={{
loading: false,
error: undefined,
value: entity,
}}
addonConfig={children}
/>
);
}
// Otherwise, return a version of the reader that is not addon-aware.
return <Reader withSearch={false} entityRef={entityName} />;
return (
<TechDocsReaderPage entityName={entityName}>
<TechDocsReaderLayout hideHeader />
</TechDocsReaderPage>
);
};
+3 -4
View File
@@ -67,10 +67,9 @@ export const EmbeddedDocsRouter = (props: PropsWithChildren<{}>) => {
return (
<Routes>
<Route
path="/*"
element={<EntityPageDocs entity={entity}>{children}</EntityPageDocs>}
/>
<Route path="/*" element={<EntityPageDocs entity={entity} />}>
{children}
</Route>
</Routes>
);
};
@@ -1,85 +0,0 @@
/*
* Copyright 2020 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 { ConfigReader } from '@backstage/config';
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils';
import { act, render } from '@testing-library/react';
import React from 'react';
import { TechDocsStorageApi, techdocsStorageApiRef } from '../../api';
import { Reader } from './Reader';
import { ApiProvider } from '@backstage/core-app-api';
import { searchApiRef } from '@backstage/plugin-search';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useParams: jest.fn(),
};
});
const { useParams }: { useParams: jest.Mock } =
jest.requireMock('react-router-dom');
describe('<Reader />', () => {
it('should render Reader content', async () => {
useParams.mockReturnValue({
entityRef: 'Component::backstage',
});
const scmIntegrationsApi: ScmIntegrationsApi =
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
);
const techdocsStorageApi: Partial<TechDocsStorageApi> = {};
const searchApi = {
query: () =>
Promise.resolve({
results: [],
}),
};
const apiRegistry = TestApiRegistry.from(
[scmIntegrationsApiRef, scmIntegrationsApi],
[techdocsStorageApiRef, techdocsStorageApi],
[searchApiRef, searchApi],
);
await act(async () => {
const rendered = render(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<Reader
entityRef={{
kind: 'Component',
namespace: 'default',
name: 'example',
}}
/>
</ApiProvider>,
),
);
expect(
rendered.getByTestId('techdocs-content-shadowroot'),
).toBeInTheDocument();
});
});
});
@@ -1,953 +0,0 @@
/*
* Copyright 2020 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, {
PropsWithChildren,
ComponentType,
createContext,
useContext,
useCallback,
useEffect,
useRef,
useState,
} from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import {
Grid,
makeStyles,
useTheme,
Theme,
lighten,
alpha,
} from '@material-ui/core';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { BackstageTheme } from '@backstage/theme';
import {
sidebarConfig,
SidebarPinStateContext,
} from '@backstage/core-components';
import { techdocsStorageApiRef } from '../../api';
import {
addBaseUrl,
addGitFeedbackLink,
addLinkClickListener,
addSidebarToggle,
injectCss,
onCssReady,
removeMkdocsHeader,
rewriteDocLinks,
sanitizeDOM,
simplifyMkdocsFooter,
scrollIntoAnchor,
transform as transformer,
copyToClipboard,
} from '../transformers';
import { TechDocsSearch } from '../../search';
import { TechDocsStateIndicator } from './TechDocsStateIndicator';
import { useReaderState } from './useReaderState';
/**
* Props for {@link Reader}
*
* @public
*/
export type ReaderProps = {
entityRef: CompoundEntityRef;
withSearch?: boolean;
onReady?: () => void;
};
const useStyles = makeStyles<BackstageTheme>(theme => ({
searchBar: {
maxWidth: 'calc(100% - 16rem * 2 - 2.4rem)',
marginTop: 0,
marginBottom: theme.spacing(1),
marginLeft: 'calc(16rem + 1.2rem)',
'@media screen and (max-width: 76.1875em)': {
marginLeft: '0',
maxWidth: '100%',
},
},
}));
type TechDocsReaderValue = ReturnType<typeof useReaderState>;
const TechDocsReaderContext = createContext<TechDocsReaderValue>(
{} as TechDocsReaderValue,
);
const TechDocsReaderProvider = ({
children,
entityRef,
}: PropsWithChildren<{ entityRef: CompoundEntityRef }>) => {
const { '*': path } = useParams();
const { kind, namespace, name } = entityRef;
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>, entityRef: CompoundEntityRef) =>
(props: T) =>
(
<TechDocsReaderProvider entityRef={entityRef}>
<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);
type TypographyHeadings = Pick<
Theme['typography'],
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
>;
type TypographyHeadingsKeys = keyof TypographyHeadings;
const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
/**
* 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 = (
entityRef: CompoundEntityRef,
): Element | null => {
const navigate = useNavigate();
const theme = useTheme<BackstageTheme>();
const techdocsStorageApi = useApi(techdocsStorageApiRef);
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
const techdocsSanitizer = useApi(configApiRef);
const { namespace = '', kind = '', name = '' } = entityRef;
const { state, path, content: rawPage } = useTechDocsReader();
const isDarkTheme = theme.palette.type === 'dark';
const [sidebars, setSidebars] = useState<HTMLElement[]>();
const [dom, setDom] = useState<HTMLElement | null>(null);
// sidebar pinned status to be used in computing CSS style injections
const { isPinned } = useContext(SidebarPinStateContext);
const updateSidebarPosition = useCallback(() => {
if (!dom || !sidebars) return;
// set sidebar height so they don't initially render in wrong position
const mdTabs = dom.querySelector('.md-container > .md-tabs');
const sidebarsCollapsed = window.matchMedia(
'screen and (max-width: 76.1875em)',
).matches;
const newTop = Math.max(dom.getBoundingClientRect().top, 0);
sidebars.forEach(sidebar => {
if (sidebarsCollapsed) {
sidebar.style.top = '0px';
} else if (mdTabs) {
sidebar.style.top = `${
newTop + mdTabs.getBoundingClientRect().height
}px`;
} else {
sidebar.style.top = `${newTop}px`;
}
});
}, [dom, sidebars]);
useEffect(() => {
updateSidebarPosition();
window.addEventListener('scroll', updateSidebarPosition, true);
window.addEventListener('resize', updateSidebarPosition);
return () => {
window.removeEventListener('scroll', updateSidebarPosition, true);
window.removeEventListener('resize', updateSidebarPosition);
};
// an update to "state" might lead to an updated UI so we include it as a trigger
}, [updateSidebarPosition, state]);
// dynamically set width of footer to accommodate for pinning of the sidebar
const updateFooterWidth = useCallback(() => {
if (!dom) return;
const footer = dom.querySelector('.md-footer') as HTMLElement;
if (footer) {
footer.style.width = `${dom.getBoundingClientRect().width}px`;
}
}, [dom]);
useEffect(() => {
updateFooterWidth();
window.addEventListener('resize', updateFooterWidth);
return () => {
window.removeEventListener('resize', updateFooterWidth);
};
});
// a function that performs transformations that are executed prior to adding it to the DOM
const preRender = useCallback(
(rawContent: string, contentPath: string) =>
transformer(rawContent, [
sanitizeDOM(techdocsSanitizer.getOptionalConfig('techdocs.sanitizer')),
addBaseUrl({
techdocsStorageApi,
entityId: {
kind,
name,
namespace,
},
path: contentPath,
}),
rewriteDocLinks(),
addSidebarToggle(),
removeMkdocsHeader(),
simplifyMkdocsFooter(),
addGitFeedbackLink(scmIntegrationsApi),
injectCss({
// Variables
css: `
/*
As the MkDocs output is rendered in shadow DOM, the CSS variable definitions on the root selector are not applied. Instead, they have to be applied on :host.
As there is no way to transform the served main*.css yet (for example in the backend), we have to copy from main*.css and modify them.
*/
:host {
/* FONT */
--md-default-fg-color: ${theme.palette.text.primary};
--md-default-fg-color--light: ${theme.palette.text.secondary};
--md-default-fg-color--lighter: ${lighten(
theme.palette.text.secondary,
0.7,
)};
--md-default-fg-color--lightest: ${lighten(
theme.palette.text.secondary,
0.3,
)};
/* BACKGROUND */
--md-default-bg-color:${theme.palette.background.default};
--md-default-bg-color--light: ${theme.palette.background.paper};
--md-default-bg-color--lighter: ${lighten(
theme.palette.background.paper,
0.7,
)};
--md-default-bg-color--lightest: ${lighten(
theme.palette.background.paper,
0.3,
)};
/* PRIMARY */
--md-primary-fg-color: ${theme.palette.primary.main};
--md-primary-fg-color--light: ${theme.palette.primary.light};
--md-primary-fg-color--dark: ${theme.palette.primary.dark};
--md-primary-bg-color: ${theme.palette.primary.contrastText};
--md-primary-bg-color--light: ${lighten(
theme.palette.primary.contrastText,
0.7,
)};
/* ACCENT */
--md-accent-fg-color: var(--md-primary-fg-color);
/* SHADOW */
--md-shadow-z1: ${theme.shadows[1]};
--md-shadow-z2: ${theme.shadows[2]};
--md-shadow-z3: ${theme.shadows[3]};
/* EXTENSIONS */
--md-admonition-fg-color: var(--md-default-fg-color);
--md-admonition-bg-color: var(--md-default-bg-color);
/* Admonitions and others are using SVG masks to define icons. These masks are defined as CSS variables. */
--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>');
--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>');
--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.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
--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="M0 0h24v24H0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-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="M0 0h24v24H0z" fill="none"/><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
--md-nav-icon--prev: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>');
--md-nav-icon--next: 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.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
--md-toc-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 9h14V7H3v2m0 4h14v-2H3v2m0 4h14v-2H3v2m16 0h2v-2h-2v2m0-10v2h2V7h-2m0 6h2v-2h-2v2z"/></svg>');
--md-clipboard-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 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1z"/></svg>');
--md-search-result-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h7c-.41-.25-.8-.56-1.14-.9-.33-.33-.61-.7-.86-1.1H6V4h7v5h5v1.18c.71.16 1.39.43 2 .82V8l-6-6m6.31 16.9c1.33-2.11.69-4.9-1.4-6.22-2.11-1.33-4.91-.68-6.22 1.4-1.34 2.11-.69 4.89 1.4 6.22 1.46.93 3.32.93 4.79.02L22 23.39 23.39 22l-3.08-3.1m-3.81.1a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5z"/></svg>');
--md-source-forks-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z"/></svg>');
--md-source-repositories-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 1 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 0 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 0 1 1-1h8zM5 12.25v3.25a.25.25 0 0 0 .4.2l1.45-1.087a.25.25 0 0 1 .3 0L8.6 15.7a.25.25 0 0 0 .4-.2v-3.25a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25z"/></svg>');
--md-source-stars-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694v.001z"/></svg>');
--md-source-version-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 0 1 .25-.25h5.025a.25.25 0 0 1 .177.073l6.25 6.25a.25.25 0 0 1 0 .354l-5.025 5.025a.25.25 0 0 1-.354 0l-6.25-6.25a.25.25 0 0 1-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.75 1.75 0 0 1 1 7.775zM6 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></svg>');
--md-version-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc.--><path d="m310.6 246.6-127.1 128c-7.1 6.3-15.3 9.4-23.5 9.4s-16.38-3.125-22.63-9.375l-127.1-128C.224 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75s3.12 25.75-6.08 34.85z"/></svg>');
}
:host > * {
/* CODE */
--md-code-fg-color: ${theme.palette.text.primary};
--md-code-bg-color: ${theme.palette.background.paper};
--md-code-hl-color: ${alpha(theme.palette.warning.main, 0.5)};
--md-code-hl-keyword-color: ${
isDarkTheme
? theme.palette.primary.light
: theme.palette.primary.dark
};
--md-code-hl-function-color: ${
isDarkTheme
? theme.palette.secondary.light
: theme.palette.secondary.dark
};
--md-code-hl-string-color: ${
isDarkTheme
? theme.palette.success.light
: theme.palette.success.dark
};
--md-code-hl-number-color: ${
isDarkTheme ? theme.palette.error.light : theme.palette.error.dark
};
--md-code-hl-constant-color: var(--md-code-hl-function-color);
--md-code-hl-special-color: var(--md-code-hl-function-color);
--md-code-hl-name-color: var(--md-code-fg-color);
--md-code-hl-comment-color: var(--md-default-fg-color--light);
--md-code-hl-generic-color: var(--md-default-fg-color--light);
--md-code-hl-variable-color: var(--md-default-fg-color--light);
--md-code-hl-operator-color: var(--md-default-fg-color--light);
--md-code-hl-punctuation-color: var(--md-default-fg-color--light);
/* TYPESET */
--md-typeset-font-size: 1rem;
--md-typeset-color: var(--md-default-fg-color);
--md-typeset-a-color: var(--md-accent-fg-color);
--md-typeset-table-color: ${theme.palette.text.primary};
--md-typeset-del-color: ${
isDarkTheme
? alpha(theme.palette.error.dark, 0.5)
: alpha(theme.palette.error.light, 0.5)
};
--md-typeset-ins-color: ${
isDarkTheme
? alpha(theme.palette.success.dark, 0.5)
: alpha(theme.palette.success.light, 0.5)
};
--md-typeset-mark-color: ${
isDarkTheme
? alpha(theme.palette.warning.dark, 0.5)
: alpha(theme.palette.warning.light, 0.5)
};
}
@media screen and (max-width: 76.1875em) {
:host > * {
/* TYPESET */
--md-typeset-font-size: .9rem;
}
}
@media screen and (max-width: 600px) {
:host > * {
/* TYPESET */
--md-typeset-font-size: .7rem;
}
}
`,
}),
injectCss({
// Reset
css: `
body {
--md-text-color: var(--md-default-fg-color);
--md-text-link-color: var(--md-accent-fg-color);
--md-text-font-family: ${theme.typography.fontFamily};
font-family: var(--md-text-font-family);
background-color: unset;
}
`,
}),
injectCss({
// Layout
css: `
.md-grid {
max-width: 100%;
margin: 0;
}
.md-nav {
font-size: calc(var(--md-typeset-font-size) * 0.9);
}
.md-nav__link {
display: flex;
align-items: center;
justify-content: space-between;
}
.md-nav__icon {
height: 20px !important;
width: 20px !important;
margin-left:${theme.spacing(1)}px;
}
.md-nav__icon svg {
margin: 0;
width: 20px !important;
height: 20px !important;
}
.md-nav__icon:after {
width: 20px !important;
height: 20px !important;
}
.md-main__inner {
margin-top: 0;
}
.md-sidebar {
bottom: 75px;
position: fixed;
width: 16rem;
overflow-y: auto;
overflow-x: hidden;
scrollbar-color: rgb(193, 193, 193) #eee;
scrollbar-width: thin;
}
.md-sidebar::-webkit-scrollbar {
width: 5px;
}
.md-sidebar::-webkit-scrollbar-button {
width: 5px;
height: 5px;
}
.md-sidebar::-webkit-scrollbar-track {
background: #eee;
border: 1 px solid rgb(250, 250, 250);
box-shadow: 0px 0px 3px #dfdfdf inset;
border-radius: 3px;
}
.md-sidebar::-webkit-scrollbar-thumb {
width: 5px;
background: rgb(193, 193, 193);
border: transparent;
border-radius: 3px;
}
.md-sidebar::-webkit-scrollbar-thumb:hover {
background: rgb(125, 125, 125);
}
.md-sidebar--secondary {
right: ${theme.spacing(3)}px;
}
.md-sidebar__scrollwrap {
overflow: unset !important;
}
.md-content {
max-width: calc(100% - 16rem * 2);
margin-left: 16rem;
margin-bottom: 50px;
}
.md-footer {
position: fixed;
bottom: 0px;
}
.md-footer__title {
background-color: unset;
}
.md-footer__link, .md-footer-nav__link {
width: 16rem;
}
.md-dialog {
background-color: unset;
}
@media screen and (min-width: 76.25em) {
.md-sidebar {
height: auto;
}
}
@media screen and (max-width: 76.1875em) {
.md-nav {
transition: none !important;
background-color: var(--md-default-bg-color)
}
.md-nav--primary .md-nav__title {
cursor: auto;
color: var(--md-default-fg-color);
font-weight: 700;
white-space: normal;
line-height: 1rem;
height: auto;
display: flex;
flex-flow: column;
row-gap: 1.6rem;
padding: 1.2rem .8rem .8rem;
background-color: var(--md-default-bg-color);
}
.md-nav--primary .md-nav__title~.md-nav__list {
box-shadow: none;
}
.md-nav--primary .md-nav__title ~ .md-nav__list > :first-child {
border-top: none;
}
.md-nav--primary .md-nav__title .md-nav__button {
display: none;
}
.md-nav--primary .md-nav__title .md-nav__icon {
color: var(--md-default-fg-color);
position: static;
height: auto;
margin: 0 0 0 -0.2rem;
}
.md-nav--primary > .md-nav__title [for="none"] {
padding-top: 0;
}
.md-nav--primary .md-nav__item {
border-top: none;
}
.md-nav--primary :is(.md-nav__title,.md-nav__item) {
font-size : var(--md-typeset-font-size);
}
.md-nav .md-source {
display: none;
}
.md-sidebar {
height: 100%;
}
.md-sidebar--primary {
width: 16rem !important;
z-index: 200;
left: ${
isPinned
? `calc(-16rem + ${sidebarConfig.drawerWidthOpen}px)`
: `calc(-16rem + ${sidebarConfig.drawerWidthClosed}px)`
} !important;
}
.md-sidebar--secondary:not([hidden]) {
display: none;
}
[data-md-toggle=drawer]:checked~.md-container .md-sidebar--primary {
transform: translateX(16rem);
}
.md-content {
max-width: 100%;
margin-left: 0;
}
.md-content__inner {
margin: 0;
}
.md-content__inner .highlighttable {
max-width: 100%;
margin: 1em 0;
}
.md-header__button {
margin: 0.4rem 0;
margin-left: 0.4rem;
padding: 0;
}
.md-overlay {
left: 0;
}
.md-footer {
position: static;
padding-left: 0;
}
.md-footer__link, .md-footer-nav__link {
/* footer links begin to overlap at small sizes without setting width */
width: 50%;
}
}
@media screen and (max-width: 600px) {
.md-sidebar--primary {
left: -16rem !important;
width: 16rem;
}
.md-sidebar--primary .md-sidebar__scrollwrap {
bottom: ${sidebarConfig.mobileSidebarHeight}px;
}
}
`,
}),
injectCss({
// Typeset
css: `
.md-typeset {
font-size: var(--md-typeset-font-size);
}
${headings.reduce<string>((style, heading) => {
const styles = theme.typography[heading];
const { lineHeight, fontFamily, fontWeight, fontSize } = styles;
const calculate = (value: typeof fontSize) => {
let factor: number | string = 1;
if (typeof value === 'number') {
// 60% of the size defined because it is too big
factor = (value / 16) * 0.6;
}
if (typeof value === 'string') {
factor = value.replace('rem', '');
}
return `calc(${factor} * var(--md-typeset-font-size))`;
};
return style.concat(`
.md-typeset ${heading} {
color: var(--md-default-fg-color);
line-height: ${lineHeight};
font-family: ${fontFamily};
font-weight: ${fontWeight};
font-size: ${calculate(fontSize)};
}
`);
}, '')}
.md-typeset .md-content__button {
color: var(--md-default-fg-color);
}
.md-typeset hr {
border-bottom: 0.05rem dotted ${theme.palette.divider};
}
.md-typeset details {
font-size: var(--md-typeset-font-size) !important;
}
.md-typeset details summary {
padding-left: 2.5rem !important;
}
.md-typeset details summary:before,
.md-typeset details summary:after {
top: 50% !important;
width: 20px !important;
height: 20px !important;
transform: rotate(0deg) translateY(-50%) !important;
}
.md-typeset details[open] > summary:after {
transform: rotate(90deg) translateX(-50%) !important;
}
.md-typeset blockquote {
color: var(--md-default-fg-color--light);
border-left: 0.2rem solid var(--md-default-fg-color--light);
}
.md-typeset table:not([class]) {
font-size: var(--md-typeset-font-size);
border: 1px solid var(--md-default-fg-color);
border-bottom: none;
border-collapse: collapse;
}
.md-typeset table:not([class]) th {
font-weight: bold;
}
.md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
border-bottom: 1px solid var(--md-default-fg-color);
}
.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({
// Animations
css: `
/*
Disable CSS animations on link colors as they lead to issues in dark mode.
The dark mode color theme is applied later and theirfore there is always an animation from light to dark mode when navigation between pages.
*/
.md-dialog, .md-nav__link, .md-footer__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
transition: none;
}
`,
}),
injectCss({
// Extensions
css: `
/* HIGHLIGHT */
.highlight .md-clipboard:after {
content: unset;
}
.highlight .nx {
color: ${isDarkTheme ? '#ff53a3' : '#ec407a'};
}
/* CODE HILITE */
.codehilite .gd {
background-color: ${
isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd'
};
}
.codehilite .gi {
background-color: ${
isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd'
};
}
/* TABBED */
.tabbed-set>input:nth-child(1):checked~.tabbed-labels>:nth-child(1),
.tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),
.tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),
.tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),
.tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),
.tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),
.tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),
.tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),
.tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),
.tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),
.tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),
.tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),
.tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),
.tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),
.tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),
.tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),
.tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),
.tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),
.tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),
.tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20) {
color: var(--md-accent-fg-color);
border-color: var(--md-accent-fg-color);
}
/* TASK-LIST */
.task-list-control .task-list-indicator::before {
background-color: ${theme.palette.action.disabledBackground};
}
.task-list-control [type="checkbox"]:checked + .task-list-indicator:before {
background-color: ${theme.palette.success.main};
}
/* ADMONITION */
.admonition {
font-size: var(--md-typeset-font-size) !important;
}
.admonition .admonition-title {
padding-left: 2.5rem !important;
}
.admonition .admonition-title:before {
top: 50% !important;
width: 20px !important;
height: 20px !important;
transform: translateY(-50%) !important;
}
`,
}),
]),
[
kind,
name,
namespace,
scmIntegrationsApi,
techdocsSanitizer,
techdocsStorageApi,
theme,
isDarkTheme,
isPinned,
],
);
// a function that performs transformations that are executed after adding it to the DOM
const postRender = useCallback(
async (transformedElement: Element) =>
transformer(transformedElement, [
scrollIntoAnchor(),
copyToClipboard(theme),
addLinkClickListener({
baseUrl: window.location.origin,
onClick: (event: MouseEvent, url: string) => {
// detect if CTRL or META keys are pressed so that links can be opened in a new tab with `window.open`
const modifierActive = event.ctrlKey || event.metaKey;
const parsedUrl = new URL(url);
// hash exists when anchor is clicked on secondary sidebar
if (parsedUrl.hash) {
if (modifierActive) {
window.open(`${parsedUrl.pathname}${parsedUrl.hash}`, '_blank');
} else {
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
// Scroll to hash if it's on the current page
transformedElement
?.querySelector(`[id='${parsedUrl.hash.slice(1)}']`)
?.scrollIntoView();
}
} else {
if (modifierActive) {
window.open(parsedUrl.pathname, '_blank');
} else {
navigate(parsedUrl.pathname);
// Scroll to top of reader if primary sidebar link is clicked
transformedElement
?.querySelector('.md-content__inner')
?.scrollIntoView();
}
}
},
}),
onCssReady({
docStorageUrl: await techdocsStorageApi.getApiOrigin(),
onLoading: (renderedElement: Element) => {
(renderedElement as HTMLElement).style.setProperty('opacity', '0');
},
onLoaded: (renderedElement: Element) => {
(renderedElement as HTMLElement).style.removeProperty('opacity');
// disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
renderedElement
.querySelector('.md-nav__title')
?.removeAttribute('for');
setSidebars(
Array.from(renderedElement.querySelectorAll('.md-sidebar')),
);
},
}),
]),
[theme, navigate, techdocsStorageApi],
);
useEffect(() => {
if (!rawPage) return () => {};
// if false, there is already a newer execution of this effect
let shouldReplaceContent = true;
// Pre-render
preRender(rawPage, path).then(async preTransformedDomElement => {
if (!preTransformedDomElement?.innerHTML) {
return; // An unexpected error occurred
}
// don't manipulate the shadow dom if this isn't the latest effect execution
if (!shouldReplaceContent) {
return;
}
// Scroll to top after render
window.scroll({ top: 0 });
// Post-render
const postTransformedDomElement = await postRender(
preTransformedDomElement,
);
setDom(postTransformedDomElement as HTMLElement);
});
// cancel this execution
return () => {
shouldReplaceContent = false;
};
}, [rawPage, path, preRender, postRender]);
return dom;
};
const TheReader = ({
entityRef,
onReady = () => {},
withSearch = true,
}: ReaderProps) => {
const classes = useStyles();
const dom = useTechDocsReaderDom(entityRef);
const shadowDomRef = useRef<HTMLDivElement>(null);
const onReadyRef = useRef<() => void>(onReady);
useEffect(() => {
onReadyRef.current = onReady;
}, [onReady]);
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);
onReadyRef.current();
// this hook must ONLY be triggered by a changed dom
}, [dom]);
return (
<>
<TechDocsStateIndicator />
{withSearch && shadowDomRef?.current?.shadowRoot?.innerHTML && (
<Grid container className={classes.searchBar}>
<TechDocsSearch entityId={entityRef} />
</Grid>
)}
<div data-testid="techdocs-content-shadowroot" ref={shadowDomRef} />
</>
);
};
/**
* Component responsible for rendering TechDocs documentation
*
* @public
*/
export const Reader = (props: ReaderProps) => {
const { entityRef, onReady = () => {}, withSearch = true } = props;
return (
<TechDocsReaderProvider entityRef={entityRef}>
<TheReader
entityRef={entityRef}
onReady={onReady}
withSearch={withSearch}
/>
</TechDocsReaderProvider>
);
};
@@ -1,176 +0,0 @@
/*
* Copyright 2020 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 { TechDocsReaderPage } from './TechDocsReaderPage';
import { render, act } from '@testing-library/react';
import { ConfigReader } from '@backstage/config';
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { TestApiRegistry, wrapInTestApp } from '@backstage/test-utils';
import { Header } from '@backstage/core-components';
import {
techdocsApiRef,
TechDocsApi,
techdocsStorageApiRef,
TechDocsStorageApi,
} from '../../api';
import { ApiProvider } from '@backstage/core-app-api';
import { searchApiRef } from '@backstage/plugin-search';
jest.mock('react-router-dom', () => {
const actual = jest.requireActual('react-router-dom');
return {
...actual,
useParams: jest.fn(),
};
});
jest.mock('./TechDocsReaderPageHeader', () => {
return {
__esModule: true,
TechDocsReaderPageHeader: () => <div />,
};
});
const { useParams }: { useParams: jest.Mock } =
jest.requireMock('react-router-dom');
global.scroll = jest.fn();
describe('<TechDocsReaderPage />', () => {
it('should render techdocs page', async () => {
useParams.mockReturnValue({
entityRef: 'Component::backstage',
});
const scmIntegrationsApi: ScmIntegrationsApi =
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
);
const techdocsApi: Partial<TechDocsApi> = {
getEntityMetadata: () =>
Promise.resolve({
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'backstage',
},
}),
getTechDocsMetadata: () =>
Promise.resolve({
site_name: 'string',
site_description: 'string',
}),
};
const techdocsStorageApi: Partial<TechDocsStorageApi> = {
getEntityDocs: (): Promise<string> => Promise.resolve('String'),
getBaseUrl: (): Promise<string> => Promise.resolve('String'),
getApiOrigin: (): Promise<string> => Promise.resolve('String'),
};
const searchApi = {
query: () =>
Promise.resolve({
results: [],
}),
};
const apiRegistry = TestApiRegistry.from(
[scmIntegrationsApiRef, scmIntegrationsApi],
[techdocsApiRef, techdocsApi],
[techdocsStorageApiRef, techdocsStorageApi],
[searchApiRef, searchApi],
);
await act(async () => {
const rendered = render(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsReaderPage />
</ApiProvider>,
),
);
expect(rendered.getByTestId('techdocs-content')).toBeInTheDocument();
});
});
it('should render techdocs page with custom header', async () => {
useParams.mockReturnValue({
entityRef: 'Component::backstage',
});
const scmIntegrationsApi: ScmIntegrationsApi =
ScmIntegrationsApi.fromConfig(
new ConfigReader({
integrations: {},
}),
);
const techdocsApi: Partial<TechDocsApi> = {
getEntityMetadata: () =>
Promise.resolve({
apiVersion: 'v1',
kind: 'Component',
metadata: {
name: 'backstage',
},
}),
getTechDocsMetadata: () =>
Promise.resolve({
site_name: 'string',
site_description: 'string',
}),
};
const techdocsStorageApi: Partial<TechDocsStorageApi> = {
getEntityDocs: (): Promise<string> => Promise.resolve('String'),
getBaseUrl: (): Promise<string> => Promise.resolve('String'),
getApiOrigin: (): Promise<string> => Promise.resolve('String'),
};
const searchApi = {
query: () =>
Promise.resolve({
results: [],
}),
};
const apiRegistry = TestApiRegistry.from(
[scmIntegrationsApiRef, scmIntegrationsApi],
[techdocsApiRef, techdocsApi],
[techdocsStorageApiRef, techdocsStorageApi],
[searchApiRef, searchApi],
);
await act(async () => {
const rendered = render(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsReaderPage>
{({ techdocsMetadataValue }) => (
<Header
type="documentation"
title="A custom header"
subtitle={techdocsMetadataValue?.site_name}
/>
)}
</TechDocsReaderPage>
</ApiProvider>,
),
);
expect(rendered.getByText('A custom header')).toBeInTheDocument();
});
});
});
@@ -1,140 +0,0 @@
/*
* Copyright 2020 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, { useCallback, useState } from 'react';
import { useOutlet } from 'react-router';
import { useParams } from 'react-router-dom';
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
import { techdocsApiRef } from '../../api';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { getComponentData, useApi, useApp } from '@backstage/core-plugin-api';
import { Page } from '@backstage/core-components';
import {
TechDocsReaderPage as AddonAwareReaderPage,
TECHDOCS_ADDONS_WRAPPER_KEY,
} from '@backstage/plugin-techdocs-addons';
import { useTechDocsReaderDom, withTechDocsReaderProvider } from './Reader';
/**
* Helper function that gives the children of {@link TechDocsReaderPage} access to techdocs and entity metadata
*
* @public
*/
export type TechDocsReaderPageRenderFunction = ({
techdocsMetadataValue,
entityMetadataValue,
entityRef,
}: {
techdocsMetadataValue?: TechDocsMetadata | undefined;
entityMetadataValue?: TechDocsEntityMetadata | undefined;
entityRef: CompoundEntityRef;
onReady: () => void;
}) => JSX.Element;
type SpecialReaderPageProps = {
entityName: CompoundEntityRef;
asyncEntityMetadata: AsyncState<TechDocsEntityMetadata>;
asyncTechDocsMetadata: AsyncState<TechDocsMetadata>;
};
const SpecialReaderPage = (props: SpecialReaderPageProps) => {
const dom = useTechDocsReaderDom(props.entityName);
return (
<AddonAwareReaderPage
asyncEntityMetadata={props.asyncEntityMetadata}
asyncTechDocsMetadata={props.asyncTechDocsMetadata}
dom={dom}
/>
);
};
/**
* Props for {@link TechDocsReaderPage}
*
* @public
*/
export type TechDocsReaderPageProps = {
children?: TechDocsReaderPageRenderFunction | React.ReactNode;
};
export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
const { children } = props;
const { NotFoundErrorPage } = useApp().getComponents();
const outlet = useOutlet();
const [documentReady, setDocumentReady] = useState<boolean>(false);
const { namespace, kind, name } = useParams();
const techdocsApi = useApi(techdocsApiRef);
const asyncTechDocsMetadata = useAsync(() => {
if (documentReady) {
return techdocsApi.getTechDocsMetadata({ kind, namespace, name });
}
return Promise.resolve(undefined);
}, [kind, namespace, name, techdocsApi, documentReady]);
const asyncEntityMetadata = useAsync(() => {
return techdocsApi.getEntityMetadata({ kind, namespace, name });
}, [kind, namespace, name, techdocsApi]);
const onReady = useCallback(() => {
setDocumentReady(true);
}, [setDocumentReady]);
if (asyncEntityMetadata.error) return <NotFoundErrorPage />;
if (!children) {
if (outlet) {
// If the outlet is a single child and that child is an instance of the
// TechDocsAddons registry, then render it a certain way.
if (
getComponentData(outlet.props.children, TECHDOCS_ADDONS_WRAPPER_KEY)
) {
const Component = withTechDocsReaderProvider(SpecialReaderPage, {
kind,
namespace,
name,
});
return (
<Component
entityName={{ kind, namespace, name }}
asyncEntityMetadata={asyncEntityMetadata}
asyncTechDocsMetadata={asyncTechDocsMetadata}
/>
);
}
// Otherwise, just return the outlet (legacy-style composability).
return outlet;
}
}
return (
<Page themeId="documentation">
{children instanceof Function
? children({
techdocsMetadataValue: asyncTechDocsMetadata.value,
entityMetadataValue: asyncEntityMetadata.value,
entityRef: { kind, namespace, name },
onReady,
})
: children}
</Page>
);
};
@@ -0,0 +1,95 @@
/*
* Copyright 2022 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, { ReactNode, useMemo } from 'react';
import { useParams } from 'react-router-dom';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent';
import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader';
import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader';
import { TechDocsReaderPageRenderFunction } from '../../../types';
import {
TechDocsEntityProvider,
TechDocsMetadataProvider,
TechDocsReaderPageProvider,
} from './context';
export type TechDocsReaderLayoutProps = {
hideHeader?: boolean;
};
export const TechDocsReaderLayout = ({
hideHeader = false,
}: TechDocsReaderLayoutProps) => (
<>
{!hideHeader && <TechDocsReaderPageHeader />}
<TechDocsReaderPageSubheader />
<TechDocsReaderPageContent />
</>
);
/**
* @public
*/
export type TechDocsReaderPageProps = {
path?: string;
entityName?: CompoundEntityRef;
children?: TechDocsReaderPageRenderFunction | ReactNode;
};
/**
* An addon-aware implementation of the TechDocsReaderPage.
* @public
*/
export const TechDocsReaderPage = ({
path: defaultPath,
entityName: defaultEntityName,
children = <TechDocsReaderLayout />,
}: TechDocsReaderPageProps) => {
const params = useParams();
const path = useMemo(() => {
if (defaultPath) {
return defaultPath;
}
return params['*'] ?? '';
}, [params, defaultPath]);
const entityName = useMemo(() => {
if (defaultEntityName) {
return defaultEntityName;
}
return {
kind: params.kind,
name: params.name,
namespace: params.namespace,
};
}, [params, defaultEntityName]);
return (
<TechDocsMetadataProvider entityName={entityName}>
<TechDocsEntityProvider entityName={entityName}>
<TechDocsReaderPageProvider path={path} entityName={entityName}>
{children}
</TechDocsReaderPageProvider>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
};
@@ -0,0 +1,146 @@
/*
* Copyright 2022 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 { TechDocsMetadata } from './types';
import {
useEntityMetadata,
useTechDocsMetadata,
useTechDocsReaderPage,
TechDocsEntityProvider,
TechDocsMetadataProvider,
TechDocsReaderPageProvider,
} from './context';
import { renderHook, act } from '@testing-library/react-hooks';
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
const mockEntity: Entity = {
apiVersion: 'v1',
kind: 'Component',
metadata: { name: 'test-component', namespace: 'default' },
};
const mockTechDocsMetadata: TechDocsMetadata = {
site_name: 'test-componnet',
site_description: 'this is a test component',
};
const mockShadowRoot = () => {
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<h1>Shadow DOM Mock</h1>';
return shadowRoot;
};
const wrapper = ({
entityName = {
namespace: mockEntity.metadata.namespace!!,
kind: mockEntity.kind,
name: mockEntity.metadata.name,
},
children,
}: {
entityName: CompoundEntityRef;
children: React.ReactNode;
}) => (
<TechDocsMetadataProvider
asyncValue={{
loading: false,
error: undefined,
value: mockTechDocsMetadata,
}}
>
<TechDocsEntityProvider
asyncValue={{
loading: false,
error: undefined,
value: mockEntity,
}}
>
<TechDocsReaderPageProvider entityName={entityName}>
{children}
</TechDocsReaderPageProvider>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
describe('context', () => {
describe('useEntityMetadata', () => {
it('should return loading state', async () => {
const { result } = renderHook(() => useEntityMetadata());
await expect(result.current.loading).toEqual(true);
});
it('should return expected entity values', async () => {
const { result } = renderHook(() => useEntityMetadata(), { wrapper });
expect(result.current.value).toBeDefined();
expect(result.current.error).toBeUndefined();
expect(result.current.value).toMatchObject(mockEntity);
});
});
describe('useTechDocsMetadata', () => {
it('should return loading state', async () => {
const { result } = renderHook(() => useTechDocsMetadata());
await expect(result.current.loading).toEqual(true);
});
it('should return expected techdocs metadata values', async () => {
const { result } = renderHook(() => useTechDocsMetadata(), { wrapper });
expect(result.current.value).toBeDefined();
expect(result.current.error).toBeUndefined();
expect(result.current.value).toMatchObject(mockTechDocsMetadata);
});
});
describe('useTechDocsReaderPage', () => {
it('should set title', () => {
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
expect(result.current.title).toBe('');
act(() => result.current.setTitle('test site title'));
expect(result.current.title).toBe('test site title');
});
it('should set subtitle', () => {
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
expect(result.current.subtitle).toBe('');
act(() => result.current.setSubtitle('test site subtitle'));
expect(result.current.subtitle).toBe('test site subtitle');
});
it('should set shadow root', async () => {
const { result } = renderHook(() => useTechDocsReaderPage(), { wrapper });
// mock shadowroot
const shadowRoot = mockShadowRoot();
act(() => result.current.setShadowRoot(shadowRoot));
expect(result.current.shadowRoot?.innerHTML).toBe(
'<h1>Shadow DOM Mock</h1>',
);
});
});
});
@@ -0,0 +1,175 @@
/*
* Copyright 2022 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, {
createContext,
Dispatch,
PropsWithChildren,
SetStateAction,
useContext,
useState,
} from 'react';
import useAsync, { AsyncState } from 'react-use/lib/useAsync';
import { Page } from '@backstage/core-components';
import { useApi } from '@backstage/core-plugin-api';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { techdocsApiRef } from '../../../api';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../../types';
type PropsWithEntityName<T = {}> = PropsWithChildren<
T & { entityName: CompoundEntityRef }
>;
const initialContextValue = {
loading: true,
error: undefined,
value: undefined,
};
const TechDocsMetadataContext =
createContext<AsyncState<TechDocsMetadata>>(initialContextValue);
export const TechDocsMetadataProvider = ({
entityName,
children,
}: PropsWithEntityName) => {
const techdocsApi = useApi(techdocsApiRef);
const value = useAsync(async () => {
return techdocsApi.getTechDocsMetadata(entityName);
}, [entityName]);
return (
<TechDocsMetadataContext.Provider value={value}>
{children}
</TechDocsMetadataContext.Provider>
);
};
/**
* Hook for use within TechDocs addons to retrieve TechDocs Metadata for the
* current TechDocs site.
* @public
*/
export const useTechDocsMetadata = () => {
return useContext(TechDocsMetadataContext);
};
const TechDocsEntityContext =
createContext<AsyncState<TechDocsEntityMetadata>>(initialContextValue);
export const TechDocsEntityProvider = ({
entityName,
children,
}: PropsWithEntityName) => {
const techdocsApi = useApi(techdocsApiRef);
const value = useAsync(async () => {
return techdocsApi.getEntityMetadata(entityName);
}, [entityName]);
return (
<TechDocsEntityContext.Provider value={value}>
{children}
</TechDocsEntityContext.Provider>
);
};
/**
* Hook for use within TechDocs addons to retrieve Entity Metadata for the
* current TechDocs site.
* @public
*/
export const useEntityMetadata = () => {
return useContext(TechDocsEntityContext);
};
export type TechDocsReaderPageValue = {
path: string;
entityName: CompoundEntityRef;
shadowRoot?: ShadowRoot;
setShadowRoot: Dispatch<SetStateAction<ShadowRoot | undefined>>;
title: string;
setTitle: Dispatch<SetStateAction<string>>;
subtitle: string;
setSubtitle: Dispatch<SetStateAction<string>>;
};
export const defaultTechDocsReaderPageValue: TechDocsReaderPageValue = {
path: '',
title: '',
setTitle: () => {},
subtitle: '',
setSubtitle: () => {},
setShadowRoot: () => {},
entityName: { kind: '', name: '', namespace: '' },
};
export const TechDocsReaderPageContext = createContext<TechDocsReaderPageValue>(
defaultTechDocsReaderPageValue,
);
export const useTechDocsReaderPage = () => {
return useContext(TechDocsReaderPageContext);
};
type TechDocsReaderPageProviderProps = PropsWithEntityName<{
path: string;
}>;
export const TechDocsReaderPageProvider = ({
path,
entityName,
children,
}: TechDocsReaderPageProviderProps) => {
const metadata = useTechDocsMetadata();
const entityMetadata = useEntityMetadata();
const [title, setTitle] = useState(defaultTechDocsReaderPageValue.title);
const [subtitle, setSubtitle] = useState(
defaultTechDocsReaderPageValue.subtitle,
);
const [shadowRoot, setShadowRoot] = useState<ShadowRoot | undefined>(
defaultTechDocsReaderPageValue.shadowRoot,
);
const value = {
path,
entityName,
shadowRoot,
setShadowRoot,
title,
setTitle,
subtitle,
setSubtitle,
};
return (
<TechDocsReaderPageContext.Provider value={value}>
<Page themeId="documentation">
{children instanceof Function
? children({
entityRef: entityName,
techdocsMetadataValue: metadata.value,
entityMetadataValue: entityMetadata.value,
})
: children}
</Page>
</TechDocsReaderPageContext.Provider>
);
};
@@ -0,0 +1,51 @@
/*
* Copyright 2022 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 { useShadowRoot, useShadowRootElements } from './hooks';
import { renderHook } from '@testing-library/react-hooks';
const mockShadowRoot = () => {
const div = document.createElement('div');
const shadowRoot = div.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '<h1>Shadow DOM Mock</h1>';
return shadowRoot;
};
const shadowRoot = mockShadowRoot();
jest.mock('./context', () => {
return {
useTechDocsReaderPage: () => ({ shadowRoot }),
};
});
describe('hooks', () => {
describe('useShadowRoot', () => {
it('should return shadow root', async () => {
const { result } = renderHook(() => useShadowRoot());
expect(result.current?.innerHTML).toBe(shadowRoot.innerHTML);
});
});
describe('useShadowRootElements', () => {
it('should return shadow root elements based on selector', () => {
const { result } = renderHook(() => useShadowRootElements(['h1']));
expect(result.current).toHaveLength(1);
});
});
});
@@ -0,0 +1,51 @@
/*
* Copyright 2022 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 { useTechDocsReaderPage } from './context';
/**
* Hook for use within TechDocs addons that provides access to the underlying
* shadow root of the current page, allowing the DOM within to be mutated.
* @public
*/
export const useShadowRoot = () => {
const { shadowRoot } = useTechDocsReaderPage();
return shadowRoot;
};
/**
* Convenience hook for use within TechDocs addons that provides access to
* elements that match a given selector within the shadow root.
*
* todo(backstage/techdocs-core): Consider extending `selectors` from string[]
* to some kind of typed object array, so users have more control over the
* shape of the result. e.g. a flag to indicate querySelector vs.
* querySelectorAll.
*
* @public
*/
export const useShadowRootElements = <
TReturnedElement extends HTMLElement = HTMLElement,
>(
selectors: string[],
): TReturnedElement[] => {
const shadowRoot = useShadowRoot();
if (!shadowRoot) return [];
return selectors
.map(selector => shadowRoot?.querySelectorAll<TReturnedElement>(selector))
.filter(nodeList => nodeList.length)
.map(nodeList => Array.from(nodeList))
.flat();
};
@@ -0,0 +1,23 @@
/*
* Copyright 2022 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.
*/
export { TechDocsReaderPage, TechDocsReaderLayout } from './TechDocsReaderPage';
export type {
TechDocsReaderPageProps,
TechDocsReaderLayoutProps,
} from './TechDocsReaderPage';
export * from './context';
export * from './hooks';
@@ -0,0 +1,136 @@
/*
* Copyright 2022 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, { useEffect, useRef, useState } from 'react';
import { create } from 'jss';
import { makeStyles, Grid, Portal } from '@material-ui/core';
import { StylesProvider, jssPreset } from '@material-ui/styles';
import {
useTechDocsAddons,
TechDocsAddonLocations as locations,
} from '@backstage/techdocs-addons';
import { Content, Progress } from '@backstage/core-components';
import { TechDocsSearch } from '../../../search';
import { useTechDocsReaderPage } from '../TechDocsReaderPage';
import { TechDocsStateIndicator } from '../TechDocsStateIndicator';
import { useTechDocsReaderDom, withTechDocsReaderProvider } from './context';
const useStyles = makeStyles({
search: {
width: '100%',
'@media (min-width: 76.1875em)': {
width: 'calc(100% - 34.4rem)',
margin: '0 auto',
},
},
});
export type TechDocsReaderPageContentProps = {
withSearch?: boolean;
};
export const TechDocsReaderPageContent = withTechDocsReaderProvider(
({ withSearch = true }: TechDocsReaderPageContentProps) => {
const classes = useStyles();
const addons = useTechDocsAddons();
const page = useTechDocsReaderPage();
const dom = useTechDocsReaderDom(page.entityName);
const ref = useRef<HTMLDivElement>(null);
const [jss, setJss] = useState(
create({
...jssPreset(),
insertionPoint: undefined,
}),
);
useEffect(() => {
const shadowHost = ref.current;
if (!dom || !shadowHost) return;
setJss(
create({
...jssPreset(),
insertionPoint: dom.querySelector('head') || undefined,
}),
);
const shadowRoot =
shadowHost.shadowRoot ?? shadowHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '';
shadowRoot.appendChild(dom);
page.setShadowRoot(shadowRoot);
}, [dom, page]);
const contentElement = ref.current?.shadowRoot?.querySelector(
'[data-md-component="container"]',
);
const primarySidebarElement = ref.current?.shadowRoot?.querySelector(
'div[data-md-component="sidebar"][data-md-type="navigation"], div[data-md-component="navigation"]',
);
const secondarySidebarElement = ref.current?.shadowRoot?.querySelector(
'div[data-md-component="sidebar"][data-md-type="toc"], div[data-md-component="toc"]',
);
const primarySidebarAddonLocation = document.createElement('div');
primarySidebarElement?.prepend(primarySidebarAddonLocation);
const secondarySidebarAddonLocation = document.createElement('div');
secondarySidebarElement?.prepend(secondarySidebarAddonLocation);
// do not return content until dom is ready
if (!dom) {
return (
<Content>
<Progress />
</Content>
);
}
return (
<Content>
<Grid container>
<Grid xs={12} item>
<TechDocsStateIndicator />
</Grid>
{withSearch && (
<Grid className={classes.search} xs="auto" item>
<TechDocsSearch entityId={page.entityName} />
</Grid>
)}
<Grid xs={12} item>
{/* sheetsManager={new Map()} is needed in order to deduplicate the injection of CSS in the page. */}
<StylesProvider jss={jss} sheetsManager={new Map()}>
<div ref={ref} data-testid="techdocs-native-shadowroot" />
<Portal container={primarySidebarAddonLocation}>
{addons.renderComponentsByLocation(locations.PRIMARY_SIDEBAR)}
</Portal>
<Portal container={contentElement}>
{addons.renderComponentsByLocation(locations.CONTENT)}
</Portal>
<Portal container={secondarySidebarAddonLocation}>
{addons.renderComponentsByLocation(locations.SECONDARY_SIDEBAR)}
</Portal>
</StylesProvider>
</Grid>
</Grid>
</Content>
);
},
);
@@ -0,0 +1,856 @@
/*
* Copyright 2020 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, {
FC,
ComponentType,
createContext,
useContext,
useCallback,
useEffect,
useState,
} from 'react';
import { useNavigate } from 'react-router-dom';
import { useTheme, Theme, lighten, alpha } from '@material-ui/core';
import { BackstageTheme } from '@backstage/theme';
import { CompoundEntityRef } from '@backstage/catalog-model';
import { useApi, configApiRef } from '@backstage/core-plugin-api';
import { SidebarPinStateContext } from '@backstage/core-components';
import { scmIntegrationsApiRef } from '@backstage/integration-react';
import { techdocsStorageApiRef } from '../../../api';
import {
addBaseUrl,
addGitFeedbackLink,
addLinkClickListener,
addSidebarToggle,
injectCss,
onCssReady,
removeMkdocsHeader,
rewriteDocLinks,
sanitizeDOM,
simplifyMkdocsFooter,
scrollIntoAnchor,
transform as transformer,
copyToClipboard,
} from '../../transformers';
import { useReaderState } from '../useReaderState';
import { useTechDocsReaderPage } from '../TechDocsReaderPage';
/**
* Props for {@link Reader}
*
* @public
*/
export type ReaderProps = {
entityRef: CompoundEntityRef;
withSearch?: boolean;
onReady?: () => void;
};
type TechDocsReaderValue = ReturnType<typeof useReaderState>;
const TechDocsReaderContext = createContext<TechDocsReaderValue>(
{} as TechDocsReaderValue,
);
export const TechDocsReaderProvider: FC = ({ children }) => {
const { path, entityName } = useTechDocsReaderPage();
const { kind, namespace, name } = entityName;
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);
type TypographyHeadings = Pick<
Theme['typography'],
'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
>;
type TypographyHeadingsKeys = keyof TypographyHeadings;
const headings: TypographyHeadingsKeys[] = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
/**
* 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 = (
entityRef: CompoundEntityRef,
): Element | null => {
const navigate = useNavigate();
const theme = useTheme<BackstageTheme>();
const techdocsStorageApi = useApi(techdocsStorageApiRef);
const scmIntegrationsApi = useApi(scmIntegrationsApiRef);
const techdocsSanitizer = useApi(configApiRef);
const { namespace = '', kind = '', name = '' } = entityRef;
const { state, path, content: rawPage } = useTechDocsReader();
const isDarkTheme = theme.palette.type === 'dark';
const [sidebars, setSidebars] = useState<HTMLElement[]>();
const [dom, setDom] = useState<HTMLElement | null>(null);
// sidebar pinned status to be used in computing CSS style injections
const { isPinned } = useContext(SidebarPinStateContext);
const updateSidebarPosition = useCallback(() => {
if (!dom || !sidebars) return;
// set sidebar height so they don't initially render in wrong position
const mdTabs = dom.querySelector('.md-container > .md-tabs');
const sidebarsCollapsed = window.matchMedia(
'screen and (max-width: 76.1875em)',
).matches;
const newTop = Math.max(dom.getBoundingClientRect().top, 0);
sidebars.forEach(sidebar => {
if (sidebarsCollapsed) {
sidebar.style.top = '0px';
} else if (mdTabs) {
sidebar.style.top = `${
newTop + mdTabs.getBoundingClientRect().height
}px`;
} else {
sidebar.style.top = `${newTop}px`;
}
});
}, [dom, sidebars]);
useEffect(() => {
updateSidebarPosition();
window.addEventListener('scroll', updateSidebarPosition, true);
window.addEventListener('resize', updateSidebarPosition);
return () => {
window.removeEventListener('scroll', updateSidebarPosition, true);
window.removeEventListener('resize', updateSidebarPosition);
};
// an update to "state" might lead to an updated UI so we include it as a trigger
}, [updateSidebarPosition, state]);
// dynamically set width of footer to accommodate for pinning of the sidebar
const updateFooterWidth = useCallback(() => {
if (!dom) return;
const footer = dom.querySelector('.md-footer') as HTMLElement;
if (footer) {
footer.style.width = `${dom.getBoundingClientRect().width}px`;
}
}, [dom]);
useEffect(() => {
updateFooterWidth();
window.addEventListener('resize', updateFooterWidth);
return () => {
window.removeEventListener('resize', updateFooterWidth);
};
});
// a function that performs transformations that are executed prior to adding it to the DOM
const preRender = useCallback(
(rawContent: string, contentPath: string) =>
transformer(rawContent, [
sanitizeDOM(techdocsSanitizer.getOptionalConfig('techdocs.sanitizer')),
addBaseUrl({
techdocsStorageApi,
entityId: {
kind,
name,
namespace,
},
path: contentPath,
}),
rewriteDocLinks(),
addSidebarToggle(),
removeMkdocsHeader(),
simplifyMkdocsFooter(),
addGitFeedbackLink(scmIntegrationsApi),
injectCss({
// Variables
css: `
/*
As the MkDocs output is rendered in shadow DOM, the CSS variable definitions on the root selector are not applied. Instead, they have to be applied on :host.
As there is no way to transform the served main*.css yet (for example in the backend), we have to copy from main*.css and modify them.
*/
:host {
/* FONT */
--md-default-fg-color: ${theme.palette.text.primary};
--md-default-fg-color--light: ${theme.palette.text.secondary};
--md-default-fg-color--lighter: ${lighten(
theme.palette.text.secondary,
0.7,
)};
--md-default-fg-color--lightest: ${lighten(
theme.palette.text.secondary,
0.3,
)};
/* BACKGROUND */
--md-default-bg-color:${theme.palette.background.default};
--md-default-bg-color--light: ${theme.palette.background.paper};
--md-default-bg-color--lighter: ${lighten(
theme.palette.background.paper,
0.7,
)};
--md-default-bg-color--lightest: ${lighten(
theme.palette.background.paper,
0.3,
)};
/* PRIMARY */
--md-primary-fg-color: ${theme.palette.primary.main};
--md-primary-fg-color--light: ${theme.palette.primary.light};
--md-primary-fg-color--dark: ${theme.palette.primary.dark};
--md-primary-bg-color: ${theme.palette.primary.contrastText};
--md-primary-bg-color--light: ${lighten(
theme.palette.primary.contrastText,
0.7,
)};
/* ACCENT */
--md-accent-fg-color: var(--md-primary-fg-color);
/* SHADOW */
--md-shadow-z1: ${theme.shadows[1]};
--md-shadow-z2: ${theme.shadows[2]};
--md-shadow-z3: ${theme.shadows[3]};
/* EXTENSIONS */
--md-admonition-fg-color: var(--md-default-fg-color);
--md-admonition-bg-color: var(--md-default-bg-color);
/* Admonitions and others are using SVG masks to define icons. These masks are defined as CSS variables. */
--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>');
--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>');
--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.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
--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="M0 0h24v24H0z" fill="none"/><path d="M19 5v14H5V5h14m0-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5c0-1.1-.9-2-2-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="M0 0h24v24H0z" fill="none"/><path d="M19 3H5c-1.11 0-2 .9-2 2v14c0 1.1.89 2 2 2h14c1.11 0 2-.9 2-2V5c0-1.1-.89-2-2-2zm-9 14l-5-5 1.41-1.41L10 14.17l7.59-7.59L19 8l-9 9z"/></svg>');
--md-nav-icon--prev: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11h12z"/></svg>');
--md-nav-icon--next: 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.58 13.17 12 8.59 7.41 10 6l6 6-6 6-1.41-1.42z"/></svg>');
--md-toc-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 9h14V7H3v2m0 4h14v-2H3v2m0 4h14v-2H3v2m16 0h2v-2h-2v2m0-10v2h2V7h-2m0 6h2v-2h-2v2z"/></svg>');
--md-clipboard-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 21H8V7h11m0-2H8a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h11a2 2 0 0 0 2-2V7a2 2 0 0 0-2-2m-3-4H4a2 2 0 0 0-2 2v14h2V3h12V1z"/></svg>');
--md-search-result-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h7c-.41-.25-.8-.56-1.14-.9-.33-.33-.61-.7-.86-1.1H6V4h7v5h5v1.18c.71.16 1.39.43 2 .82V8l-6-6m6.31 16.9c1.33-2.11.69-4.9-1.4-6.22-2.11-1.33-4.91-.68-6.22 1.4-1.34 2.11-.69 4.89 1.4 6.22 1.46.93 3.32.93 4.79.02L22 23.39 23.39 22l-3.08-3.1m-3.81.1a2.5 2.5 0 0 1-2.5-2.5 2.5 2.5 0 0 1 2.5-2.5 2.5 2.5 0 0 1 2.5 2.5 2.5 2.5 0 0 1-2.5 2.5z"/></svg>');
--md-source-forks-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M5 3.25a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm0 2.122a2.25 2.25 0 1 0-1.5 0v.878A2.25 2.25 0 0 0 5.75 8.5h1.5v2.128a2.251 2.251 0 1 0 1.5 0V8.5h1.5a2.25 2.25 0 0 0 2.25-2.25v-.878a2.25 2.25 0 1 0-1.5 0v.878a.75.75 0 0 1-.75.75h-4.5A.75.75 0 0 1 5 6.25v-.878zm3.75 7.378a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0zm3-8.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5z"/></svg>');
--md-source-repositories-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2 2.5A2.5 2.5 0 0 1 4.5 0h8.75a.75.75 0 0 1 .75.75v12.5a.75.75 0 0 1-.75.75h-2.5a.75.75 0 1 1 0-1.5h1.75v-2h-8a1 1 0 0 0-.714 1.7.75.75 0 0 1-1.072 1.05A2.495 2.495 0 0 1 2 11.5v-9zm10.5-1V9h-8c-.356 0-.694.074-1 .208V2.5a1 1 0 0 1 1-1h8zM5 12.25v3.25a.25.25 0 0 0 .4.2l1.45-1.087a.25.25 0 0 1 .3 0L8.6 15.7a.25.25 0 0 0 .4-.2v-3.25a.25.25 0 0 0-.25-.25h-3.5a.25.25 0 0 0-.25.25z"/></svg>');
--md-source-stars-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M8 .25a.75.75 0 0 1 .673.418l1.882 3.815 4.21.612a.75.75 0 0 1 .416 1.279l-3.046 2.97.719 4.192a.75.75 0 0 1-1.088.791L8 12.347l-3.766 1.98a.75.75 0 0 1-1.088-.79l.72-4.194L.818 6.374a.75.75 0 0 1 .416-1.28l4.21-.611L7.327.668A.75.75 0 0 1 8 .25zm0 2.445L6.615 5.5a.75.75 0 0 1-.564.41l-3.097.45 2.24 2.184a.75.75 0 0 1 .216.664l-.528 3.084 2.769-1.456a.75.75 0 0 1 .698 0l2.77 1.456-.53-3.084a.75.75 0 0 1 .216-.664l2.24-2.183-3.096-.45a.75.75 0 0 1-.564-.41L8 2.694v.001z"/></svg>');
--md-source-version-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" d="M2.5 7.775V2.75a.25.25 0 0 1 .25-.25h5.025a.25.25 0 0 1 .177.073l6.25 6.25a.25.25 0 0 1 0 .354l-5.025 5.025a.25.25 0 0 1-.354 0l-6.25-6.25a.25.25 0 0 1-.073-.177zm-1.5 0V2.75C1 1.784 1.784 1 2.75 1h5.025c.464 0 .91.184 1.238.513l6.25 6.25a1.75 1.75 0 0 1 0 2.474l-5.026 5.026a1.75 1.75 0 0 1-2.474 0l-6.25-6.25A1.75 1.75 0 0 1 1 7.775zM6 5a1 1 0 1 0 0 2 1 1 0 0 0 0-2z"/></svg>');
--md-version-icon: url('data:image/svg+xml;charset=utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 320 512"><!--! Font Awesome Free 6.0.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free (Icons: CC BY 4.0, Fonts: SIL OFL 1.1, Code: MIT License) Copyright 2022 Fonticons, Inc.--><path d="m310.6 246.6-127.1 128c-7.1 6.3-15.3 9.4-23.5 9.4s-16.38-3.125-22.63-9.375l-127.1-128C.224 237.5-2.516 223.7 2.438 211.8S19.07 192 32 192h255.1c12.94 0 24.62 7.781 29.58 19.75s3.12 25.75-6.08 34.85z"/></svg>');
}
:host > * {
/* CODE */
--md-code-fg-color: ${theme.palette.text.primary};
--md-code-bg-color: ${theme.palette.background.paper};
--md-code-hl-color: ${alpha(theme.palette.warning.main, 0.5)};
--md-code-hl-keyword-color: ${
isDarkTheme
? theme.palette.primary.light
: theme.palette.primary.dark
};
--md-code-hl-function-color: ${
isDarkTheme
? theme.palette.secondary.light
: theme.palette.secondary.dark
};
--md-code-hl-string-color: ${
isDarkTheme
? theme.palette.success.light
: theme.palette.success.dark
};
--md-code-hl-number-color: ${
isDarkTheme
? theme.palette.error.light
: theme.palette.error.dark
};
--md-code-hl-constant-color: var(--md-code-hl-function-color);
--md-code-hl-special-color: var(--md-code-hl-function-color);
--md-code-hl-name-color: var(--md-code-fg-color);
--md-code-hl-comment-color: var(--md-default-fg-color--light);
--md-code-hl-generic-color: var(--md-default-fg-color--light);
--md-code-hl-variable-color: var(--md-default-fg-color--light);
--md-code-hl-operator-color: var(--md-default-fg-color--light);
--md-code-hl-punctuation-color: var(--md-default-fg-color--light);
/* TYPESET */
--md-typeset-font-size: 1rem;
--md-typeset-color: var(--md-default-fg-color);
--md-typeset-a-color: var(--md-accent-fg-color);
--md-typeset-table-color: ${theme.palette.text.primary};
--md-typeset-del-color: ${
isDarkTheme
? alpha(theme.palette.error.dark, 0.5)
: alpha(theme.palette.error.light, 0.5)
};
--md-typeset-ins-color: ${
isDarkTheme
? alpha(theme.palette.success.dark, 0.5)
: alpha(theme.palette.success.light, 0.5)
};
--md-typeset-mark-color: ${
isDarkTheme
? alpha(theme.palette.warning.dark, 0.5)
: alpha(theme.palette.warning.light, 0.5)
};
}
@media screen and (max-width: 76.1875em) {
:host > * {
/* TYPESET */
--md-typeset-font-size: .9rem;
}
}
@media screen and (max-width: 600px) {
:host > * {
/* TYPESET */
--md-typeset-font-size: .7rem;
}
}
`,
}),
injectCss({
// Reset
css: `
body {
--md-text-color: var(--md-default-fg-color);
--md-text-link-color: var(--md-accent-fg-color);
--md-text-font-family: ${theme.typography.fontFamily};
font-family: var(--md-text-font-family);
background-color: unset;
}
`,
}),
injectCss({
// Layout
css: `
.md-grid {
max-width: 100%;
margin: 0;
}
.md-nav {
font-size: calc(var(--md-typeset-font-size) * 0.9);
}
.md-nav__link {
display: flex;
align-items: center;
justify-content: space-between;
}
.md-nav__icon {
height: 20px !important;
width: 20px !important;
margin-left:${theme.spacing(1)}px;
}
.md-nav__icon svg {
margin: 0;
width: 20px !important;
height: 20px !important;
}
.md-nav__icon:after {
width: 20px !important;
height: 20px !important;
}
.md-main__inner {
margin-top: 0;
}
.md-sidebar {
bottom: 75px;
position: fixed;
width: 16rem;
overflow-y: auto;
overflow-x: hidden;
scrollbar-color: rgb(193, 193, 193) #eee;
scrollbar-width: thin;
}
.md-sidebar .md-sidebar__scrollwrap {
width: calc(16rem - 10px);
}
.md-sidebar--secondary {
right: ${theme.spacing(3)}px;
}
.md-sidebar::-webkit-scrollbar {
width: 5px;
}
.md-sidebar::-webkit-scrollbar-button {
width: 5px;
height: 5px;
}
.md-sidebar::-webkit-scrollbar-track {
background: #eee;
border: 1 px solid rgb(250, 250, 250);
box-shadow: 0px 0px 3px #dfdfdf inset;
border-radius: 3px;
}
.md-sidebar::-webkit-scrollbar-thumb {
width: 5px;
background: rgb(193, 193, 193);
border: transparent;
border-radius: 3px;
}
.md-sidebar::-webkit-scrollbar-thumb:hover {
background: rgb(125, 125, 125);
}
.md-content {
max-width: calc(100% - 16rem * 2);
margin-left: 16rem;
margin-bottom: 50px;
}
.md-footer {
position: fixed;
bottom: 0px;
}
.md-footer__title {
background-color: unset;
}
.md-footer-nav__link {
width: 16rem;
}
.md-dialog {
background-color: unset;
}
@media screen and (min-width: 76.25em) {
.md-sidebar {
height: auto;
}
}
@media screen and (max-width: 76.1875em) {
.md-nav {
transition: none !important;
background-color: var(--md-default-bg-color)
}
.md-nav--primary .md-nav__title {
cursor: auto;
color: var(--md-default-fg-color);
font-weight: 700;
white-space: normal;
line-height: 1rem;
height: auto;
display: flex;
flex-flow: column;
row-gap: 1.6rem;
padding: 1.2rem .8rem .8rem;
background-color: var(--md-default-bg-color);
}
.md-nav--primary .md-nav__title~.md-nav__list {
box-shadow: none;
}
.md-nav--primary .md-nav__title ~ .md-nav__list > :first-child {
border-top: none;
}
.md-nav--primary .md-nav__title .md-nav__button {
display: none;
}
.md-nav--primary .md-nav__title .md-nav__icon {
color: var(--md-default-fg-color);
position: static;
height: auto;
margin: 0 0 0 -0.2rem;
}
.md-nav--primary > .md-nav__title [for="none"] {
padding-top: 0;
}
.md-nav--primary .md-nav__item {
border-top: none;
}
.md-nav--primary :is(.md-nav__title,.md-nav__item) {
font-size : var(--md-typeset-font-size);
}
.md-nav .md-source {
display: none;
}
.md-sidebar {
height: 100%;
}
.md-sidebar--primary {
width: 12.1rem !important;
z-index: 200;
left: ${
isPinned
? 'calc(-12.1rem + 242px)'
: 'calc(-12.1rem + 72px)'
} !important;
}
.md-sidebar--secondary:not([hidden]) {
display: none;
}
.md-content {
max-width: 100%;
margin-left: 0;
}
.md-header__button {
margin: 0.4rem 0;
margin-left: 0.4rem;
padding: 0;
}
.md-overlay {
left: 0;
}
.md-footer {
position: static;
padding-left: 0;
}
.md-footer-nav__link {
/* footer links begin to overlap at small sizes without setting width */
width: 50%;
}
}
@media screen and (max-width: 600px) {
.md-sidebar--primary {
left: -12.1rem !important;
width: 12.1rem;
}
}
`,
}),
injectCss({
// Typeset
css: `
.md-typeset {
font-size: var(--md-typeset-font-size);
}
${headings.reduce<string>((style, heading) => {
const styles = theme.typography[heading];
const { lineHeight, fontFamily, fontWeight, fontSize } = styles;
const calculate = (value: typeof fontSize) => {
let factor: number | string = 1;
if (typeof value === 'number') {
// 60% of the size defined because it is too big
factor = (value / 16) * 0.6;
}
if (typeof value === 'string') {
factor = value.replace('rem', '');
}
return `calc(${factor} * var(--md-typeset-font-size))`;
};
return style.concat(`
.md-typeset ${heading} {
color: var(--md-default-fg-color);
line-height: ${lineHeight};
font-family: ${fontFamily};
font-weight: ${fontWeight};
font-size: ${calculate(fontSize)};
}
`);
}, '')}
.md-typeset .md-content__button {
color: var(--md-default-fg-color);
}
.md-typeset hr {
border-bottom: 0.05rem dotted ${theme.palette.divider};
}
.md-typeset details {
font-size: var(--md-typeset-font-size) !important;
}
.md-typeset details summary {
padding-left: 2.5rem !important;
}
.md-typeset details summary:before,
.md-typeset details summary:after {
top: 50% !important;
width: 20px !important;
height: 20px !important;
transform: rotate(0deg) translateY(-50%) !important;
}
.md-typeset details[open] > summary:after {
transform: rotate(90deg) translateX(-50%) !important;
}
.md-typeset blockquote {
color: var(--md-default-fg-color--light);
border-left: 0.2rem solid var(--md-default-fg-color--light);
}
.md-typeset table:not([class]) {
font-size: var(--md-typeset-font-size);
border: 1px solid var(--md-default-fg-color);
border-bottom: none;
border-collapse: collapse;
}
.md-typeset table:not([class]) th {
font-weight: bold;
}
.md-typeset table:not([class]) td, .md-typeset table:not([class]) th {
border-bottom: 1px solid var(--md-default-fg-color);
}
.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({
// Animations
css: `
/*
Disable CSS animations on link colors as they lead to issues in dark mode.
The dark mode color theme is applied later and theirfore there is always an animation from light to dark mode when navigation between pages.
*/
.md-dialog, .md-nav__link, .md-footer__link, .md-typeset a, .md-typeset a::before, .md-typeset .headerlink {
transition: none;
}
`,
}),
injectCss({
// Extensions
css: `
/* HIGHLIGHT */
.highlight .md-clipboard:after {
content: unset;
}
.highlight .nx {
color: ${isDarkTheme ? '#ff53a3' : '#ec407a'};
}
/* CODE HILITE */
.codehilite .gd {
background-color: ${
isDarkTheme ? 'rgba(248,81,73,0.65)' : '#fdd'
};
}
.codehilite .gi {
background-color: ${
isDarkTheme ? 'rgba(46,160,67,0.65)' : '#dfd'
};
}
/* TABBED */
.tabbed-set>input:nth-child(1):checked~.tabbed-labels>:nth-child(1),
.tabbed-set>input:nth-child(2):checked~.tabbed-labels>:nth-child(2),
.tabbed-set>input:nth-child(3):checked~.tabbed-labels>:nth-child(3),
.tabbed-set>input:nth-child(4):checked~.tabbed-labels>:nth-child(4),
.tabbed-set>input:nth-child(5):checked~.tabbed-labels>:nth-child(5),
.tabbed-set>input:nth-child(6):checked~.tabbed-labels>:nth-child(6),
.tabbed-set>input:nth-child(7):checked~.tabbed-labels>:nth-child(7),
.tabbed-set>input:nth-child(8):checked~.tabbed-labels>:nth-child(8),
.tabbed-set>input:nth-child(9):checked~.tabbed-labels>:nth-child(9),
.tabbed-set>input:nth-child(10):checked~.tabbed-labels>:nth-child(10),
.tabbed-set>input:nth-child(11):checked~.tabbed-labels>:nth-child(11),
.tabbed-set>input:nth-child(12):checked~.tabbed-labels>:nth-child(12),
.tabbed-set>input:nth-child(13):checked~.tabbed-labels>:nth-child(13),
.tabbed-set>input:nth-child(14):checked~.tabbed-labels>:nth-child(14),
.tabbed-set>input:nth-child(15):checked~.tabbed-labels>:nth-child(15),
.tabbed-set>input:nth-child(16):checked~.tabbed-labels>:nth-child(16),
.tabbed-set>input:nth-child(17):checked~.tabbed-labels>:nth-child(17),
.tabbed-set>input:nth-child(18):checked~.tabbed-labels>:nth-child(18),
.tabbed-set>input:nth-child(19):checked~.tabbed-labels>:nth-child(19),
.tabbed-set>input:nth-child(20):checked~.tabbed-labels>:nth-child(20) {
color: var(--md-accent-fg-color);
border-color: var(--md-accent-fg-color);
}
/* TASK-LIST */
.task-list-control .task-list-indicator::before {
background-color: ${theme.palette.action.disabledBackground};
}
.task-list-control [type="checkbox"]:checked + .task-list-indicator:before {
background-color: ${theme.palette.success.main};
}
/* ADMONITION */
.admonition {
font-size: var(--md-typeset-font-size) !important;
}
.admonition .admonition-title {
padding-left: 2.5rem !important;
}
.admonition .admonition-title:before {
top: 50% !important;
width: 20px !important;
height: 20px !important;
transform: translateY(-50%) !important;
}
`,
}),
]),
[
kind,
name,
namespace,
scmIntegrationsApi,
techdocsSanitizer,
techdocsStorageApi,
theme,
isDarkTheme,
isPinned,
],
);
// a function that performs transformations that are executed after adding it to the DOM
const postRender = useCallback(
async (transformedElement: Element) =>
transformer(transformedElement, [
scrollIntoAnchor(),
copyToClipboard(theme),
addLinkClickListener({
baseUrl: window.location.origin,
onClick: (event: MouseEvent, url: string) => {
// detect if CTRL or META keys are pressed so that links can be opened in a new tab with `window.open`
const modifierActive = event.ctrlKey || event.metaKey;
const parsedUrl = new URL(url);
// hash exists when anchor is clicked on secondary sidebar
if (parsedUrl.hash) {
if (modifierActive) {
window.open(`${parsedUrl.pathname}${parsedUrl.hash}`, '_blank');
} else {
navigate(`${parsedUrl.pathname}${parsedUrl.hash}`);
// Scroll to hash if it's on the current page
transformedElement
?.querySelector(`#${parsedUrl.hash.slice(1)}`)
?.scrollIntoView();
}
} else {
if (modifierActive) {
window.open(parsedUrl.pathname, '_blank');
} else {
navigate(parsedUrl.pathname);
// Scroll to top of reader if primary sidebar link is clicked
transformedElement
?.querySelector('.md-content__inner')
?.scrollIntoView();
}
}
},
}),
onCssReady({
docStorageUrl: await techdocsStorageApi.getApiOrigin(),
onLoading: (renderedElement: Element) => {
(renderedElement as HTMLElement).style.setProperty('opacity', '0');
},
onLoaded: (renderedElement: Element) => {
(renderedElement as HTMLElement).style.removeProperty('opacity');
// disable MkDocs drawer toggling ('for' attribute => checkbox mechanism)
renderedElement
.querySelector('.md-nav__title')
?.removeAttribute('for');
setSidebars(
Array.from(renderedElement.querySelectorAll('.md-sidebar')),
);
},
}),
]),
[theme, navigate, techdocsStorageApi],
);
useEffect(() => {
if (!rawPage) return () => {};
// if false, there is already a newer execution of this effect
let shouldReplaceContent = true;
// Pre-render
preRender(rawPage, path).then(async preTransformedDomElement => {
if (!preTransformedDomElement?.innerHTML) {
return; // An unexpected error occurred
}
// don't manipulate the shadow dom if this isn't the latest effect execution
if (!shouldReplaceContent) {
return;
}
// Scroll to top after render
window.scroll({ top: 0 });
// Post-render
const postTransformedDomElement = await postRender(
preTransformedDomElement,
);
setDom(postTransformedDomElement as HTMLElement);
});
// cancel this execution
return () => {
shouldReplaceContent = false;
};
}, [rawPage, path, preRender, postRender]);
return dom;
};
@@ -0,0 +1,18 @@
/*
* Copyright 2022 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.
*/
export { TechDocsReaderPageContent } from './TechDocsReaderPageContent';
export * from './context';
@@ -1,5 +1,5 @@
/*
* Copyright 2020 The Backstage Authors
* Copyright 2022 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.
@@ -14,45 +14,62 @@
* limitations under the License.
*/
import React, { PropsWithChildren } from 'react';
import React, { FC, useEffect } from 'react';
import Helmet from 'react-helmet';
import { Skeleton } from '@material-ui/lab';
import CodeIcon from '@material-ui/icons/Code';
import { useRouteRef } from '@backstage/core-plugin-api';
import { Header, HeaderLabel } from '@backstage/core-components';
import { CompoundEntityRef, RELATION_OWNED_BY } from '@backstage/catalog-model';
import {
TechDocsAddonLocations as locations,
useTechDocsAddons,
} from '@backstage/techdocs-addons';
import {
EntityRefLink,
EntityRefLinks,
getEntityRelations,
} from '@backstage/plugin-catalog-react';
import { RELATION_OWNED_BY } from '@backstage/catalog-model';
import { Header, HeaderLabel } from '@backstage/core-components';
import { useRouteRef, configApiRef, useApi } from '@backstage/core-plugin-api';
import { rootRouteRef } from '../../routes';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types';
import {
useTechDocsReaderPage,
useTechDocsMetadata,
useEntityMetadata,
} from '../TechDocsReaderPage';
/**
* Props for {@link TechDocsReaderPageHeader}
*
* @public
*/
export type TechDocsReaderPageHeaderProps = PropsWithChildren<{
entityRef: CompoundEntityRef;
entityMetadata?: TechDocsEntityMetadata;
techDocsMetadata?: TechDocsMetadata;
}>;
import { rootRouteRef } from '../../../routes';
/**
* Component responsible for rendering a Header with metadata on TechDocs reader page.
*
* @public
*/
export const TechDocsReaderPageHeader = (
props: TechDocsReaderPageHeaderProps,
) => {
const { entityRef, entityMetadata, techDocsMetadata, children } = props;
const { name } = entityRef;
const skeleton = <Skeleton animation="wave" variant="text" height={40} />;
const { site_name: siteName, site_description: siteDescription } =
techDocsMetadata || {};
export const TechDocsReaderPageHeader: FC = props => {
const { children } = props;
const addons = useTechDocsAddons();
const configApi = useApi(configApiRef);
const { value: techDocsMetadata } = useTechDocsMetadata();
const { value: entityMetadata } = useEntityMetadata();
const {
title,
setTitle,
subtitle,
setSubtitle,
entityName: entityRef,
} = useTechDocsReaderPage();
useEffect(() => {
if (!techDocsMetadata) return;
setTitle(prevTitle => prevTitle || techDocsMetadata.site_name);
setSubtitle(
prevSubtitle =>
prevSubtitle || techDocsMetadata.site_description || 'Home',
);
}, [techDocsMetadata, setTitle, setSubtitle]);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const tabTitle = [subtitle, title, appTitle].filter(Boolean).join(' | ');
const { locationMetadata, spec } = entityMetadata || {};
const lifecycle = spec?.lifecycle;
@@ -109,16 +126,17 @@ export const TechDocsReaderPageHeader = (
return (
<Header
title={siteName ? siteName : '.'}
pageTitleOverride={siteName || name}
subtitle={
siteDescription && siteDescription !== 'None' ? siteDescription : ''
}
type="Docs"
type="Documentation"
typeLink={docsRootLink}
title={title || skeleton}
subtitle={subtitle || skeleton}
>
<Helmet titleTemplate="%s">
<title>{tabTitle}</title>
</Helmet>
{labels}
{children}
{addons.renderComponentsByLocation(locations.HEADER)}
</Header>
);
};
@@ -0,0 +1,17 @@
/*
* Copyright 2022 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.
*/
export { TechDocsReaderPageHeader } from './TechDocsReaderPageHeader';
@@ -0,0 +1,52 @@
/*
* Copyright 2022 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 { Box, Toolbar, ToolbarProps, withStyles } from '@material-ui/core';
import {
TechDocsAddonLocations as locations,
useTechDocsAddons,
} from '@backstage/techdocs-addons';
export const TechDocsReaderPageSubheader = withStyles(theme => ({
root: {
gridArea: 'pageSubheader',
flexDirection: 'column',
minHeight: 'auto',
padding: theme.spacing(3, 3, 0),
},
}))(({ ...rest }: ToolbarProps) => {
const addons = useTechDocsAddons();
if (!addons.renderComponentsByLocation(locations.SUBHEADER)) return null;
return (
<Toolbar {...rest}>
{addons.renderComponentsByLocation(locations.SUBHEADER) && (
<Box
display="flex"
justifyContent="flex-end"
width="100%"
flexWrap="wrap"
>
{addons.renderComponentsByLocation(locations.SUBHEADER)}
</Box>
)}
</Toolbar>
);
});
@@ -0,0 +1,17 @@
/*
* Copyright 2022 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.
*/
export { TechDocsReaderPageSubheader } from './TechDocsReaderPageSubheader';
@@ -21,7 +21,7 @@ import { Alert } from '@material-ui/lab';
import { TechDocsBuildLogs } from './TechDocsBuildLogs';
import { TechDocsNotFound } from './TechDocsNotFound';
import { useTechDocsReader } from './Reader';
import { useTechDocsReader } from './TechDocsReaderPageContent';
const useStyles = makeStyles(theme => ({
root: {
+10 -15
View File
@@ -14,23 +14,18 @@
* limitations under the License.
*/
export * from './Reader';
export type {
TechDocsReaderPageProps,
TechDocsReaderPageRenderFunction,
TechDocsReaderLayoutProps,
} from './TechDocsReaderPage';
export {
TechDocsReaderLayout,
useTechDocsMetadata,
useEntityMetadata,
useTechDocsReaderPage,
useShadowRoot,
useShadowRootElements,
} from './TechDocsReaderPage';
export * from './TechDocsReaderPageHeader';
export * from './TechDocsReaderPageContent';
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
*/
+16 -1
View File
@@ -14,7 +14,22 @@
* limitations under the License.
*/
import { Entity } from '@backstage/catalog-model';
import { Entity, CompoundEntityRef } from '@backstage/catalog-model';
/**
* Helper function that gives the children of {@link TechDocsReaderPage} access to techdocs and entity metadata
*
* @public
*/
export type TechDocsReaderPageRenderFunction = ({
techdocsMetadataValue,
entityMetadataValue,
entityRef,
}: {
techdocsMetadataValue?: TechDocsMetadata | undefined;
entityMetadataValue?: TechDocsEntityMetadata | undefined;
entityRef: CompoundEntityRef;
}) => JSX.Element;
/**
* Metadata for TechDocs page