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:
committed by
Tim Hansen
parent
f3a53bf046
commit
bc2c35b2e1
@@ -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<
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user