Review feedback.

Co-authored-by: Emma Indal <emma.indahl@gmail.com>
Co-authored-by: Anders Näsman <andersn@spotify.com>

Signed-off-by: Eric Peterson <ericpeterson@spotify.com>
This commit is contained in:
Eric Peterson
2022-03-16 12:02:03 +01:00
committed by Emma Indal
parent ff1cc8bced
commit d8db6db3d7
13 changed files with 371 additions and 224 deletions
+5 -2
View File
@@ -18,7 +18,10 @@ then be composed within a Backstage app.
When you create a new Addon, it requires three things.
1. A `name` for debugging and analytics purposes)
2. A `location`, indicating where/how the addon will be rendered
2. A `location`, indicating where/how the addon will be rendered. Valid
locations include: `header`, `subheader`, `primary sidebar`,
`secondary sidebar`, `content`, and `component`. Values are available on an
enumerable `TechDocsAddonLocations` and are type-hinted.
3. A `component`, encapsulating the addon's logic and functionality
```tsx
@@ -31,7 +34,7 @@ import { StackOverflowSecondarySidebarAddon } from './components';
export const StackOverflowSecondarySidebar = yourBackstagePlugin.provide(
createTechDocsAddon({
name: 'StackOverflowSecondarySidebar',
type: TechDocsAddonLocations.SECONDARY_SIDEBAR,
location: TechDocsAddonLocations.SECONDARY_SIDEBAR,
component: StackOverflowSecondarySidebarAddon,
}),
);
+3 -3
View File
@@ -100,7 +100,7 @@ export const useTechDocsAddons = () => {
[collection],
);
const renderComponentWithName = useCallback(
const renderComponentByName = useCallback(
(name: string) => {
const data = options.find(option => option.name === name);
return data ? findAddonByData(data) : null;
@@ -108,7 +108,7 @@ export const useTechDocsAddons = () => {
[options, findAddonByData],
);
const renderComponentsWithLocation = useCallback(
const renderComponentsByLocation = useCallback(
(location: TechDocsAddonLocations) => {
const data = options.filter(option => option.location === location);
return data.length ? data.map(findAddonByData) : null;
@@ -116,5 +116,5 @@ export const useTechDocsAddons = () => {
[options, findAddonByData],
);
return { renderComponentWithName, renderComponentsWithLocation };
return { renderComponentByName, renderComponentsByLocation };
};
@@ -0,0 +1,64 @@
/*
* 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 { CompoundEntityRef } from '@backstage/catalog-model';
import { Page } from '@backstage/core-components';
// todo(backstage/techdocs-core): Export these from @backstage/plugin-techdocs
import {
withTechDocsReaderProvider,
// @ts-ignore
TechDocsStateIndicator as TechDocReaderPageIndicator,
} from '@backstage/plugin-techdocs';
import React from 'react';
import {
TechDocsMetadataProvider,
TechDocsEntityProvider,
TechDocsReaderPageProvider,
} from '../../context';
import { TechDocsReaderPageContent } from '../TechDocsReaderPageContent';
import { TechDocsReaderPageHeader } from '../TechDocsReaderPageHeader';
import { TechDocsReaderPageSubheader } from '../TechDocsReaderPageSubheader';
/**
* @public
*/
export type TechDocsReaderPageProps = { entityName: CompoundEntityRef };
/**
* An addon-aware implementation of the TechDocsReaderPage.
* @public
*/
export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
const { entityName } = props;
const Component = withTechDocsReaderProvider(() => {
return (
<TechDocsMetadataProvider entityName={entityName}>
<TechDocsEntityProvider entityName={entityName}>
<TechDocsReaderPageProvider entityName={entityName}>
<Page themeId="documentation">
<TechDocsReaderPageHeader />
<TechDocsReaderPageSubheader />
<TechDocReaderPageIndicator />
<TechDocsReaderPageContent />
</Page>
</TechDocsReaderPageProvider>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
}, entityName);
return <Component />;
};
@@ -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 { TechDocsReaderPage } from './TechDocsReaderPage';
export type { TechDocsReaderPageProps } from './TechDocsReaderPage';
@@ -0,0 +1,102 @@
/*
* 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 { Content, Progress } from '@backstage/core-components';
// todo(backstage/techdocs-core): Export these from @backstage/plugin-techdocs
// @ts-ignore
import { useTechDocsReaderDom } from '@backstage/plugin-techdocs';
import { Portal } from '@material-ui/core';
import { StylesProvider, jssPreset } from '@material-ui/styles';
import React, { useEffect, useRef, useState } from 'react';
import { create } from 'jss';
import { useTechDocsAddons } from '../../addons';
import { useTechDocsReaderPage } from '../../context';
import { TechDocsAddonLocations as locations } from '../../types';
export const TechDocsReaderPageContent = () => {
const ref = useRef<HTMLDivElement>(null);
const [jss, setJss] = useState(
create({
...jssPreset(),
insertionPoint: undefined,
}),
);
const addons = useTechDocsAddons();
const { entityName, setShadowRoot } = useTechDocsReaderPage();
const dom = useTechDocsReaderDom(entityName);
useEffect(() => {
const shadowHost = ref.current;
if (!dom || !shadowHost || shadowHost.shadowRoot) return;
setJss(
create({
...jssPreset(),
insertionPoint: dom.querySelector('head') || undefined,
}),
);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '';
shadowRoot.appendChild(dom);
setShadowRoot(shadowRoot);
}, [dom, setShadowRoot]);
const contentElement = ref.current?.shadowRoot?.querySelector(
'[data-md-component="container"]',
);
const primarySidebarElement = ref.current?.shadowRoot?.querySelector(
'[data-md-component="navigation"]',
);
const secondarySidebarElement = ref.current?.shadowRoot?.querySelector(
'[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>
{/* 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>
</Content>
);
};
@@ -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 { TechDocsReaderPageContent } from './TechDocsReaderPageContent';
@@ -0,0 +1,61 @@
/*
* 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 { Header } from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
// todo(backstage/techdocs-core): Export these from @backstage/plugin-techdocs
import { Skeleton } from '@material-ui/lab';
import React, { useEffect } from 'react';
import Helmet from 'react-helmet';
import { useTechDocsAddons } from '../../addons';
import { useMetadata, useTechDocsReaderPage } from '../../context';
import { TechDocsAddonLocations as locations } from '../../types';
const skeleton = <Skeleton animation="wave" variant="text" height={40} />;
export const TechDocsReaderPageHeader = () => {
const addons = useTechDocsAddons();
const configApi = useApi(configApiRef);
const metadata = useMetadata();
const { title, setTitle, subtitle, setSubtitle } = useTechDocsReaderPage();
useEffect(() => {
if (!metadata) return;
setTitle(prevTitle => prevTitle || metadata.site_name);
setSubtitle(
prevSubtitle => prevSubtitle || metadata.site_description || 'Home',
);
}, [metadata, setTitle, setSubtitle]);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const tabTitle = [subtitle, title, appTitle].filter(Boolean).join(' | ');
return (
<Header
type="Documentation"
title={title || skeleton}
subtitle={subtitle || skeleton}
>
<Helmet titleTemplate="%s">
<title>{tabTitle}</title>
</Helmet>
{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,49 @@
/*
* 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 { Box, Toolbar, ToolbarProps, withStyles } from '@material-ui/core';
import React from 'react';
import { useTechDocsAddons } from '../../addons';
import { TechDocsAddonLocations as locations } from '../../types';
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';
@@ -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 * from './TechDocsReaderPage';
+1 -2
View File
@@ -21,12 +21,11 @@
*/
export { createTechDocsAddon, TechDocsAddons } from './addons';
export * from './components';
export {
useEntityMetadata,
useMetadata,
useShadowRoot,
useShadowRootElements,
} from './context';
export { TechDocsReaderPage } from './reader';
export type { TechDocsReaderPageProps } from './reader';
export type { TechDocsAddonLocations, TechDocsAddonOptions } from './types';
-217
View File
@@ -1,217 +0,0 @@
/*
* 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 { CompoundEntityRef } from '@backstage/catalog-model';
import { Content, Header, Page, Progress } from '@backstage/core-components';
import { configApiRef, useApi } from '@backstage/core-plugin-api';
// todo(backstage/techdocs-core): Export these from @backstage/plugin-techdocs
import {
// @ts-ignore
useTechDocsReaderDom,
// @ts-ignore
withTechDocsReaderProvider,
// @ts-ignore
TechDocsStateIndicator as TechDocReaderPageIndicator,
} from '@backstage/plugin-techdocs';
import {
withStyles,
Portal,
Box,
Toolbar,
ToolbarProps,
} from '@material-ui/core';
import { Skeleton } from '@material-ui/lab';
import { StylesProvider, jssPreset } from '@material-ui/styles';
import React, { useEffect, useRef, useState } from 'react';
import { create } from 'jss';
import Helmet from 'react-helmet';
import { useTechDocsAddons } from './addons';
import {
TechDocsMetadataProvider,
useMetadata,
TechDocsEntityProvider,
TechDocsReaderPageProvider,
useTechDocsReaderPage,
} from './context';
import { TechDocsAddonLocations as locations } from './types';
const TechDocsReaderPageSubheader = withStyles(theme => ({
root: {
gridArea: 'pageSubheader',
flexDirection: 'column',
minHeight: 'auto',
padding: theme.spacing(3, 3, 0),
},
}))(({ ...rest }: ToolbarProps) => {
const addons = useTechDocsAddons();
if (!addons.renderComponentsWithLocation(locations.SUBHEADER)) return null;
return (
<Toolbar {...rest}>
{addons.renderComponentsWithLocation(locations.SUBHEADER) && (
<Box
display="flex"
justifyContent="flex-end"
width="100%"
flexWrap="wrap"
>
{addons.renderComponentsWithLocation(locations.SUBHEADER)}
</Box>
)}
</Toolbar>
);
});
const skeleton = <Skeleton animation="wave" variant="text" height={40} />;
const TechDocsReaderPageHeader = () => {
const addons = useTechDocsAddons();
const configApi = useApi(configApiRef);
const metadata = useMetadata();
const { title, setTitle, subtitle, setSubtitle } = useTechDocsReaderPage();
useEffect(() => {
if (!metadata) return;
setTitle(prevTitle => prevTitle || metadata.site_name);
setSubtitle(
prevSubtitle => prevSubtitle || metadata.site_description || 'Home',
);
}, [metadata, setTitle, setSubtitle]);
const appTitle = configApi.getOptional('app.title') || 'Backstage';
const tabTitle = [subtitle, title, appTitle].filter(Boolean).join(' | ');
return (
<Header
type="Documentation"
title={title || skeleton}
subtitle={subtitle || skeleton}
>
<Helmet titleTemplate="%s">
<title>{tabTitle}</title>
</Helmet>
{addons.renderComponentsWithLocation(locations.HEADER)}
</Header>
);
};
const TechDocsReaderPageContent = () => {
const ref = useRef<HTMLDivElement>(null);
const [jss, setJss] = useState(
create({
...jssPreset(),
insertionPoint: undefined,
}),
);
const addons = useTechDocsAddons();
const { entityName, setShadowRoot } = useTechDocsReaderPage();
const dom = useTechDocsReaderDom(entityName);
useEffect(() => {
const shadowHost = ref.current;
if (!dom || !shadowHost || shadowHost.shadowRoot) return;
setJss(
create({
...jssPreset(),
insertionPoint: dom.querySelector('head') || undefined,
}),
);
const shadowRoot = shadowHost.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = '';
shadowRoot.appendChild(dom);
setShadowRoot(shadowRoot);
}, [dom, setShadowRoot]);
const contentElement = ref.current?.shadowRoot?.querySelector(
'[data-md-component="container"]',
);
const primarySidebarElement = ref.current?.shadowRoot?.querySelector(
'[data-md-component="navigation"]',
);
const secondarySidebarElement = ref.current?.shadowRoot?.querySelector(
'[data-md-component="toc"]',
);
const primarySidebarAddonSpace = document.createElement('div');
primarySidebarElement?.prepend(primarySidebarAddonSpace);
const secondarySidebarAddonSpace = document.createElement('div');
secondarySidebarElement?.prepend(secondarySidebarAddonSpace);
// do not return content until dom is ready
if (!dom) {
return (
<Content>
<Progress />
</Content>
);
}
return (
<Content>
{/* 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={primarySidebarAddonSpace}>
{addons.renderComponentsWithLocation(locations.PRIMARY_SIDEBAR)}
</Portal>
<Portal container={contentElement}>
{addons.renderComponentsWithLocation(locations.CONTENT)}
</Portal>
<Portal container={secondarySidebarAddonSpace}>
{addons.renderComponentsWithLocation(locations.SECONDARY_SIDEBAR)}
</Portal>
</StylesProvider>
</Content>
);
};
/**
* @public
*/
export type TechDocsReaderPageProps = { entityName: CompoundEntityRef };
/**
* An addon-aware implementation of the TechDocsReaderPage.
* @public
*/
export const TechDocsReaderPage = (props: TechDocsReaderPageProps) => {
const { entityName } = props;
const Component = withTechDocsReaderProvider(() => {
return (
<TechDocsMetadataProvider entityName={entityName}>
<TechDocsEntityProvider entityName={entityName}>
<TechDocsReaderPageProvider entityName={entityName}>
<Page themeId="documentation">
<TechDocsReaderPageHeader />
<TechDocsReaderPageSubheader />
<TechDocReaderPageIndicator />
<TechDocsReaderPageContent />
</Page>
</TechDocsReaderPageProvider>
</TechDocsEntityProvider>
</TechDocsMetadataProvider>
);
}, entityName);
return <Component />;
};