Add swimlanes to scaffolder page

Add swimlane filtering to the scaffolder page. This will allow
developers to surface specific templates ahead of others or group
templates together. This is just a first pass, in the future we can also
all developers to customize the listing component for fancier one

Fixes https://github.com/backstage/backstage/issues/6661s

Signed-off-by: jrusso1020 <jrusso@brex.com>
This commit is contained in:
jrusso1020
2021-11-25 20:10:47 -05:00
parent dc57a35ad5
commit ed5bef529e
6 changed files with 137 additions and 33 deletions
+6
View File
@@ -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
+15 -1
View File
@@ -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}
</Route>
<Route path="/create" element={<ScaffolderPage />}>
<Route
path="/create"
element={
<ScaffolderPage
ExtraSwimlanes={[
{
title: <ContentHeader title="Recommended" />,
filter: entity =>
entity?.metadata?.tags?.includes('recommended') || false,
},
]}
/>
}
>
<ScaffolderFieldExtensions>
<LowerCaseValuePickerFieldExtension />
</ScaffolderFieldExtensions>
+14 -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,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)
+13 -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,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) => {
<Route
path="/"
element={
<ScaffolderPage TemplateCardComponent={TemplateCardComponent} />
<ScaffolderPage
TemplateCardComponent={TemplateCardComponent}
ExtraSwimlanes={ExtraSwimlanes}
/>
}
/>
<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,13 +51,17 @@ export type ScaffolderPageProps = {
TemplateCardComponent?:
| ComponentType<{ template: TemplateEntityV1beta2 }>
| 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 = ({
<EntityTagPicker />
</div>
<div>
{ExtraSwimlanes &&
ExtraSwimlanes.map(swimlane => (
<TemplateList
TemplateCardComponent={TemplateCardComponent}
Swimlane={swimlane}
/>
))}
<TemplateList TemplateCardComponent={TemplateCardComponent} />
</div>
</div>
@@ -106,8 +117,12 @@ export const ScaffolderPageContents = ({
export const ScaffolderPage = ({
TemplateCardComponent,
ExtraSwimlanes,
}: ScaffolderPageProps) => (
<EntityListProvider>
<ScaffolderPageContents TemplateCardComponent={TemplateCardComponent} />
<ScaffolderPageContents
TemplateCardComponent={TemplateCardComponent}
ExtraSwimlanes={ExtraSwimlanes}
/>
</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,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 (
<ItemCardGrid>
{entities &&
entities?.length > 0 &&
entities.map((template: Entity) => (
<Card
key={stringifyEntityRef(template)}
template={template as TemplateEntityV1beta2}
/>
))}
</ItemCardGrid>
);
};
export const TemplateList = ({
TemplateCardComponent,
Swimlane,
}: TemplateListProps) => {
const { loading, error, entities } = useEntityListProvider();
const Card = TemplateCardComponent || TemplateCard;
return (
<>
{loading && <Progress />}
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),
);
if (filteredEntities.length === 0) {
return null;
}
return (
<Content>
{Swimlane.title}
{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>
)}
<ItemCardGrid>
{entities &&
entities?.length > 0 &&
entities.map(template => (
<Card
key={stringifyEntityRef(template)}
template={template as TemplateEntityV1beta2}
/>
))}
</ItemCardGrid>
</>
<TemplateCardGrid Card={Card} entities={filteredEntities} />
</Content>
);
};