move search logic into SearchToolbar

Co-authored-by: Tim <timbonicus@gmail.com>
Signed-off-by: Chase Rutherford-Jenkins <chaseajen@users.noreply.github.com>
This commit is contained in:
Chase Rutherford-Jenkins
2021-06-11 14:39:14 -07:00
committed by Tim Hansen
parent f3a53bf046
commit bc2c35b2e1
5 changed files with 101 additions and 54 deletions
@@ -31,6 +31,7 @@ import {
EntityLifecycleFilter,
EntityOwnerFilter,
EntityTagFilter,
EntityTextFilter,
EntityTypeFilter,
UserListFilter,
} from '../types';
@@ -44,6 +45,7 @@ export type DefaultEntityFilters = {
owners?: EntityOwnerFilter;
lifecycles?: EntityLifecycleFilter;
tags?: EntityTagFilter;
text?: EntityTextFilter;
};
export type EntityListContextProps<
+19
View File
@@ -66,6 +66,25 @@ export class EntityTagFilter implements EntityFilter {
}
}
// TODO(chaseajen): add unit test for logic
export class EntityTextFilter implements EntityFilter {
constructor(readonly value: string) {}
filterEntity(entity: Entity): boolean {
const upperCaseValue = this.value.toLocaleUpperCase('en-US');
return (
`${entity.metadata.title}`
.toLocaleUpperCase('en-US')
.includes(upperCaseValue) ||
entity.metadata.tags
?.join('')
.toLocaleUpperCase('en-US')
.indexOf(upperCaseValue) !== -1
);
}
}
export class EntityOwnerFilter implements EntityFilter {
constructor(readonly values: string[]) {}
@@ -14,11 +14,7 @@
* limitations under the License.
*/
import {
Entity,
EntityMeta,
TemplateEntityV1alpha1,
} from '@backstage/catalog-model';
import { TemplateEntityV1alpha1 } from '@backstage/catalog-model';
import {
Content,
ContentHeader,
@@ -39,7 +35,7 @@ import {
UserListPicker,
} from '@backstage/plugin-catalog-react';
import { Button, Link, makeStyles, Typography } from '@material-ui/core';
import React, { useEffect, useState } from 'react';
import React from 'react';
import { Link as RouterLink } from 'react-router-dom';
import { registerComponentRouteRef } from '../../routes';
import SearchToolbar from '../SearchToolbar/SearchToolbar';
@@ -58,26 +54,8 @@ export const ScaffolderPageContents = () => {
const styles = useStyles();
const { loading, error, entities } = useEntityListProvider();
const [search, setSearch] = useState('');
const [matchingEntities, setMatchingEntities] = useState([] as Entity[]);
const matchesQuery = (metadata: EntityMeta, query: string) =>
`${metadata.title}`.toLocaleUpperCase('en-US').includes(query) ||
metadata.tags?.join('').toLocaleUpperCase('en-US').indexOf(query) !== -1;
const registerComponentLink = useRouteRef(registerComponentRouteRef);
useEffect(() => {
if (search.length === 0) {
return setMatchingEntities(entities);
}
return setMatchingEntities(
entities.filter(template =>
matchesQuery(template.metadata, search.toLocaleUpperCase('en-US')),
),
);
}, [search, entities]);
return (
<Page themeId="home">
<Header
@@ -110,8 +88,7 @@ export const ScaffolderPageContents = () => {
<div className={styles.contentWrapper}>
<div>
{/* TODO(mtlewis) extract SearchToolbar as a frontend filter */}
<SearchToolbar search={search} setSearch={setSearch} />
<SearchToolbar />
<EntityKindPicker initialFilter="template" hidden />
<UserListPicker
initialFilter="all"
@@ -133,23 +110,20 @@ export const ScaffolderPageContents = () => {
</WarningPanel>
)}
{!error &&
!loading &&
matchingEntities &&
!matchingEntities.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>
)}
{!error && !loading && entities && !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>
{matchingEntities &&
matchingEntities?.length > 0 &&
matchingEntities.map((template, i) => (
{entities &&
entities?.length > 0 &&
entities.map((template, i) => (
<TemplateCard
key={i}
template={template as TemplateEntityV1alpha1}
@@ -17,17 +17,59 @@
import React from 'react';
import { fireEvent, render } from '@testing-library/react';
import SearchToolbar from './SearchToolbar';
import {
DefaultEntityFilters,
EntityTextFilter,
MockEntityListContextProvider,
} from '@backstage/plugin-catalog-react';
import { Entity } from '@backstage/catalog-model';
const entities: Entity[] = [
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'react-app',
tags: ['react', 'experimental'],
},
},
{
apiVersion: '1',
kind: 'Component',
metadata: {
name: 'gRPC service',
tags: ['gRPC', 'java'],
},
},
];
describe('SearchToolbar', () => {
it('should display search value and execute set callback', async () => {
const setSearchSpy = jest.fn();
const updateFilters = jest.fn();
const filters: DefaultEntityFilters = {
text: new EntityTextFilter('hello'),
};
const { getByDisplayValue } = render(
<SearchToolbar search="hello" setSearch={setSearchSpy} />,
<MockEntityListContextProvider
value={{ entities, updateFilters, filters }}
>
<SearchToolbar />
</MockEntityListContextProvider>,
);
const searchInput = getByDisplayValue('hello');
expect(searchInput).toBeInTheDocument();
fireEvent.change(searchInput, { target: { value: 'world' } });
expect(setSearchSpy).toHaveBeenCalled();
expect(updateFilters).toHaveBeenCalledWith({
text: new EntityTextFilter('world'),
});
fireEvent.change(searchInput, { target: { value: '' } });
expect(updateFilters).toHaveBeenCalledWith({
text: undefined,
});
});
});
@@ -13,22 +13,21 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import {
EntityTextFilter,
useEntityListProvider,
} from '@backstage/plugin-catalog-react';
import {
FormControl,
IconButton,
Input,
InputAdornment,
makeStyles,
Toolbar,
Input,
IconButton,
} from '@material-ui/core';
import Search from '@material-ui/icons/Search';
import Clear from '@material-ui/icons/Clear';
interface Props {
search: string;
setSearch: Function;
}
import Search from '@material-ui/icons/Search';
import React, { useEffect, useState } from 'react';
const useStyles = makeStyles(_theme => ({
searchToolbar: {
@@ -37,8 +36,19 @@ const useStyles = makeStyles(_theme => ({
},
}));
const SearchToolbar = ({ search, setSearch }: Props) => {
// TODO(chaseajen): move component to /plugins/catalog-react
const SearchToolbar = () => {
const styles = useStyles();
const { filters, updateFilters } = useEntityListProvider();
const [search, setSearch] = useState(filters.text?.value ?? '');
useEffect(() => {
updateFilters({
text: search.length ? new EntityTextFilter(search) : undefined,
});
}, [search, updateFilters]);
return (
<Toolbar className={styles.searchToolbar}>
<FormControl>