Merge pull request #8252 from brexhq/jrusso/add-template-swimlanes

Add swimlanes to scaffolder page
This commit is contained in:
Tim Hansen
2021-11-28 20:31:43 -07:00
committed by GitHub
8 changed files with 152 additions and 20 deletions
+17
View File
@@ -0,0 +1,17 @@
---
'@backstage/plugin-scaffolder': patch
---
Add group filtering to the scaffolder page so that individuals can surface specific templates to end users ahead of others, or group templates together. This can be accomplished by passing in a `groups` prop to the `ScaffolderPage`
```
<ScaffolderPage
groups={[
{
title: "Recommended",
filter: entity =>
entity?.metadata?.tags?.includes('recommended') ?? false,
},
]}
/>
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -53,3 +53,30 @@ 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 `groups` and passing them to your `ScaffolderPage`
like below
```
<ScaffolderPage
groups={[
{
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 group or others.
You can also further customize groups 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)
+14 -1
View File
@@ -175,7 +175,20 @@ const routes = (
>
{techDocsPage}
</Route>
<Route path="/create" element={<ScaffolderPage />}>
<Route
path="/create"
element={
<ScaffolderPage
groups={[
{
title: 'Recommended',
filter: entity =>
entity?.metadata?.tags?.includes('recommended') ?? false,
},
]}
/>
}
>
<ScaffolderFieldExtensions>
<LowerCaseValuePickerFieldExtension />
</ScaffolderFieldExtensions>
+16 -1
View File
@@ -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,20 @@ export const ScaffolderFieldExtensions: React_2.ComponentType;
// @public (undocumented)
export const ScaffolderPage: ({
TemplateCardComponent,
groups,
}: {
TemplateCardComponent?:
| ComponentType<{
template: TemplateEntityV1beta2;
}>
| undefined;
groups?:
| {
title?: string | undefined;
titleComponent?: 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 +249,8 @@ export { scaffolderPlugin };
// @public (undocumented)
export const TemplateList: ({
TemplateCardComponent,
}: TemplateListProps) => JSX.Element;
group,
}: 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 +261,11 @@ export type TemplateListProps = {
template: TemplateEntityV1beta2;
}>
| undefined;
group?: {
title?: string;
titleComponent?: 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)
+11 -3
View File
@@ -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,14 @@ type RouterProps = {
TemplateCardComponent?:
| ComponentType<{ template: TemplateEntityV1beta2 }>
| undefined;
groups?: Array<{
title?: string;
titleComponent?: React.ReactNode;
filter: (entity: Entity) => boolean;
}>;
};
export const Router = ({ TemplateCardComponent }: RouterProps) => {
export const Router = ({ TemplateCardComponent, groups }: RouterProps) => {
const outlet = useOutlet();
const customFieldExtensions = useElementFilter(outlet, elements =>
@@ -64,7 +69,10 @@ export const Router = ({ TemplateCardComponent }: RouterProps) => {
<Route
path="/"
element={
<ScaffolderPage TemplateCardComponent={TemplateCardComponent} />
<ScaffolderPage
TemplateCardComponent={TemplateCardComponent}
groups={groups}
/>
}
/>
<Route
@@ -23,7 +23,7 @@ import {
Page,
SupportButton,
} from '@backstage/core-components';
import { TemplateEntityV1beta2 } from '@backstage/catalog-model';
import { TemplateEntityV1beta2, Entity } from '@backstage/catalog-model';
import { useRouteRef } from '@backstage/core-plugin-api';
import {
EntityKindPicker,
@@ -51,14 +51,26 @@ export type ScaffolderPageProps = {
TemplateCardComponent?:
| ComponentType<{ template: TemplateEntityV1beta2 }>
| undefined;
groups?: Array<{
title?: string;
titleComponent?: React.ReactNode;
filter: (entity: Entity) => boolean;
}>;
};
export const ScaffolderPageContents = ({
TemplateCardComponent,
groups,
}: ScaffolderPageProps) => {
const styles = useStyles();
const registerComponentLink = useRouteRef(registerComponentRouteRef);
const otherTemplatesGroup = {
title: groups ? 'Other Templates' : 'Templates',
filter: (entity: Entity) => {
const filtered = (groups ?? []).map(group => group.filter(entity));
return !filtered.some(result => result === true);
},
};
return (
<Page themeId="home">
@@ -96,7 +108,17 @@ export const ScaffolderPageContents = ({
<EntityTagPicker />
</div>
<div>
<TemplateList TemplateCardComponent={TemplateCardComponent} />
{groups &&
groups.map(group => (
<TemplateList
TemplateCardComponent={TemplateCardComponent}
group={group}
/>
))}
<TemplateList
TemplateCardComponent={TemplateCardComponent}
group={otherTemplatesGroup}
/>
</div>
</div>
</Content>
@@ -106,8 +128,12 @@ export const ScaffolderPageContents = ({
export const ScaffolderPage = ({
TemplateCardComponent,
groups,
}: ScaffolderPageProps) => (
<EntityListProvider>
<ScaffolderPageContents TemplateCardComponent={TemplateCardComponent} />
<ScaffolderPageContents
TemplateCardComponent={TemplateCardComponent}
groups={groups}
/>
</EntityListProvider>
);
@@ -16,10 +16,13 @@
import React, { ComponentType } from 'react';
import {
Entity,
stringifyEntityRef,
TemplateEntityV1beta2,
} from '@backstage/catalog-model';
import {
Content,
ContentHeader,
ItemCardGrid,
Progress,
WarningPanel,
@@ -32,11 +35,31 @@ export type TemplateListProps = {
TemplateCardComponent?:
| ComponentType<{ template: TemplateEntityV1beta2 }>
| undefined;
group?: {
title?: string;
titleComponent?: React.ReactNode;
filter: (entity: Entity) => boolean;
};
};
export const TemplateList = ({ TemplateCardComponent }: TemplateListProps) => {
export const TemplateList = ({
TemplateCardComponent,
group,
}: TemplateListProps) => {
const { loading, error, entities } = useEntityListProvider();
const Card = TemplateCardComponent || TemplateCard;
const maybeFilteredEntities = group
? entities.filter(e => group.filter(e))
: entities;
const title = group ? (
group.titleComponent || <ContentHeader title={group.title} />
) : (
<ContentHeader title="Other Templates" />
);
if (group && maybeFilteredEntities.length === 0) {
return null;
}
return (
<>
{loading && <Progress />}
@@ -57,16 +80,19 @@ export const TemplateList = ({ TemplateCardComponent }: TemplateListProps) => {
</Typography>
)}
<ItemCardGrid>
{entities &&
entities?.length > 0 &&
entities.map(template => (
<Card
key={stringifyEntityRef(template)}
template={template as TemplateEntityV1beta2}
/>
))}
</ItemCardGrid>
<Content>
{title}
<ItemCardGrid>
{maybeFilteredEntities &&
maybeFilteredEntities?.length > 0 &&
maybeFilteredEntities.map((template: Entity) => (
<Card
key={stringifyEntityRef(template)}
template={template as TemplateEntityV1beta2}
/>
))}
</ItemCardGrid>
</Content>
</>
);
};