@@ -1,5 +1,4 @@
|
||||
---
|
||||
'example-app': patch
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -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
|
||||
|
||||
```
|
||||
<ScaffolderPage
|
||||
extraSwimlanes={[
|
||||
{
|
||||
title: "Recommended",
|
||||
filter: entity =>
|
||||
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.
|
||||

|
||||
|
||||
@@ -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={
|
||||
<ScaffolderPage
|
||||
ExtraSwimlanes={[
|
||||
extraSwimlanes={[
|
||||
{
|
||||
title: <ContentHeader title="Recommended" />,
|
||||
title: 'Recommended',
|
||||
filter: entity =>
|
||||
entity?.metadata?.tags?.includes('recommended') || false,
|
||||
entity?.metadata?.tags?.includes('recommended') ?? false,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
};
|
||||
|
||||
@@ -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={
|
||||
<ScaffolderPage
|
||||
TemplateCardComponent={TemplateCardComponent}
|
||||
ExtraSwimlanes={ExtraSwimlanes}
|
||||
extraSwimlanes={extraSwimlanes}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -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 (
|
||||
<Page themeId="home">
|
||||
@@ -100,14 +110,17 @@ export const ScaffolderPageContents = ({
|
||||
<EntityTagPicker />
|
||||
</div>
|
||||
<div>
|
||||
{ExtraSwimlanes &&
|
||||
ExtraSwimlanes.map(swimlane => (
|
||||
{extraSwimlanes &&
|
||||
extraSwimlanes.map(swimlane => (
|
||||
<TemplateList
|
||||
TemplateCardComponent={TemplateCardComponent}
|
||||
Swimlane={swimlane}
|
||||
swimlane={swimlane}
|
||||
/>
|
||||
))}
|
||||
<TemplateList TemplateCardComponent={TemplateCardComponent} />
|
||||
<TemplateList
|
||||
TemplateCardComponent={TemplateCardComponent}
|
||||
swimlane={otherTemplatesSwimlane}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Content>
|
||||
@@ -117,12 +130,12 @@ export const ScaffolderPageContents = ({
|
||||
|
||||
export const ScaffolderPage = ({
|
||||
TemplateCardComponent,
|
||||
ExtraSwimlanes,
|
||||
extraSwimlanes,
|
||||
}: ScaffolderPageProps) => (
|
||||
<EntityListProvider>
|
||||
<ScaffolderPageContents
|
||||
TemplateCardComponent={TemplateCardComponent}
|
||||
ExtraSwimlanes={ExtraSwimlanes}
|
||||
extraSwimlanes={extraSwimlanes}
|
||||
/>
|
||||
</EntityListProvider>
|
||||
);
|
||||
|
||||
@@ -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 (
|
||||
<ItemCardGrid>
|
||||
{entities &&
|
||||
entities?.length > 0 &&
|
||||
entities.map((template: Entity) => (
|
||||
<Card
|
||||
key={stringifyEntityRef(template)}
|
||||
template={template as TemplateEntityV1beta2}
|
||||
/>
|
||||
))}
|
||||
</ItemCardGrid>
|
||||
);
|
||||
};
|
||||
|
||||
export const TemplateList = ({
|
||||
TemplateCardComponent,
|
||||
Swimlane,
|
||||
swimlane,
|
||||
}: TemplateListProps) => {
|
||||
const { loading, error, entities } = useEntityListProvider();
|
||||
const Card = TemplateCardComponent || TemplateCard;
|
||||
if (Swimlane === null || Swimlane === undefined) {
|
||||
return (
|
||||
<>
|
||||
{loading && <Progress />}
|
||||
|
||||
{error && (
|
||||
<WarningPanel title="Oops! Something went wrong loading the templates">
|
||||
{error.message}
|
||||
</WarningPanel>
|
||||
)}
|
||||
|
||||
{!error && !loading && !entities.length && (
|
||||
<Typography variant="body2">
|
||||
No templates found that match your filter. Learn more about{' '}
|
||||
<Link href="https://backstage.io/docs/features/software-templates/adding-templates">
|
||||
adding templates
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
)}
|
||||
<Content>
|
||||
<ContentHeader title="All Templates" />
|
||||
<TemplateCardGrid Card={Card} entities={entities} />
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const filteredEntities = entities.filter((entity: Entity) =>
|
||||
Swimlane.filter(entity),
|
||||
const maybeFilteredEntities = swimlane
|
||||
? entities.filter(e => swimlane.filter(e))
|
||||
: entities;
|
||||
const title = swimlane ? (
|
||||
swimlane.titleComponent || <ContentHeader title={swimlane.title} />
|
||||
) : (
|
||||
<ContentHeader title="Other Templates" />
|
||||
);
|
||||
|
||||
if (filteredEntities.length === 0) {
|
||||
if (swimlane && maybeFilteredEntities.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Content>
|
||||
{Swimlane.title}
|
||||
<>
|
||||
{loading && <Progress />}
|
||||
|
||||
{error && (
|
||||
<WarningPanel title="Oops! Something went wrong loading the templates">
|
||||
{error.message}
|
||||
</WarningPanel>
|
||||
)}
|
||||
|
||||
<TemplateCardGrid Card={Card} entities={filteredEntities} />
|
||||
</Content>
|
||||
{!error && !loading && !entities.length && (
|
||||
<Typography variant="body2">
|
||||
No templates found that match your filter. Learn more about{' '}
|
||||
<Link href="https://backstage.io/docs/features/software-templates/adding-templates">
|
||||
adding templates
|
||||
</Link>
|
||||
.
|
||||
</Typography>
|
||||
)}
|
||||
|
||||
<Content>
|
||||
{title}
|
||||
<ItemCardGrid>
|
||||
{maybeFilteredEntities &&
|
||||
maybeFilteredEntities?.length > 0 &&
|
||||
maybeFilteredEntities.map((template: Entity) => (
|
||||
<Card
|
||||
key={stringifyEntityRef(template)}
|
||||
template={template as TemplateEntityV1beta2}
|
||||
/>
|
||||
))}
|
||||
</ItemCardGrid>
|
||||
</Content>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user