diff --git a/.changeset/witty-cats-tell.md b/.changeset/witty-cats-tell.md index c60be53d03..55879b5fb5 100644 --- a/.changeset/witty-cats-tell.md +++ b/.changeset/witty-cats-tell.md @@ -1,5 +1,4 @@ --- -'example-app': patch '@backstage/plugin-scaffolder': patch --- diff --git a/docs/assets/software-templates/grouped-templates.png b/docs/assets/software-templates/grouped-templates.png new file mode 100644 index 0000000000..9a3689ad1e Binary files /dev/null and b/docs/assets/software-templates/grouped-templates.png differ diff --git a/docs/features/software-templates/configuration.md b/docs/features/software-templates/configuration.md index 737f77f67b..498bb41411 100644 --- a/docs/features/software-templates/configuration.md +++ b/docs/features/software-templates/configuration.md @@ -53,3 +53,31 @@ You can do so by including the following lines in the last step of your RUN apt-get update && apt-get install -y python3 python3-pip RUN pip3 install cookiecutter ``` + +### Customizing the ScaffolderPage with Grouping and Filtering + +Once you have more than a few software templates you may want to customize your +`ScaffolderPage` by grouping and surfacing certain templates together. You can +accomplish this by creating `swimLanes` and passing them to your +`ScaffolderPage` like below + +``` + + entity?.metadata?.tags?.includes('recommended') ?? false, + }, + ]} +/> +``` + +This code will group all templates with the 'recommended' tag together at the +top of the page above any other templates not filtered by this swimlane or +others. + +You can also further customize swimlanes by passing in a `titleComponent` +instead of a `title` which will be a component to use as the header instead of +just the default `ContentHeader` with the `title` set as it's value. +![Grouped Templates](../../assets/software-templates/grouped-templates.png) diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index b1a2235288..4e093107e2 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -32,7 +32,6 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, - ContentHeader, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -180,11 +179,11 @@ const routes = ( path="/create" element={ , + title: 'Recommended', filter: entity => - entity?.metadata?.tags?.includes('recommended') || false, + entity?.metadata?.tags?.includes('recommended') ?? false, }, ]} /> diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 164f9acf83..1ebeff2ff8 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -214,16 +214,17 @@ export const ScaffolderFieldExtensions: React_2.ComponentType; // @public (undocumented) export const ScaffolderPage: ({ TemplateCardComponent, - ExtraSwimlanes, + extraSwimlanes, }: { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2; }> | undefined; - ExtraSwimlanes?: + extraSwimlanes?: | { - title: ReactNode; + title?: string | undefined; + titleComponent?: ReactNode; filter: (entity: Entity) => boolean; }[] | undefined; @@ -248,7 +249,7 @@ export { scaffolderPlugin }; // @public (undocumented) export const TemplateList: ({ TemplateCardComponent, - Swimlane, + swimlane, }: TemplateListProps) => JSX.Element | null; // Warning: (ae-missing-release-tag) "TemplateListProps" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -260,8 +261,9 @@ export type TemplateListProps = { template: TemplateEntityV1beta2; }> | undefined; - Swimlane?: { - title: React_2.ReactNode; + swimlane?: { + title?: string; + titleComponent?: React_2.ReactNode; filter: (entity: Entity) => boolean; }; }; diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 5d4e750777..7d12b02c38 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -34,15 +34,16 @@ type RouterProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; - ExtraSwimlanes?: Array<{ - title: React.ReactNode; + extraSwimlanes?: Array<{ + title?: string; + titleComponent?: React.ReactNode; filter: (entity: Entity) => boolean; }>; }; export const Router = ({ TemplateCardComponent, - ExtraSwimlanes, + extraSwimlanes, }: RouterProps) => { const outlet = useOutlet(); @@ -73,7 +74,7 @@ export const Router = ({ element={ } /> diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx index af03e01f0c..cfb12e4e53 100644 --- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx +++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx @@ -51,18 +51,28 @@ export type ScaffolderPageProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; - ExtraSwimlanes?: Array<{ - title: React.ReactNode; + extraSwimlanes?: Array<{ + title?: string; + titleComponent?: React.ReactNode; filter: (entity: Entity) => boolean; }>; }; export const ScaffolderPageContents = ({ TemplateCardComponent, - ExtraSwimlanes, + extraSwimlanes, }: ScaffolderPageProps) => { const styles = useStyles(); const registerComponentLink = useRouteRef(registerComponentRouteRef); + const otherTemplatesSwimlane = { + title: extraSwimlanes ? 'Other Templates' : 'Templates', + filter: (entity: Entity) => { + const filtered = (extraSwimlanes ?? []).map(swimlane => + swimlane.filter(entity), + ); + return !filtered.some(result => result === true); + }, + }; return ( @@ -100,14 +110,17 @@ export const ScaffolderPageContents = ({
- {ExtraSwimlanes && - ExtraSwimlanes.map(swimlane => ( + {extraSwimlanes && + extraSwimlanes.map(swimlane => ( ))} - +
@@ -117,12 +130,12 @@ export const ScaffolderPageContents = ({ export const ScaffolderPage = ({ TemplateCardComponent, - ExtraSwimlanes, + extraSwimlanes, }: ScaffolderPageProps) => ( ); diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx index 939fe7f59c..a30655d6e8 100644 --- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -35,84 +35,64 @@ export type TemplateListProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; - Swimlane?: { - title: React.ReactNode; + swimlane?: { + title?: string; + titleComponent?: React.ReactNode; filter: (entity: Entity) => boolean; }; }; -type TemplateCardGridProps = { - Card: ComponentType<{ template: TemplateEntityV1beta2 }>; - entities: Entity[]; -}; -const TemplateCardGrid = ({ Card, entities }: TemplateCardGridProps) => { - return ( - - {entities && - entities?.length > 0 && - entities.map((template: Entity) => ( - - ))} - - ); -}; - export const TemplateList = ({ TemplateCardComponent, - Swimlane, + swimlane, }: TemplateListProps) => { const { loading, error, entities } = useEntityListProvider(); const Card = TemplateCardComponent || TemplateCard; - if (Swimlane === null || Swimlane === undefined) { - return ( - <> - {loading && } - - {error && ( - - {error.message} - - )} - - {!error && !loading && !entities.length && ( - - No templates found that match your filter. Learn more about{' '} - - adding templates - - . - - )} - - - - - - ); - } - - const filteredEntities = entities.filter((entity: Entity) => - Swimlane.filter(entity), + const maybeFilteredEntities = swimlane + ? entities.filter(e => swimlane.filter(e)) + : entities; + const title = swimlane ? ( + swimlane.titleComponent || + ) : ( + ); - if (filteredEntities.length === 0) { + if (swimlane && maybeFilteredEntities.length === 0) { return null; } - return ( - - {Swimlane.title} + <> {loading && } + {error && ( {error.message} )} - - + {!error && !loading && !entities.length && ( + + No templates found that match your filter. Learn more about{' '} + + adding templates + + . + + )} + + + {title} + + {maybeFilteredEntities && + maybeFilteredEntities?.length > 0 && + maybeFilteredEntities.map((template: Entity) => ( + + ))} + + + ); };