Scaffolder: search/filter cleanup

This commit is contained in:
Oscar Hernandez
2021-02-10 10:29:17 -06:00
parent 8cea525a32
commit 6759060064
4 changed files with 192 additions and 228 deletions
@@ -87,25 +87,27 @@ export const ResultsFilter = ({ availableCategories }: Props) => {
Categories
</Typography>
<List disablePadding dense>
{availableCategories.map(t => {
const labelId = `checkbox-list-label-${t}`;
{availableCategories.map(category => {
const labelId = `checkbox-list-label-${category}`;
return (
<ListItem
key={t}
key={category}
dense
button
onClick={() =>
updateSelectedCategories(
selectedCategories.includes(t)
? selectedCategories.filter(s => s !== t)
: [...selectedCategories, t],
selectedCategories.includes(category)
? selectedCategories.filter(
selectedCategory => selectedCategory !== category,
)
: [...selectedCategories, category],
)
}
>
<Checkbox
edge="start"
color="primary"
checked={selectedCategories.includes(t)}
checked={selectedCategories.includes(category)}
tabIndex={-1}
disableRipple
className={classes.checkbox}
@@ -113,7 +115,7 @@ export const ResultsFilter = ({ availableCategories }: Props) => {
/>
<ListItemText
id={labelId}
primary={t.charAt(0).toUpperCase() + t.slice(1)}
primary={category.charAt(0).toUpperCase() + category.slice(1)}
/>
</ListItem>
);
@@ -14,8 +14,9 @@
* limitations under the License.
*/
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import React from 'react';
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
import { fireEvent, render, waitFor } from '@testing-library/react';
import {
ApiProvider,
ApiRegistry,
@@ -23,10 +24,9 @@ import {
identityApiRef,
storageApiRef,
} from '@backstage/core';
import { CatalogApi } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { MockStorageApi, wrapInTestApp } from '@backstage/test-utils';
import { fireEvent, render, waitFor } from '@testing-library/react';
import React from 'react';
import { EntityFilterGroupsProvider } from '../../filter';
import { ButtonGroup, ScaffolderFilter } from './ScaffolderFilter';
@@ -80,185 +80,192 @@ describe('Catalog Filter', () => {
),
);
it('should render the different groups', async () => {
const mockGroups: ButtonGroup[] = [
{ name: 'Test Group 1', items: [] },
{ name: 'Test Group 2', items: [] },
];
const { findByText } = renderWrapped(
<ScaffolderFilter buttonGroups={mockGroups} initiallySelected="" />,
);
for (const group of mockGroups) {
expect(await findByText(group.name)).toBeInTheDocument();
}
});
it('should render the different items and their names', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'all',
label: 'First Label',
filterFn: () => true,
},
{
id: 'starred',
label: 'Second Label',
filterFn: () => false,
},
],
},
];
const { findByText } = renderWrapped(
<ScaffolderFilter buttonGroups={mockGroups} initiallySelected="all" />,
);
for (const item of mockGroups[0].items) {
expect(await findByText(item.label)).toBeInTheDocument();
}
});
it('selects the first item if no desired initial one is set', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'all',
label: 'First Label',
filterFn: () => true,
},
{
id: 'starred',
label: 'Second Label',
filterFn: () => false,
},
],
},
];
const onChange = jest.fn();
renderWrapped(
<ScaffolderFilter
buttonGroups={mockGroups}
initiallySelected="all"
onChange={onChange}
/>,
);
await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith({
id: 'all',
label: 'First Label',
});
describe('filter groups', () => {
it('should render the different groups', async () => {
const mockGroups: ButtonGroup[] = [
{ name: 'Test Group 1', items: [] },
{ name: 'Test Group 2', items: [] },
];
const { findByText } = renderWrapped(
<ScaffolderFilter buttonGroups={mockGroups} initiallySelected="" />,
);
for (const group of mockGroups) {
expect(await findByText(group.name)).toBeInTheDocument();
}
});
});
it('selects the initial item', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'all',
label: 'First Label',
filterFn: () => true,
},
{
id: 'starred',
label: 'Second Label',
filterFn: () => false,
},
],
},
];
describe('filter items', () => {
it('should render the different items and their names', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'all',
label: 'First Label',
filterFn: () => true,
},
{
id: 'starred',
label: 'Second Label',
filterFn: () => false,
},
],
},
];
const onChange = jest.fn();
const { findByText } = renderWrapped(
<ScaffolderFilter buttonGroups={mockGroups} initiallySelected="all" />,
);
renderWrapped(
<ScaffolderFilter
buttonGroups={mockGroups}
onChange={onChange}
initiallySelected="starred"
/>,
);
await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith({
id: 'starred',
label: 'Second Label',
});
for (const item of mockGroups[0].items) {
expect(await findByText(item.label)).toBeInTheDocument();
}
});
});
it('can change the selected item', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'all',
label: 'First Label',
filterFn: () => true,
},
{
id: 'starred',
label: 'Second Label',
filterFn: () => false,
},
],
},
];
it('selects the first item if no desired initial one is set', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'all',
label: 'First Label',
filterFn: () => true,
},
{
id: 'starred',
label: 'Second Label',
filterFn: () => false,
},
],
},
];
const onChange = jest.fn();
const onChange = jest.fn();
const { findByText } = renderWrapped(
<ScaffolderFilter
buttonGroups={mockGroups}
initiallySelected="all"
onChange={onChange}
/>,
);
renderWrapped(
<ScaffolderFilter
buttonGroups={mockGroups}
initiallySelected="all"
onChange={onChange}
/>,
);
await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith({
id: 'all',
label: 'First Label',
await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith({
id: 'all',
label: 'First Label',
});
});
});
fireEvent.click(await findByText('Second Label'));
it('selects the initial item', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'all',
label: 'First Label',
filterFn: () => true,
},
{
id: 'starred',
label: 'Second Label',
filterFn: () => false,
},
],
},
];
await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith({
id: 'starred',
label: 'Second Label',
const onChange = jest.fn();
renderWrapped(
<ScaffolderFilter
buttonGroups={mockGroups}
onChange={onChange}
initiallySelected="starred"
/>,
);
await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith({
id: 'starred',
label: 'Second Label',
});
});
});
});
it('displays match counts properly', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'owned',
label: 'First Label',
filterFn: entity => entity.spec?.owner === 'tools@example.com',
},
],
},
];
it('can change the selected item', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'all',
label: 'First Label',
filterFn: () => true,
},
{
id: 'starred',
label: 'Second Label',
filterFn: () => false,
},
],
},
];
const { findByText } = renderWrapped(
<ScaffolderFilter buttonGroups={mockGroups} initiallySelected="owned" />,
);
const onChange = jest.fn();
expect(await findByText('1')).toBeInTheDocument();
const { findByText } = renderWrapped(
<ScaffolderFilter
buttonGroups={mockGroups}
initiallySelected="all"
onChange={onChange}
/>,
);
await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith({
id: 'all',
label: 'First Label',
});
});
fireEvent.click(await findByText('Second Label'));
await waitFor(() => {
expect(onChange).toHaveBeenLastCalledWith({
id: 'starred',
label: 'Second Label',
});
});
});
it('displays match counts properly', async () => {
const mockGroups: ButtonGroup[] = [
{
name: 'Test Group 1',
items: [
{
id: 'owned',
label: 'First Label',
filterFn: entity => entity.spec?.owner === 'tools@example.com',
},
],
},
];
const { findByText } = renderWrapped(
<ScaffolderFilter
buttonGroups={mockGroups}
initiallySelected="owned"
/>,
);
expect(await findByText('1')).toBeInTheDocument();
});
});
});
@@ -14,7 +14,8 @@
* limitations under the License.
*/
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import React, { useEffect, useMemo, useState } from 'react';
import { EntityMeta, TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import {
configApiRef,
Content,
@@ -28,7 +29,6 @@ import {
WarningPanel,
} from '@backstage/core';
import { Button, Grid, Link, makeStyles, Typography } from '@material-ui/core';
import React, { useEffect, useMemo, useState } from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { EntityFilterGroupsProvider, useFilteredEntities } from '../../filter';
import { TemplateCard, TemplateCardProps } from '../TemplateCard';
@@ -36,7 +36,6 @@ import { ResultsFilter } from '../ResultsFilter/ResultsFilter';
import { ScaffolderFilter } from '../ScaffolderFilter';
import { ButtonGroup } from '../ScaffolderFilter/ScaffolderFilter';
import StarIcon from '@material-ui/icons/Star';
import { useOwnUser } from '../useOwnUser';
import { useStarredEntities } from '../../hooks/useStarredEntities';
import SearchToolbar from '../SearchToolbar/SearchToolbar';
@@ -104,21 +103,18 @@ export const ScaffolderPageContents = () => {
[] as TemplateEntityV1alpha1[],
);
// Match templates by search input value
const matchesQuery = (metadata: EntityMeta, query: string) =>
`${metadata.title}`.toUpperCase().indexOf(query) !== -1 ||
metadata.tags?.join('').toUpperCase().indexOf(query) !== -1;
useEffect(() => {
const searchUppercase = search.toUpperCase();
if (search.length === 0) {
return setMatchingEntities(filteredEntities);
}
// Match search by title|tags
return setMatchingEntities(
filteredEntities.filter(template => {
const { title, tags } = template.metadata;
return (
`${title}`.toUpperCase().indexOf(searchUppercase) !== -1 ||
tags?.join('').toUpperCase().indexOf(searchUppercase) !== -1
);
}),
filteredEntities.filter(template =>
matchesQuery(template.metadata, search.toUpperCase()),
),
);
}, [search, filteredEntities]);
@@ -1,41 +0,0 @@
/*
* Copyright 2020 Spotify AB
*
* 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 { UserEntity } from '@backstage/catalog-model';
import { identityApiRef, useApi } from '@backstage/core';
import { catalogApiRef } from '@backstage/plugin-catalog-react';
import { useAsync } from 'react-use';
import { AsyncState } from 'react-use/lib/useAsync';
/**
* Get the catalog User entity (if any) that matches the logged-in user.
*/
export function useOwnUser(): AsyncState<UserEntity | undefined> {
const catalogApi = useApi(catalogApiRef);
const identityApi = useApi(identityApiRef);
// TODO: get the full entity (or at least the full entity name) from the
// identityApi
return useAsync(
() =>
catalogApi.getEntityByName({
kind: 'User',
namespace: 'default',
name: identityApi.getUserId(),
}) as Promise<UserEntity | undefined>,
[catalogApi, identityApi],
);
}