Merge pull request #7378 from backstage/emmaindal/custom-techdocs-header

[TechDocs] Support custom TechDocs Reader Page
This commit is contained in:
Patrik Oldsberg
2021-09-30 13:37:09 +02:00
committed by GitHub
15 changed files with 384 additions and 44 deletions
+48 -2
View File
@@ -182,7 +182,7 @@ export type PanelType = 'DocsCardGrid' | 'DocsTable';
//
// @public (undocumented)
export const Reader: ({
entityId,
entityRef,
onReady,
withSearch,
}: Props_3) => JSX.Element;
@@ -264,11 +264,55 @@ export const TechDocsCustomHome: ({
// @public (undocumented)
export const TechDocsIndexPage: () => JSX.Element;
// Warning: (ae-missing-release-tag) "TechDocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsPage: ({ children }: TechDocsPageProps) => JSX.Element;
// Warning: (ae-missing-release-tag) "TechdocsPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechdocsPage: () => JSX.Element;
// Warning: (ae-missing-release-tag) "TechDocsPageHeader" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsPageHeader: ({
entityRef,
entityMetadata,
techDocsMetadata,
}: TechDocsPageHeaderProps) => JSX.Element;
// Warning: (ae-missing-release-tag) "TechDocsPageHeaderProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type TechDocsPageHeaderProps = {
entityRef: EntityName;
entityMetadata?: TechDocsEntityMetadata;
techDocsMetadata?: TechDocsMetadata;
};
// Warning: (ae-missing-release-tag) "TechDocsPageProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type TechDocsPageProps = {
children?: TechDocsPageRenderFunction | React_2.ReactNode;
};
// Warning: (ae-missing-release-tag) "TechDocsPageRenderFunction" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export type TechDocsPageRenderFunction = ({
techdocsMetadataValue,
entityMetadataValue,
entityRef,
}: {
techdocsMetadataValue?: TechDocsMetadata | undefined;
entityMetadataValue?: TechDocsEntityMetadata | undefined;
entityRef: EntityName;
onReady: () => void;
}) => JSX.Element;
// Warning: (ae-forgotten-export) The symbol "Props" needs to be exported by the entry point index.d.ts
// Warning: (ae-missing-release-tag) "TechDocsPageWrapper" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
@@ -301,7 +345,9 @@ export { techdocsPlugin };
// Warning: (ae-missing-release-tag) "TechDocsReaderPage" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
// @public (undocumented)
export const TechDocsReaderPage: () => JSX.Element;
export const TechDocsReaderPage: ({
children,
}: TechDocsPageProps) => JSX.Element;
// Warning: (ae-missing-release-tag) "TechDocsStorageApi" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal)
//
+1 -1
View File
@@ -110,7 +110,7 @@ function createPage({
render() {
return (
<Reader
entityId={{
entityRef={{
kind: 'Component',
namespace: 'default',
name: 'my-docs',
+1 -1
View File
@@ -22,7 +22,7 @@ export const EntityPageDocs = ({ entity }: { entity: Entity }) => {
return (
<Reader
withSearch={false}
entityId={{
entityRef={{
kind: entity.kind,
namespace: entity.metadata.namespace ?? 'default',
name: entity.metadata.name,
@@ -0,0 +1,77 @@
/*
* 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 { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { techdocsApiRef } from '../../api';
import { TechDocsNotFound } from './TechDocsNotFound';
import { useApi } from '@backstage/core-plugin-api';
import { Page, Content } from '@backstage/core-components';
import { Reader } from './Reader';
import { TechDocsPageHeader } from './TechDocsPageHeader';
export const LegacyTechDocsPage = () => {
const [documentReady, setDocumentReady] = useState<boolean>(false);
const { namespace, kind, name } = useParams();
const techdocsApi = useApi(techdocsApiRef);
const { value: techdocsMetadataValue } = useAsync(() => {
if (documentReady) {
return techdocsApi.getTechDocsMetadata({ kind, namespace, name });
}
return Promise.resolve(undefined);
}, [kind, namespace, name, techdocsApi, documentReady]);
const { value: entityMetadataValue, error: entityMetadataError } =
useAsync(() => {
return techdocsApi.getEntityMetadata({ kind, namespace, name });
}, [kind, namespace, name, techdocsApi]);
const onReady = useCallback(() => {
setDocumentReady(true);
}, [setDocumentReady]);
if (entityMetadataError) {
return <TechDocsNotFound errorMessage={entityMetadataError.message} />;
}
return (
<Page themeId="documentation">
<TechDocsPageHeader
techDocsMetadata={techdocsMetadataValue}
entityMetadata={entityMetadataValue}
entityRef={{
kind,
namespace,
name,
}}
/>
<Content data-testid="techdocs-content">
<Reader
onReady={onReady}
entityRef={{
kind,
namespace,
name,
}}
/>
</Content>
</Page>
);
};
@@ -41,7 +41,7 @@ const { useParams }: { useParams: jest.Mock } =
describe('<Reader />', () => {
it('should render Reader content', async () => {
useParams.mockReturnValue({
entityId: 'Component::backstage',
entityRef: 'Component::backstage',
});
const scmIntegrationsApi: ScmIntegrationsApi =
@@ -68,7 +68,7 @@ describe('<Reader />', () => {
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<Reader
entityId={{
entityRef={{
kind: 'Component',
namespace: 'default',
name: 'example',
@@ -48,7 +48,7 @@ import { TechDocsSearch } from './TechDocsSearch';
import { useReaderState } from './useReaderState';
type Props = {
entityId: EntityName;
entityRef: EntityName;
onReady?: () => void;
withSearch?: boolean;
};
@@ -71,8 +71,8 @@ const useStyles = makeStyles<BackstageTheme>(theme => ({
},
}));
export const Reader = ({ entityId, onReady, withSearch = true }: Props) => {
const { kind, namespace, name } = entityId;
export const Reader = ({ entityRef, onReady, withSearch = true }: Props) => {
const { kind, namespace, name } = entityRef;
const theme = useTheme<BackstageTheme>();
const classes = useStyles();
@@ -448,7 +448,7 @@ export const Reader = ({ entityId, onReady, withSearch = true }: Props) => {
{withSearch && shadowDomRef?.current?.shadowRoot?.innerHTML && (
<Grid container className={classes.searchBar}>
<TechDocsSearch entityId={entityId} />
<TechDocsSearch entityId={entityRef} />
</Grid>
)}
<div data-testid="techdocs-content-shadowroot" ref={shadowDomRef} />
@@ -22,6 +22,7 @@ import {
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { wrapInTestApp } from '@backstage/test-utils';
import { Header } from '@backstage/core-components';
import {
techdocsApiRef,
TechDocsApi,
@@ -53,7 +54,7 @@ global.scroll = jest.fn();
describe('<TechDocsPage />', () => {
it('should render techdocs page', async () => {
useParams.mockReturnValue({
entityId: 'Component::backstage',
entityRef: 'Component::backstage',
});
const scmIntegrationsApi: ScmIntegrationsApi =
@@ -107,4 +108,69 @@ describe('<TechDocsPage />', () => {
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 = ApiRegistry.from([
[scmIntegrationsApiRef, scmIntegrationsApi],
[techdocsApiRef, techdocsApi],
[techdocsStorageApiRef, techdocsStorageApi],
[searchApiRef, searchApi],
]);
await act(async () => {
const rendered = render(
wrapInTestApp(
<ApiProvider apis={apiRegistry}>
<TechDocsPage>
{({ techdocsMetadataValue }) => (
<Header
type="documentation"
title="A custom header"
subtitle={techdocsMetadataValue?.site_name}
/>
)}
</TechDocsPage>
</ApiProvider>,
),
);
expect(rendered.getByText('A custom header')).toBeInTheDocument();
});
});
});
@@ -15,17 +15,35 @@
*/
import React, { useCallback, useState } from 'react';
import { useOutlet } from 'react-router';
import { useParams } from 'react-router-dom';
import { useAsync } from 'react-use';
import { techdocsApiRef } from '../../api';
import { Reader } from './Reader';
import { TechDocsNotFound } from './TechDocsNotFound';
import { TechDocsPageHeader } from './TechDocsPageHeader';
import { Content, Page } from '@backstage/core-components';
import { LegacyTechDocsPage } from './LegacyTechDocsPage';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types';
import { EntityName } from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
import { Page } from '@backstage/core-components';
export type TechDocsPageRenderFunction = ({
techdocsMetadataValue,
entityMetadataValue,
entityRef,
}: {
techdocsMetadataValue?: TechDocsMetadata | undefined;
entityMetadataValue?: TechDocsEntityMetadata | undefined;
entityRef: EntityName;
onReady: () => void;
}) => JSX.Element;
export type TechDocsPageProps = {
children?: TechDocsPageRenderFunction | React.ReactNode;
};
export const TechDocsPage = ({ children }: TechDocsPageProps) => {
const outlet = useOutlet();
export const TechDocsPage = () => {
const [documentReady, setDocumentReady] = useState<boolean>(false);
const { namespace, kind, name } = useParams();
@@ -52,27 +70,18 @@ export const TechDocsPage = () => {
return <TechDocsNotFound errorMessage={entityMetadataError.message} />;
}
if (!children) return outlet || <LegacyTechDocsPage />;
return (
<Page themeId="documentation">
<TechDocsPageHeader
techDocsMetadata={techdocsMetadataValue}
entityMetadata={entityMetadataValue}
entityId={{
kind,
namespace,
name,
}}
/>
<Content data-testid="techdocs-content">
<Reader
onReady={onReady}
entityId={{
kind,
namespace,
name,
}}
/>
</Content>
{children instanceof Function
? children({
techdocsMetadataValue,
entityMetadataValue,
entityRef: { kind, namespace, name },
onReady,
})
: children}
</Page>
);
};
@@ -25,7 +25,7 @@ describe('<TechDocsPageHeader />', () => {
await act(async () => {
const rendered = await renderInTestApp(
<TechDocsPageHeader
entityId={{
entityRef={{
kind: 'test',
name: 'test-name',
namespace: 'test-namespace',
@@ -67,7 +67,7 @@ describe('<TechDocsPageHeader />', () => {
await act(async () => {
const rendered = await renderInTestApp(
<TechDocsPageHeader
entityId={{
entityRef={{
kind: 'test',
name: 'test-name',
namespace: 'test-namespace',
@@ -89,7 +89,7 @@ describe('<TechDocsPageHeader />', () => {
await act(async () => {
const rendered = await renderInTestApp(
<TechDocsPageHeader
entityId={{
entityRef={{
kind: 'test',
name: 'test-name',
namespace: 'test-namespace',
@@ -27,18 +27,18 @@ import React from 'react';
import { rootRouteRef } from '../../routes';
import { TechDocsEntityMetadata, TechDocsMetadata } from '../../types';
type TechDocsPageHeaderProps = {
entityId: EntityName;
export type TechDocsPageHeaderProps = {
entityRef: EntityName;
entityMetadata?: TechDocsEntityMetadata;
techDocsMetadata?: TechDocsMetadata;
};
export const TechDocsPageHeader = ({
entityId,
entityRef,
entityMetadata,
techDocsMetadata,
}: TechDocsPageHeaderProps) => {
const { name } = entityId;
const { name } = entityRef;
const { site_name: siteName, site_description: siteDescription } =
techDocsMetadata || {};
@@ -59,7 +59,7 @@ export const TechDocsPageHeader = ({
value={
<EntityRefLink
color="inherit"
entityRef={entityId}
entityRef={entityRef}
defaultKind="Component"
/>
}
@@ -15,3 +15,5 @@
*/
export * from './Reader';
export * from './TechDocsPage';
export * from './TechDocsPageHeader';