Merge pull request #27853 from autodesk-forks/nikolarAutodesk/makeTechDocsCustomizable
[plugin-techdocs] Add props to increase flexibility and reusability
This commit is contained in:
@@ -52,15 +52,24 @@ export const DefaultTechDocsHome = (props: TechDocsIndexPageProps) => {
|
||||
actions,
|
||||
ownerPickerMode,
|
||||
pagination,
|
||||
options,
|
||||
PageWrapper,
|
||||
CustomHeader,
|
||||
} = props;
|
||||
const Wrapper: React.FC<{
|
||||
children: React.ReactNode;
|
||||
}> = PageWrapper ? PageWrapper : TechDocsPageWrapper;
|
||||
const Header: React.FC =
|
||||
CustomHeader ||
|
||||
(() => (
|
||||
<ContentHeader title="">
|
||||
<SupportButton>Discover documentation in your ecosystem.</SupportButton>
|
||||
</ContentHeader>
|
||||
));
|
||||
return (
|
||||
<TechDocsPageWrapper>
|
||||
<Wrapper>
|
||||
<Content>
|
||||
<ContentHeader title="">
|
||||
<SupportButton>
|
||||
Discover documentation in your ecosystem.
|
||||
</SupportButton>
|
||||
</ContentHeader>
|
||||
<Header />
|
||||
<EntityListProvider pagination={pagination}>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>
|
||||
@@ -70,11 +79,15 @@ export const DefaultTechDocsHome = (props: TechDocsIndexPageProps) => {
|
||||
<EntityTagPicker />
|
||||
</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>
|
||||
<EntityListDocsTable actions={actions} columns={columns} />
|
||||
<EntityListDocsTable
|
||||
actions={actions}
|
||||
columns={columns}
|
||||
options={options}
|
||||
/>
|
||||
</CatalogFilterLayout.Content>
|
||||
</CatalogFilterLayout>
|
||||
</EntityListProvider>
|
||||
</Content>
|
||||
</TechDocsPageWrapper>
|
||||
</Wrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { TestApiProvider, renderInTestApp } from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { entityPresentationApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { DefaultEntityPresentationApi } from '@backstage/plugin-catalog';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
|
||||
import React from 'react';
|
||||
import { rootDocsRouteRef } from '../../../routes';
|
||||
import { InfoCardGrid } from './InfoCardGrid';
|
||||
|
||||
describe('Entity Info Card Grid', () => {
|
||||
const catalogApi = catalogApiMock();
|
||||
let Wrapper: React.ComponentType<React.PropsWithChildren<{}>>;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
Wrapper = ({ children }: { children?: React.ReactNode }) => (
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
entityPresentationApiRef,
|
||||
DefaultEntityPresentationApi.create({ catalogApi }),
|
||||
],
|
||||
]}
|
||||
>
|
||||
{children}
|
||||
</TestApiProvider>
|
||||
);
|
||||
});
|
||||
|
||||
it('should render multiple entities', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<InfoCardGrid
|
||||
entities={[
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
title: 'TestTitle',
|
||||
},
|
||||
spec: {
|
||||
owner: 'techdocs@example.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind2',
|
||||
metadata: {
|
||||
name: 'testName2',
|
||||
title: 'TestTitle2',
|
||||
},
|
||||
spec: {
|
||||
owner: 'techdocs2@example.com',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(await screen.findByText('TestTitle')).toBeInTheDocument();
|
||||
expect(await screen.findByText('TestTitle2')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render links correctly', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<InfoCardGrid
|
||||
entities={[
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
title: 'TestTitle',
|
||||
},
|
||||
spec: {
|
||||
owner: 'techdocs@example.com',
|
||||
},
|
||||
},
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind2',
|
||||
metadata: {
|
||||
name: 'testName2',
|
||||
title: 'TestTitle2',
|
||||
},
|
||||
spec: {
|
||||
owner: 'techdocs2@example.com',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(await screen.findByText('TestTitle')).toBeInTheDocument();
|
||||
expect(await screen.findByText('TestTitle2')).toBeInTheDocument();
|
||||
const [button1, button2] = await screen.findAllByTestId('read-docs-link');
|
||||
expect(button1.getAttribute('href')).toContain(
|
||||
'/docs/default/testkind/testname',
|
||||
);
|
||||
expect(button2.getAttribute('href')).toContain(
|
||||
'/docs/default/testkind2/testname2',
|
||||
);
|
||||
});
|
||||
|
||||
it('should render entity title if available', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<InfoCardGrid
|
||||
entities={[
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
title: 'TestTitle',
|
||||
},
|
||||
spec: {
|
||||
owner: 'techdocs@example.com',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(await screen.findByText('TestTitle')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render entity name if title is not available', async () => {
|
||||
await renderInTestApp(
|
||||
<Wrapper>
|
||||
<InfoCardGrid
|
||||
entities={[
|
||||
{
|
||||
apiVersion: 'version',
|
||||
kind: 'TestKind',
|
||||
metadata: {
|
||||
name: 'testName',
|
||||
},
|
||||
spec: {
|
||||
owner: 'techdocs@example.com',
|
||||
},
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Wrapper>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(await screen.findByText('testName')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,125 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import useAsync from 'react-use/esm/useAsync';
|
||||
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { useApi, useRouteRef, configApiRef } from '@backstage/core-plugin-api';
|
||||
import {
|
||||
ItemCardGrid,
|
||||
InfoCard,
|
||||
Link,
|
||||
Progress,
|
||||
} from '@backstage/core-components';
|
||||
import {
|
||||
EntityRefPresentationSnapshot,
|
||||
entityPresentationApiRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import { rootDocsRouteRef } from '../../../routes';
|
||||
import { toLowerMaybe } from '../../../helpers';
|
||||
|
||||
/** @public */
|
||||
export type InfoCardGridClassKey = 'linkSpacer' | 'readMoreLink';
|
||||
|
||||
const useStyles = makeStyles(
|
||||
theme => ({
|
||||
linkSpacer: {
|
||||
paddingTop: theme.spacing(0.2),
|
||||
},
|
||||
readMoreLink: {
|
||||
paddingTop: theme.spacing(0.2),
|
||||
},
|
||||
}),
|
||||
{ name: 'BackstageInfoCardGrid' },
|
||||
);
|
||||
|
||||
/**
|
||||
* Props for {@link InfoCardGrid}
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type InfoCardGridProps = {
|
||||
entities: Entity[] | undefined;
|
||||
linkContent?: string | JSX.Element;
|
||||
linkDestination?: (entity: Entity) => string | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Component which accepts a list of entities and renders a info card for each entity
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const InfoCardGrid = (props: InfoCardGridProps) => {
|
||||
const { entities, linkContent, linkDestination } = props;
|
||||
const classes = useStyles();
|
||||
const getRouteToReaderPageFor = useRouteRef(rootDocsRouteRef);
|
||||
const config = useApi(configApiRef);
|
||||
const linkRoute = (entity: Entity) => {
|
||||
if (linkDestination) {
|
||||
const destination = linkDestination(entity);
|
||||
if (destination) {
|
||||
return destination;
|
||||
}
|
||||
}
|
||||
return getRouteToReaderPageFor({
|
||||
namespace: toLowerMaybe(entity.metadata.namespace ?? 'default', config),
|
||||
kind: toLowerMaybe(entity.kind, config),
|
||||
name: toLowerMaybe(entity.metadata.name, config),
|
||||
});
|
||||
};
|
||||
const entityPresentationApi = useApi(entityPresentationApiRef);
|
||||
const { value: entityRefToPresentation, loading } = useAsync(async () => {
|
||||
return new Map<string, EntityRefPresentationSnapshot>(
|
||||
await Promise.all(
|
||||
entities?.map(async entity => {
|
||||
const presentation = await entityPresentationApi.forEntity(entity)
|
||||
.promise;
|
||||
return [stringifyEntityRef(entity), presentation] as [
|
||||
string,
|
||||
EntityRefPresentationSnapshot,
|
||||
];
|
||||
}) || [],
|
||||
),
|
||||
);
|
||||
});
|
||||
if (loading) return <Progress />;
|
||||
if (!entities || !entities?.length) return null;
|
||||
return (
|
||||
<ItemCardGrid data-testid="info-card-container">
|
||||
{entities.map(entity => (
|
||||
<InfoCard
|
||||
key={entity.metadata.name}
|
||||
data-testid={entity?.metadata?.title}
|
||||
title={
|
||||
entityRefToPresentation?.get(stringifyEntityRef(entity))
|
||||
?.primaryTitle
|
||||
}
|
||||
>
|
||||
<div>{entity?.metadata?.description}</div>
|
||||
<div className={classes.linkSpacer} />
|
||||
<Link
|
||||
to={linkRoute(entity)}
|
||||
className={classes.readMoreLink}
|
||||
data-testid="read-docs-link"
|
||||
>
|
||||
{linkContent || 'Read Docs'}
|
||||
</Link>
|
||||
</InfoCard>
|
||||
))}
|
||||
</ItemCardGrid>
|
||||
);
|
||||
};
|
||||
@@ -16,3 +16,4 @@
|
||||
|
||||
export * from './EntityListDocsGrid';
|
||||
export * from './DocsCardGrid';
|
||||
export * from './InfoCardGrid';
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
starredEntitiesApiRef,
|
||||
MockStarredEntitiesApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { ContentHeader, PageWithHeader } from '@backstage/core-components';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
import { renderInTestApp, TestApiRegistry } from '@backstage/test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
@@ -103,4 +104,80 @@ describe('TechDocsCustomHome', () => {
|
||||
await screen.findByText('Second Tab Description'),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
it('should render ContentHeader based on CustomHeader prop', async () => {
|
||||
const tabsConfig = [
|
||||
{
|
||||
label: 'First Tab',
|
||||
panels: [
|
||||
{
|
||||
title: 'First Tab',
|
||||
description: 'First Tab Description',
|
||||
panelType: 'DocsCardGrid' as PanelType,
|
||||
panelProps: {
|
||||
CustomHeader: () => (
|
||||
<ContentHeader
|
||||
title="Custom Header"
|
||||
description="useful docs"
|
||||
/>
|
||||
),
|
||||
},
|
||||
filterPredicate: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<TechDocsCustomHome tabsConfig={tabsConfig} />
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText('Custom Header')).toBeInTheDocument();
|
||||
});
|
||||
it('should render CustomPageWrapper', async () => {
|
||||
const tabsConfig = [
|
||||
{
|
||||
label: 'First Tab',
|
||||
panels: [
|
||||
{
|
||||
title: 'First Tab',
|
||||
description: 'First Tab Description',
|
||||
panelType: 'DocsCardGrid' as PanelType,
|
||||
filterPredicate: () => true,
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
await renderInTestApp(
|
||||
<ApiProvider apis={apiRegistry}>
|
||||
<TechDocsCustomHome
|
||||
tabsConfig={tabsConfig}
|
||||
CustomPageWrapper={({ children }: React.PropsWithChildren<{}>) => (
|
||||
<PageWithHeader
|
||||
title="Custom Title"
|
||||
subtitle="Custom Subtitle"
|
||||
themeId="documentation"
|
||||
>
|
||||
{children}
|
||||
</PageWithHeader>
|
||||
)}
|
||||
/>
|
||||
</ApiProvider>,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/docs/:namespace/:kind/:name/*': rootDocsRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(screen.getByText('Custom Title')).toBeInTheDocument();
|
||||
expect(screen.getByText('Custom Subtitle')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,9 +26,10 @@ import {
|
||||
EntityListProvider,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { DocsTable } from './Tables';
|
||||
import { DocsCardGrid } from './Grids';
|
||||
import { DocsTable, DocsTableRow } from './Tables';
|
||||
import { DocsCardGrid, InfoCardGrid } from './Grids';
|
||||
import { TechDocsPageWrapper } from './TechDocsPageWrapper';
|
||||
import { TechDocsIndexPage } from './TechDocsIndexPage';
|
||||
|
||||
import {
|
||||
CodeSnippet,
|
||||
@@ -38,13 +39,17 @@ import {
|
||||
WarningPanel,
|
||||
SupportButton,
|
||||
ContentHeader,
|
||||
TableOptions,
|
||||
} from '@backstage/core-components';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { TECHDOCS_ANNOTATION } from '@backstage/plugin-techdocs-common';
|
||||
import { EntityFilterQuery } from '@backstage/catalog-client';
|
||||
|
||||
const panels = {
|
||||
DocsTable: DocsTable,
|
||||
DocsCardGrid: DocsCardGrid,
|
||||
TechDocsIndexPage: TechDocsIndexPage,
|
||||
InfoCardGrid: InfoCardGrid,
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -52,7 +57,24 @@ const panels = {
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export type PanelType = 'DocsCardGrid' | 'DocsTable';
|
||||
export type PanelType =
|
||||
| 'DocsCardGrid'
|
||||
| 'DocsTable'
|
||||
| 'TechDocsIndexPage'
|
||||
| 'InfoCardGrid';
|
||||
|
||||
/**
|
||||
* Type representing Panel props
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export interface PanelProps {
|
||||
options?: TableOptions<DocsTableRow>;
|
||||
linkContent?: string | JSX.Element;
|
||||
linkDestination?: (entity: Entity) => string | undefined;
|
||||
PageWrapper?: React.FC;
|
||||
CustomHeader?: React.FC;
|
||||
}
|
||||
|
||||
/**
|
||||
* Type representing a TechDocsCustomHome panel.
|
||||
@@ -65,6 +87,7 @@ export interface PanelConfig {
|
||||
panelType: PanelType;
|
||||
panelCSS?: CSSProperties;
|
||||
filterPredicate: ((entity: Entity) => boolean) | string;
|
||||
panelProps?: PanelProps;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -84,7 +107,12 @@ export interface TabConfig {
|
||||
*/
|
||||
export type TabsConfig = TabConfig[];
|
||||
|
||||
const CustomPanel = ({
|
||||
/**
|
||||
* Component which can be used to render entities in a custom way.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const CustomDocsPanel = ({
|
||||
config,
|
||||
entities,
|
||||
index,
|
||||
@@ -118,8 +146,9 @@ const CustomPanel = ({
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
const Header: React.FC =
|
||||
config.panelProps?.CustomHeader ||
|
||||
(() => (
|
||||
<ContentHeader title={config.title} description={config.description}>
|
||||
{index === 0 ? (
|
||||
<SupportButton>
|
||||
@@ -127,9 +156,18 @@ const CustomPanel = ({
|
||||
</SupportButton>
|
||||
) : null}
|
||||
</ContentHeader>
|
||||
));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header />
|
||||
<div className={classes.panelContainer}>
|
||||
<EntityListProvider>
|
||||
<Panel data-testid="techdocs-custom-panel" entities={shownEntities} />
|
||||
<Panel
|
||||
data-testid="techdocs-custom-panel"
|
||||
entities={shownEntities}
|
||||
{...config.panelProps}
|
||||
/>
|
||||
</EntityListProvider>
|
||||
</div>
|
||||
</>
|
||||
@@ -143,10 +181,12 @@ const CustomPanel = ({
|
||||
*/
|
||||
export type TechDocsCustomHomeProps = {
|
||||
tabsConfig: TabsConfig;
|
||||
filter?: EntityFilterQuery;
|
||||
CustomPageWrapper?: React.FC;
|
||||
};
|
||||
|
||||
export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => {
|
||||
const { tabsConfig } = props;
|
||||
const { tabsConfig, filter, CustomPageWrapper } = props;
|
||||
const [selectedTab, setSelectedTab] = useState<number>(0);
|
||||
const catalogApi: CatalogApi = useApi(catalogApiRef);
|
||||
|
||||
@@ -157,6 +197,7 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => {
|
||||
} = useAsync(async () => {
|
||||
const response = await catalogApi.getEntities({
|
||||
filter: {
|
||||
...filter,
|
||||
[`metadata.annotations.${TECHDOCS_ANNOTATION}`]: CATALOG_FILTER_EXISTS,
|
||||
},
|
||||
fields: [
|
||||
@@ -177,7 +218,7 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => {
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<TechDocsPageWrapper>
|
||||
<TechDocsPageWrapper CustomPageWrapper={CustomPageWrapper}>
|
||||
<Content>
|
||||
<Progress />
|
||||
</Content>
|
||||
@@ -187,7 +228,7 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => {
|
||||
|
||||
if (error) {
|
||||
return (
|
||||
<TechDocsPageWrapper>
|
||||
<TechDocsPageWrapper CustomPageWrapper={CustomPageWrapper}>
|
||||
<Content>
|
||||
<WarningPanel
|
||||
severity="error"
|
||||
@@ -201,7 +242,7 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<TechDocsPageWrapper>
|
||||
<TechDocsPageWrapper CustomPageWrapper={CustomPageWrapper}>
|
||||
<HeaderTabs
|
||||
selectedIndex={selectedTab}
|
||||
onChange={index => setSelectedTab(index)}
|
||||
@@ -212,7 +253,7 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => {
|
||||
/>
|
||||
<Content data-testid="techdocs-content">
|
||||
{currentTabConfig.panels.map((config, index) => (
|
||||
<CustomPanel
|
||||
<CustomDocsPanel
|
||||
key={index}
|
||||
config={config}
|
||||
entities={!!entities ? entities : []}
|
||||
|
||||
@@ -16,7 +16,11 @@
|
||||
|
||||
import React from 'react';
|
||||
import { useOutlet } from 'react-router-dom';
|
||||
import { TableColumn, TableProps } from '@backstage/core-components';
|
||||
import {
|
||||
TableColumn,
|
||||
TableProps,
|
||||
TableOptions,
|
||||
} from '@backstage/core-components';
|
||||
import {
|
||||
EntityListPagination,
|
||||
EntityOwnerPickerProps,
|
||||
@@ -36,6 +40,9 @@ export type TechDocsIndexPageProps = {
|
||||
actions?: TableProps<DocsTableRow>['actions'];
|
||||
ownerPickerMode?: EntityOwnerPickerProps['mode'];
|
||||
pagination?: EntityListPagination;
|
||||
options?: TableOptions<DocsTableRow>;
|
||||
PageWrapper?: React.FC;
|
||||
CustomHeader?: React.FC;
|
||||
};
|
||||
|
||||
export const TechDocsIndexPage = (props: TechDocsIndexPageProps) => {
|
||||
|
||||
@@ -26,6 +26,7 @@ import { useApi, configApiRef } from '@backstage/core-plugin-api';
|
||||
*/
|
||||
export type TechDocsPageWrapperProps = {
|
||||
children?: React.ReactNode;
|
||||
CustomPageWrapper?: React.FC<{ children?: React.ReactNode }>;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -34,19 +35,25 @@ export type TechDocsPageWrapperProps = {
|
||||
* @public
|
||||
*/
|
||||
export const TechDocsPageWrapper = (props: TechDocsPageWrapperProps) => {
|
||||
const { children } = props;
|
||||
const { children, CustomPageWrapper } = props;
|
||||
const configApi = useApi(configApiRef);
|
||||
const generatedSubtitle = `Documentation available in ${
|
||||
configApi.getOptionalString('organization.name') ?? 'Backstage'
|
||||
}`;
|
||||
|
||||
return (
|
||||
<PageWithHeader
|
||||
title="Documentation"
|
||||
subtitle={generatedSubtitle}
|
||||
themeId="documentation"
|
||||
>
|
||||
{children}
|
||||
</PageWithHeader>
|
||||
<>
|
||||
{CustomPageWrapper ? (
|
||||
<CustomPageWrapper>{children}</CustomPageWrapper>
|
||||
) : (
|
||||
<PageWithHeader
|
||||
title="Documentation"
|
||||
subtitle={generatedSubtitle}
|
||||
themeId="documentation"
|
||||
>
|
||||
{children}
|
||||
</PageWithHeader>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -20,10 +20,12 @@ export * from './DefaultTechDocsHome';
|
||||
export type {
|
||||
PanelType,
|
||||
PanelConfig,
|
||||
PanelProps,
|
||||
TabConfig,
|
||||
TabsConfig,
|
||||
TechDocsCustomHomeProps,
|
||||
} from './TechDocsCustomHome';
|
||||
export { CustomDocsPanel } from './TechDocsCustomHome';
|
||||
export type { TechDocsIndexPageProps } from './TechDocsIndexPage';
|
||||
export * from './TechDocsPageWrapper';
|
||||
export * from './TechDocsPicker';
|
||||
|
||||
@@ -67,3 +67,5 @@ export type {
|
||||
DeprecatedTechDocsEntityMetadata as TechDocsEntityMetadata,
|
||||
DeprecatedTechDocsMetadata as TechDocsMetadata,
|
||||
};
|
||||
|
||||
export * from './overridableComponents';
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/*
|
||||
* Copyright 2021 The Backstage Authors
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Overrides } from '@material-ui/core/styles/overrides';
|
||||
import { StyleRules } from '@material-ui/core/styles/withStyles';
|
||||
import { InfoCardGridClassKey } from './home/components/Grids/InfoCardGrid';
|
||||
|
||||
/** @public */
|
||||
export type CatalogReactComponentsNameToClassKey = {
|
||||
BackstageInfoCardGrid: InfoCardGridClassKey;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export type BackstageOverrides = Overrides & {
|
||||
[Name in keyof CatalogReactComponentsNameToClassKey]?: Partial<
|
||||
StyleRules<CatalogReactComponentsNameToClassKey[Name]>
|
||||
>;
|
||||
};
|
||||
|
||||
declare module '@backstage/theme' {
|
||||
interface OverrideComponentNameToClassKeys
|
||||
extends CatalogReactComponentsNameToClassKey {}
|
||||
}
|
||||
Reference in New Issue
Block a user