diff --git a/.changeset/warm-masks-ring.md b/.changeset/warm-masks-ring.md new file mode 100644 index 0000000000..bfb521a005 --- /dev/null +++ b/.changeset/warm-masks-ring.md @@ -0,0 +1,114 @@ +--- +'@backstage/plugin-techdocs': patch +--- + +Add optional props to `TechDocCustomHome` to allow for more flexibility: + +```tsx +import { TechDocsCustomHome } from '@backstage/plugin-techdocs'; +//... + +const options = { emptyRowsWhenPaging: false }; +const linkDestination = (entity: Entity): string | undefined => { + return entity.metadata.annotations?.['external-docs']; +}; +const techDocsTabsConfig = [ + { + label: 'Recommended Documentation', + panels: [ + { + title: 'Golden Path', + description: 'Documentation about standards to follow', + panelType: 'DocsCardGrid', + panelProps: { CustomHeader: () => }, + filterPredicate: entity => + entity?.metadata?.tags?.includes('golden-path') ?? false, + }, + { + title: 'Recommended', + description: 'Useful documentation', + panelType: 'InfoCardGrid', + panelProps: { + CustomHeader: () => + linkDestination: linkDestination, + }, + filterPredicate: entity => + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ], + }, + { + label: 'Browse All', + panels: [ + { + description: 'Browse all docs', + filterPredicate: filterEntity, + panelType: 'TechDocsIndexPage', + title: 'All', + panelProps: { PageWrapper: React.Fragment, CustomHeader: React.Fragment, options: options }, + }, + ], + }, +]; + +const AppRoutes = () => { + + ) => ({children})} + /> + } + /> + ; +}; +``` + +Add new Grid option called `InfoCardGrid` which is a more customizable card option for the Docs grid. + +```tsx + entity.metadata['external-docs']} +/> +``` + +Expose existing `CustomDocsPanel` so that it can be used independently if desired. + +```tsx +const panels: PanelConfig[] = [ + { + description: '', + filterPredicate: entity => {}, + panelType: 'InfoCardGrid', + title: 'Standards', + panelProps: { + CustomHeader: () => + linkDestination: linkDestination, + }, + }, + { + description: '', + filterPredicate: entity => {}, + panelType: 'DocsCardGrid', + title: 'Contribute', + }, +]; +{ + panels.map((config, index) => ( + + )); +} +``` diff --git a/docs/features/techdocs/how-to-guides.md b/docs/features/techdocs/how-to-guides.md index 31d573fdbb..5a9044842f 100644 --- a/docs/features/techdocs/how-to-guides.md +++ b/docs/features/techdocs/how-to-guides.md @@ -137,6 +137,10 @@ Modify your `App.tsx` as follows: import { TechDocsCustomHome } from '@backstage/plugin-techdocs'; //... +const options = { emptyRowsWhenPaging: false }; +const linkDestination = (entity: Entity): string | undefined => { + return entity.metadata.annotations?.['external-docs']; +}; const techDocsTabsConfig = [ { label: 'Recommended Documentation', @@ -145,18 +149,53 @@ const techDocsTabsConfig = [ title: 'Golden Path', description: 'Documentation about standards to follow', panelType: 'DocsCardGrid', + panelProps: { CustomHeader: () => }, + filterPredicate: entity => + entity?.metadata?.tags?.includes('golden-path') ?? false, + }, + { + title: 'Recommended', + description: 'Useful documentation', + panelType: 'InfoCardGrid', + panelProps: { + CustomHeader: () => + linkDestination: linkDestination, + }, filterPredicate: entity => entity?.metadata?.tags?.includes('recommended') ?? false, }, ], }, + { + label: 'Browse All', + panels: [ + { + description: 'Browse all docs', + filterPredicate: filterEntity, + panelType: 'TechDocsIndexPage', + title: 'All', + panelProps: { PageWrapper: React.Fragment, CustomHeader: React.Fragment, options: options }, + }, + ], + }, ]; - +const docsFilter = { + kind: ['Location', 'Resource', 'Component'], + 'metadata.annotations.featured-docs': CATALOG_FILTER_EXISTS, +} +const customPageWrapper = ({ children }: React.PropsWithChildren<{}>) => + ({children}) const AppRoutes = () => { } + element={ + + } /> ; }; diff --git a/plugins/techdocs/package.json b/plugins/techdocs/package.json index e10eb5480f..d0279dd61d 100644 --- a/plugins/techdocs/package.json +++ b/plugins/techdocs/package.json @@ -59,6 +59,7 @@ "test": "backstage-cli package test" }, "dependencies": { + "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", "@backstage/config": "workspace:^", "@backstage/core-compat-api": "workspace:^", @@ -91,6 +92,7 @@ "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", "@backstage/dev-utils": "workspace:^", + "@backstage/plugin-catalog": "workspace:^", "@backstage/plugin-techdocs-module-addons-contrib": "workspace:^", "@backstage/test-utils": "workspace:^", "@testing-library/dom": "^10.0.0", diff --git a/plugins/techdocs/report.api.md b/plugins/techdocs/report.api.md index 196fef5f35..12496bb5bd 100644 --- a/plugins/techdocs/report.api.md +++ b/plugins/techdocs/report.api.md @@ -12,17 +12,20 @@ import { Config } from '@backstage/config'; import { CSSProperties } from '@material-ui/styles/withStyles'; import { DiscoveryApi } from '@backstage/core-plugin-api'; import { Entity } from '@backstage/catalog-model'; +import { EntityFilterQuery } from '@backstage/catalog-client'; import { EntityListPagination } from '@backstage/plugin-catalog-react'; import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react'; import { FetchApi } from '@backstage/core-plugin-api'; import { IdentityApi } from '@backstage/core-plugin-api'; import { JSX as JSX_2 } from 'react'; +import { Overrides } from '@material-ui/core/styles/overrides'; import { PropsWithChildren } from 'react'; import { default as React_2 } from 'react'; import { ReactNode } from 'react'; import { ResultHighlight } from '@backstage/plugin-search-common'; import { RouteRef } from '@backstage/core-plugin-api'; import { SearchResultListItemExtensionProps } from '@backstage/plugin-search-react'; +import { StyleRules } from '@material-ui/core/styles/withStyles'; import { SyncResult as SyncResult_2 } from '@backstage/plugin-techdocs-react'; import { TableColumn } from '@backstage/core-components'; import { TableOptions } from '@backstage/core-components'; @@ -35,6 +38,18 @@ import { ThemeOptions } from '@material-ui/core/styles'; import { ToolbarProps } from '@material-ui/core/Toolbar'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; +// @public (undocumented) +export type BackstageOverrides = Overrides & { + [Name in keyof CatalogReactComponentsNameToClassKey]?: Partial< + StyleRules + >; +}; + +// @public (undocumented) +export type CatalogReactComponentsNameToClassKey = { + BackstageInfoCardGrid: InfoCardGridClassKey; +}; + // @public export type ContentStateTypes = /** There is nothing to display but a loading indicator */ @@ -52,6 +67,17 @@ export type ContentStateTypes = /** There is only the latest and greatest content */ | 'CONTENT_FRESH'; +// @public +export const CustomDocsPanel: ({ + config, + entities, + index, +}: { + config: PanelConfig; + entities: Entity[]; + index: number; +}) => React_2.JSX.Element; + // @public export const DefaultTechDocsHome: ( props: TechDocsIndexPageProps, @@ -201,6 +227,21 @@ export const EntityTechdocsContent: ({ withSearch?: boolean | undefined; }>) => JSX_2.Element; +// @public +export const InfoCardGrid: ( + props: InfoCardGridProps, +) => React_2.JSX.Element | null; + +// @public (undocumented) +export type InfoCardGridClassKey = 'linkSpacer' | 'readMoreLink'; + +// @public +export type InfoCardGridProps = { + entities: Entity[] | undefined; + linkContent?: string | JSX.Element; + linkDestination?: (entity: Entity) => string | undefined; +}; + // @public export const isTechDocsAvailable: (entity: Entity) => boolean; @@ -213,13 +254,33 @@ export interface PanelConfig { // (undocumented) panelCSS?: CSSProperties; // (undocumented) + panelProps?: PanelProps; + // (undocumented) panelType: PanelType; // (undocumented) title: string; } // @public -export type PanelType = 'DocsCardGrid' | 'DocsTable'; +export interface PanelProps { + // (undocumented) + CustomHeader?: React_2.FC; + // (undocumented) + linkContent?: string | JSX.Element; + // (undocumented) + linkDestination?: (entity: Entity) => string | undefined; + // (undocumented) + options?: TableOptions; + // (undocumented) + PageWrapper?: React_2.FC; +} + +// @public +export type PanelType = + | 'DocsCardGrid' + | 'DocsTable' + | 'TechDocsIndexPage' + | 'InfoCardGrid'; // @public @deprecated export const Reader: ( @@ -300,6 +361,8 @@ export const TechDocsCustomHome: ( // @public export type TechDocsCustomHomeProps = { tabsConfig: TabsConfig; + filter?: EntityFilterQuery; + CustomPageWrapper?: React_2.FC; }; // @public @deprecated (undocumented) @@ -317,6 +380,9 @@ export type TechDocsIndexPageProps = { actions?: TableProps['actions']; ownerPickerMode?: EntityOwnerPickerProps['mode']; pagination?: EntityListPagination; + options?: TableOptions; + PageWrapper?: React_2.FC; + CustomHeader?: React_2.FC; }; // @public @deprecated (undocumented) @@ -333,6 +399,9 @@ export const TechDocsPageWrapper: ( // @public export type TechDocsPageWrapperProps = { children?: React_2.ReactNode; + CustomPageWrapper?: React_2.FC<{ + children?: React_2.ReactNode; + }>; }; // @public diff --git a/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx index f9757f29cc..9b6ee490a4 100644 --- a/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx +++ b/plugins/techdocs/src/home/components/DefaultTechDocsHome.tsx @@ -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 || + (() => ( + + Discover documentation in your ecosystem. + + )); return ( - + - - - Discover documentation in your ecosystem. - - +
@@ -70,11 +79,15 @@ export const DefaultTechDocsHome = (props: TechDocsIndexPageProps) => { - + - + ); }; diff --git a/plugins/techdocs/src/home/components/Grids/InfoCardGrid.test.tsx b/plugins/techdocs/src/home/components/Grids/InfoCardGrid.test.tsx new file mode 100644 index 0000000000..e1e3ce1511 --- /dev/null +++ b/plugins/techdocs/src/home/components/Grids/InfoCardGrid.test.tsx @@ -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>; + + beforeEach(() => { + jest.resetAllMocks(); + Wrapper = ({ children }: { children?: React.ReactNode }) => ( + + {children} + + ); + }); + + it('should render multiple entities', async () => { + await renderInTestApp( + + + , + { + 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( + + + , + { + 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( + + + , + { + 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( + + + , + { + mountedRoutes: { + '/docs/:namespace/:kind/:name/*': rootDocsRouteRef, + }, + }, + ); + + expect(await screen.findByText('testName')).toBeInTheDocument(); + }); +}); diff --git a/plugins/techdocs/src/home/components/Grids/InfoCardGrid.tsx b/plugins/techdocs/src/home/components/Grids/InfoCardGrid.tsx new file mode 100644 index 0000000000..5a71710174 --- /dev/null +++ b/plugins/techdocs/src/home/components/Grids/InfoCardGrid.tsx @@ -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( + await Promise.all( + entities?.map(async entity => { + const presentation = await entityPresentationApi.forEntity(entity) + .promise; + return [stringifyEntityRef(entity), presentation] as [ + string, + EntityRefPresentationSnapshot, + ]; + }) || [], + ), + ); + }); + if (loading) return ; + if (!entities || !entities?.length) return null; + return ( + + {entities.map(entity => ( + +
{entity?.metadata?.description}
+
+ + {linkContent || 'Read Docs'} + + + ))} + + ); +}; diff --git a/plugins/techdocs/src/home/components/Grids/index.ts b/plugins/techdocs/src/home/components/Grids/index.ts index c28b5a0723..136e31d569 100644 --- a/plugins/techdocs/src/home/components/Grids/index.ts +++ b/plugins/techdocs/src/home/components/Grids/index.ts @@ -16,3 +16,4 @@ export * from './EntityListDocsGrid'; export * from './DocsCardGrid'; +export * from './InfoCardGrid'; diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx index 585248c6c9..9295d933a7 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.test.tsx @@ -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: () => ( + + ), + }, + filterPredicate: () => true, + }, + ], + }, + ]; + + await renderInTestApp( + + + , + { + 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( + + ) => ( + + {children} + + )} + /> + , + { + mountedRoutes: { + '/docs/:namespace/:kind/:name/*': rootDocsRouteRef, + }, + }, + ); + + expect(screen.getByText('Custom Title')).toBeInTheDocument(); + expect(screen.getByText('Custom Subtitle')).toBeInTheDocument(); + }); }); diff --git a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx index d89bc5ee1b..e1c5a93161 100644 --- a/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx +++ b/plugins/techdocs/src/home/components/TechDocsCustomHome.tsx @@ -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; + 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 || + (() => ( {index === 0 ? ( @@ -127,9 +156,18 @@ const CustomPanel = ({ ) : null} + )); + + return ( + <> +
- +
@@ -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(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 ( - + @@ -187,7 +228,7 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => { if (error) { return ( - + { } return ( - + setSelectedTab(index)} @@ -212,7 +253,7 @@ export const TechDocsCustomHome = (props: TechDocsCustomHomeProps) => { /> {currentTabConfig.panels.map((config, index) => ( - ['actions']; ownerPickerMode?: EntityOwnerPickerProps['mode']; pagination?: EntityListPagination; + options?: TableOptions; + PageWrapper?: React.FC; + CustomHeader?: React.FC; }; export const TechDocsIndexPage = (props: TechDocsIndexPageProps) => { diff --git a/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx index a6192c13ee..c07876f7f9 100644 --- a/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx +++ b/plugins/techdocs/src/home/components/TechDocsPageWrapper.tsx @@ -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 ( - - {children} - + <> + {CustomPageWrapper ? ( + {children} + ) : ( + + {children} + + )} + ); }; diff --git a/plugins/techdocs/src/home/components/index.ts b/plugins/techdocs/src/home/components/index.ts index 75053cd3c1..9c625817aa 100644 --- a/plugins/techdocs/src/home/components/index.ts +++ b/plugins/techdocs/src/home/components/index.ts @@ -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'; diff --git a/plugins/techdocs/src/index.ts b/plugins/techdocs/src/index.ts index 522bcd0c99..713efc17c6 100644 --- a/plugins/techdocs/src/index.ts +++ b/plugins/techdocs/src/index.ts @@ -67,3 +67,5 @@ export type { DeprecatedTechDocsEntityMetadata as TechDocsEntityMetadata, DeprecatedTechDocsMetadata as TechDocsMetadata, }; + +export * from './overridableComponents'; diff --git a/plugins/techdocs/src/overridableComponents.ts b/plugins/techdocs/src/overridableComponents.ts new file mode 100644 index 0000000000..8b371d8798 --- /dev/null +++ b/plugins/techdocs/src/overridableComponents.ts @@ -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 + >; +}; + +declare module '@backstage/theme' { + interface OverrideComponentNameToClassKeys + extends CatalogReactComponentsNameToClassKey {} +} diff --git a/yarn.lock b/yarn.lock index 6b294b2c83..cf388516b3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8145,6 +8145,7 @@ __metadata: version: 0.0.0-use.local resolution: "@backstage/plugin-techdocs@workspace:plugins/techdocs" dependencies: + "@backstage/catalog-client": "workspace:^" "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/config": "workspace:^" @@ -8158,6 +8159,7 @@ __metadata: "@backstage/integration": "workspace:^" "@backstage/integration-react": "workspace:^" "@backstage/plugin-auth-react": "workspace:^" + "@backstage/plugin-catalog": "workspace:^" "@backstage/plugin-catalog-react": "workspace:^" "@backstage/plugin-search-common": "workspace:^" "@backstage/plugin-search-react": "workspace:^"