diff --git a/.changeset/witty-cats-tell.md b/.changeset/witty-cats-tell.md new file mode 100644 index 0000000000..c60be53d03 --- /dev/null +++ b/.changeset/witty-cats-tell.md @@ -0,0 +1,6 @@ +--- +'example-app': patch +'@backstage/plugin-scaffolder': patch +--- + +Add swim lane filtering to the scaffolder page so that individuals can surface specific templates to end users ahead of others, or group templates together diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 401665b62e..b1a2235288 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -32,6 +32,7 @@ import { AlertDisplay, OAuthRequestDialog, SignInPage, + ContentHeader, } from '@backstage/core-components'; import { apiDocsPlugin, ApiExplorerPage } from '@backstage/plugin-api-docs'; import { @@ -175,7 +176,20 @@ const routes = ( > {techDocsPage} - }> + , + filter: entity => + entity?.metadata?.tags?.includes('recommended') || false, + }, + ]} + /> + } + > diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 0ac6910264..164f9acf83 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -24,6 +24,7 @@ import { JSONSchema } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/types'; import { Observable } from '@backstage/types'; import { default as React_2 } from 'react'; +import { ReactNode } from 'react'; import { RouteRef } from '@backstage/core-plugin-api'; import { ScmIntegrationRegistry } from '@backstage/integration'; import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; @@ -213,12 +214,19 @@ export const ScaffolderFieldExtensions: React_2.ComponentType; // @public (undocumented) export const ScaffolderPage: ({ TemplateCardComponent, + ExtraSwimlanes, }: { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2; }> | undefined; + ExtraSwimlanes?: + | { + title: ReactNode; + filter: (entity: Entity) => boolean; + }[] + | undefined; }) => JSX.Element; // Warning: (ae-missing-release-tag) "scaffolderPlugin" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -240,7 +248,8 @@ export { scaffolderPlugin }; // @public (undocumented) export const TemplateList: ({ TemplateCardComponent, -}: TemplateListProps) => JSX.Element; + 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) // @@ -251,6 +260,10 @@ export type TemplateListProps = { template: TemplateEntityV1beta2; }> | undefined; + Swimlane?: { + title: React_2.ReactNode; + filter: (entity: Entity) => boolean; + }; }; // Warning: (ae-missing-release-tag) "TemplateTypePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index 9e4736ab9f..5d4e750777 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -16,7 +16,7 @@ import React, { ComponentType } from 'react'; import { Routes, Route, useOutlet } from 'react-router'; -import { TemplateEntityV1beta2 } from '@backstage/catalog-model'; +import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model'; import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; @@ -34,9 +34,16 @@ type RouterProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; + ExtraSwimlanes?: Array<{ + title: React.ReactNode; + filter: (entity: Entity) => boolean; + }>; }; -export const Router = ({ TemplateCardComponent }: RouterProps) => { +export const Router = ({ + TemplateCardComponent, + ExtraSwimlanes, +}: RouterProps) => { const outlet = useOutlet(); const customFieldExtensions = useElementFilter(outlet, elements => @@ -64,7 +71,10 @@ export const Router = ({ TemplateCardComponent }: RouterProps) => { + } /> | undefined; + ExtraSwimlanes?: Array<{ + title: React.ReactNode; + filter: (entity: Entity) => boolean; + }>; }; export const ScaffolderPageContents = ({ TemplateCardComponent, + ExtraSwimlanes, }: ScaffolderPageProps) => { const styles = useStyles(); - const registerComponentLink = useRouteRef(registerComponentRouteRef); return ( @@ -96,6 +100,13 @@ export const ScaffolderPageContents = ({
+ {ExtraSwimlanes && + ExtraSwimlanes.map(swimlane => ( + + ))}
@@ -106,8 +117,12 @@ export const ScaffolderPageContents = ({ export const ScaffolderPage = ({ TemplateCardComponent, + ExtraSwimlanes, }: ScaffolderPageProps) => ( - + ); diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx index dcf708db10..939fe7f59c 100644 --- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx +++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx @@ -16,10 +16,13 @@ import React, { ComponentType } from 'react'; import { + Entity, stringifyEntityRef, TemplateEntityV1beta2, } from '@backstage/catalog-model'; import { + Content, + ContentHeader, ItemCardGrid, Progress, WarningPanel, @@ -32,41 +35,84 @@ export type TemplateListProps = { TemplateCardComponent?: | ComponentType<{ template: TemplateEntityV1beta2 }> | undefined; + Swimlane?: { + title: React.ReactNode; + filter: (entity: Entity) => boolean; + }; }; -export const TemplateList = ({ TemplateCardComponent }: TemplateListProps) => { +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, +}: TemplateListProps) => { const { loading, error, entities } = useEntityListProvider(); const Card = TemplateCardComponent || TemplateCard; - return ( - <> - {loading && } + 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), + ); + + if (filteredEntities.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 - - . - - )} - - - {entities && - entities?.length > 0 && - entities.map(template => ( - - ))} - - + + ); };