Filter out templates

Signed-off-by: Bogdan Nechyporenko <bnechyporenko@bol.com>
This commit is contained in:
Bogdan Nechyporenko
2023-03-09 17:28:31 +01:00
committed by blam
parent 0182d61b98
commit 92cf86a4b5
11 changed files with 198 additions and 8 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Making it possible to hide software templates via templateFilter
@@ -93,3 +93,18 @@ 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)
There is also an option to hide some templates.
You can have several use cases for that:
- it's still in an experimental phase, so you can combine it with feature flagging for example
- you don't want to make them accessible from template list, but only open it on some action with pre-filled data.
- show different set of templates depends on target environment
```typescript jsx
<ScaffolderPage
templateFilter={entity =>
entity?.metadata?.tags?.includes('experimental') ?? false
}
/>
```
+5
View File
@@ -31,6 +31,7 @@ export type NextRouterProps = {
}>;
};
groups?: TemplateGroupFilter[];
templateFilter?: TemplatetemplateFilterFilter;
FormProps?: FormProps_2;
contextMenu?: {
editor?: boolean;
@@ -50,5 +51,9 @@ export type TemplateGroupFilter = {
filter: (entity: Entity) => boolean;
};
// Warnings were encountered during analysis:
//
// src/next/Router/Router.d.ts:22:5 - (ae-forgotten-export) The symbol "TemplatetemplateFilterFilter" needs to be exported by the entry point alpha.d.ts
// (No @packageDocumentation comment for this package)
```
+8 -1
View File
@@ -58,6 +58,7 @@ export type RouterProps = {
title?: React.ReactNode;
filter: (entity: Entity) => boolean;
}>;
templateFilter?: (entity: Entity) => boolean;
defaultPreviewTemplate?: string;
headerOptions?: {
pageTitleOverride?: string;
@@ -81,7 +82,12 @@ export type RouterProps = {
* @public
*/
export const Router = (props: RouterProps) => {
const { groups, components = {}, defaultPreviewTemplate } = props;
const {
groups,
templateFilter,
components = {},
defaultPreviewTemplate,
} = props;
const { ReviewStepComponent, TemplateCardComponent, TaskPageComponent } =
components;
@@ -126,6 +132,7 @@ export const Router = (props: RouterProps) => {
element={
<ScaffolderPage
groups={groups}
templateFilter={templateFilter}
TemplateCardComponent={TemplateCardComponent}
contextMenu={props.contextMenu}
headerOptions={props.headerOptions}
@@ -49,6 +49,7 @@ export type ScaffolderPageProps = {
title?: React.ReactNode;
filter: (entity: Entity) => boolean;
}>;
templateFilter?: (entity: Entity) => boolean;
contextMenu?: {
editor?: boolean;
actions?: boolean;
@@ -64,6 +65,7 @@ export type ScaffolderPageProps = {
export const ScaffolderPageContents = ({
TemplateCardComponent,
groups,
templateFilter,
contextMenu,
headerOptions,
}: ScaffolderPageProps) => {
@@ -123,11 +125,13 @@ export const ScaffolderPageContents = ({
key={index}
TemplateCardComponent={TemplateCardComponent}
group={group}
templateFilter={templateFilter}
/>
))}
<TemplateList
key="other"
TemplateCardComponent={TemplateCardComponent}
templateFilter={templateFilter}
group={otherTemplatesGroup}
/>
</CatalogFilterLayout.Content>
@@ -140,6 +144,7 @@ export const ScaffolderPageContents = ({
export const ScaffolderPage = ({
TemplateCardComponent,
groups,
templateFilter,
contextMenu,
headerOptions,
}: ScaffolderPageProps) => (
@@ -147,6 +152,7 @@ export const ScaffolderPage = ({
<ScaffolderPageContents
TemplateCardComponent={TemplateCardComponent}
groups={groups}
templateFilter={templateFilter}
contextMenu={contextMenu}
headerOptions={headerOptions}
/>
@@ -0,0 +1,83 @@
/*
* Copyright 2020 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { screen } from '@testing-library/react';
import React from 'react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { TemplateList } from './TemplateList';
import { rootRouteRef } from '../../routes';
import {
ScmIntegrationsApi,
scmIntegrationsApiRef,
} from '@backstage/integration-react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
jest.mock('@backstage/plugin-catalog-react', () => ({
useEntityList: jest.fn().mockReturnValue({
loading: false,
entities: [
{
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 't1',
},
spec: {},
},
{
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 't2',
},
spec: {},
},
],
}),
getEntityRelations: jest.fn().mockImplementation(() => []),
getEntitySourceLocation: jest.fn().mockImplementation(() => ({})),
}));
describe('TemplateList', () => {
const mockIntegrationsApi: Partial<ScmIntegrationsApi> = {
byHost: () => ({ type: 'github' }),
};
it('should filter out templates based on provided filter condition', async () => {
const TemplateCardComponent = ({
template,
}: {
template: TemplateEntityV1beta3;
}) => (
<div data-testid={template.metadata.name}>{template.metadata.name}</div>
);
await renderInTestApp(
<TestApiProvider apis={[[scmIntegrationsApiRef, mockIntegrationsApi]]}>
<div data-testid="container">
<TemplateList
templateFilter={e => e.metadata.name === 't1'}
TemplateCardComponent={TemplateCardComponent}
/>
</div>
</TestApiProvider>,
{ mountedRoutes: { '/': rootRouteRef } },
);
expect(() => screen.getByTestId('t1')).toThrow();
expect(screen.getByTestId('t2')).toBeDefined();
});
});
@@ -40,6 +40,7 @@ export type TemplateListProps = {
title?: React.ReactNode;
filter: (entity: Entity) => boolean;
};
templateFilter?: (entity: Entity) => boolean;
};
/**
@@ -48,12 +49,13 @@ export type TemplateListProps = {
export const TemplateList = ({
TemplateCardComponent,
group,
templateFilter,
}: TemplateListProps) => {
const { loading, error, entities } = useEntityList();
const Card = TemplateCardComponent || TemplateCard;
const maybeFilteredEntities = group
? entities.filter(e => group.filter(e))
: entities;
const maybeFilteredEntities = (
group ? entities.filter(e => group.filter(e)) : entities
).filter(e => (templateFilter ? !templateFilter(e) : true));
const titleComponent: React.ReactNode = (() => {
if (group && group.title) {
@@ -29,7 +29,10 @@ import {
} from '@backstage/plugin-scaffolder-react';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups';
import {
TemplateGroupFilter,
TemplatetemplateFilterFilter,
} from '../TemplateListPage/TemplateGroups';
import { DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS } from '../../extensions/default';
import {
@@ -61,6 +64,7 @@ export type NextRouterProps = {
}>;
};
groups?: TemplateGroupFilter[];
templateFilter?: TemplatetemplateFilterFilter;
// todo(blam): rename this to formProps
FormProps?: FormProps;
contextMenu?: {
@@ -111,6 +115,7 @@ export const Router = (props: PropsWithChildren<NextRouterProps>) => {
TemplateCardComponent={TemplateCardComponent}
contextMenu={props.contextMenu}
groups={props.groups}
templateFilter={props.templateFilter}
/>
}
/>
@@ -207,4 +207,52 @@ describe('TemplateGroups', () => {
{},
);
});
it('should filter out templates based on filter condition', async () => {
const mockEntities = [
{
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 't1',
},
spec: {},
},
{
apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 't2',
},
spec: {},
},
];
(useEntityList as jest.Mock).mockReturnValue({
entities: mockEntities,
loading: false,
error: null,
});
await renderInTestApp(
<TestApiProvider apis={[[errorApiRef, {}]]}>
<TemplateGroups
groups={[{ title: 'all', filter: _ => true }]}
templateFilter={e => e.metadata.name === 't1'}
/>
</TestApiProvider>,
{
mountedRoutes: {
'/next': nextRouteRef,
},
},
);
expect(TemplateGroup).toHaveBeenCalledWith(
expect.objectContaining({
templates: [expect.objectContaining({ template: mockEntities[1] })],
}),
{},
);
});
});
@@ -34,6 +34,8 @@ import { TemplateGroup } from '@backstage/plugin-scaffolder-react/alpha';
import { viewTechDocRouteRef, selectedTemplateRouteRef } from '../../routes';
import { useNavigate } from 'react-router-dom';
export type TemplatetemplateFilterFilter = (entity: Entity) => boolean;
/**
* @alpha
*/
@@ -44,6 +46,7 @@ export type TemplateGroupFilter = {
export interface TemplateGroupsProps {
groups: TemplateGroupFilter[];
templateFilter?: TemplatetemplateFilterFilter;
TemplateCardComponent?: React.ComponentType<{
template: TemplateEntityV1beta3;
}>;
@@ -51,7 +54,7 @@ export interface TemplateGroupsProps {
export const TemplateGroups = (props: TemplateGroupsProps) => {
const { loading, error, entities } = useEntityList();
const { groups, TemplateCardComponent } = props;
const { groups, templateFilter, TemplateCardComponent } = props;
const errorApi = useApi(errorApiRef);
const app = useApp();
const viewTechDocsLink = useRouteRef(viewTechDocRouteRef);
@@ -90,6 +93,7 @@ export const TemplateGroups = (props: TemplateGroupsProps) => {
<>
{groups.map(({ title, filter }, index) => {
const templates = entities
.filter(e => (templateFilter ? !templateFilter(e) : true))
.filter((e): e is TemplateEntityV1beta3 => filter(e))
.map(template => {
const { kind, namespace, name } = parseEntityRef(
@@ -35,7 +35,11 @@ import {
import { CategoryPicker } from './CategoryPicker';
import { RegisterExistingButton } from './RegisterExistingButton';
import { useRouteRef } from '@backstage/core-plugin-api';
import { TemplateGroupFilter, TemplateGroups } from './TemplateGroups';
import {
TemplateGroupFilter,
TemplateGroups,
TemplatetemplateFilterFilter,
} from './TemplateGroups';
import { registerComponentRouteRef } from '../../routes';
import { ContextMenu } from './ContextMenu';
@@ -44,6 +48,7 @@ export type TemplateListPageProps = {
template: TemplateEntityV1beta3;
}>;
groups?: TemplateGroupFilter[];
templateFilter?: TemplatetemplateFilterFilter;
contextMenu?: {
editor?: boolean;
actions?: boolean;
@@ -68,7 +73,11 @@ const createGroupsWithOther = (
export const TemplateListPage = (props: TemplateListPageProps) => {
const registerComponentLink = useRouteRef(registerComponentRouteRef);
const { TemplateCardComponent, groups: givenGroups = [] } = props;
const {
TemplateCardComponent,
groups: givenGroups = [],
templateFilter,
} = props;
const groups = givenGroups.length
? createGroupsWithOther(givenGroups)
@@ -111,6 +120,7 @@ export const TemplateListPage = (props: TemplateListPageProps) => {
<CatalogFilterLayout.Content>
<TemplateGroups
groups={groups}
templateFilter={templateFilter}
TemplateCardComponent={TemplateCardComponent}
/>
</CatalogFilterLayout.Content>