diff --git a/.changeset/tidy-bugs-throw.md b/.changeset/tidy-bugs-throw.md new file mode 100644 index 0000000000..e2785e588c --- /dev/null +++ b/.changeset/tidy-bugs-throw.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-org': patch +--- + +Add search functionality in MembersListCard diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx index bdc9456fc1..891c1ba3c7 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.test.tsx @@ -136,7 +136,7 @@ describe('MemberTab Test', () => { expect(screen.getByText('Super Awesome Developer')).toBeInTheDocument(); - expect(screen.getByText('Members (1)')).toBeInTheDocument(); + expect(screen.getByText('Members (1 of 1)')).toBeInTheDocument(); }); it('Can render different member display title', async () => { @@ -154,7 +154,7 @@ describe('MemberTab Test', () => { }, ); - expect(screen.getByText('Testers (1)')).toBeInTheDocument(); + expect(screen.getByText('Testers (1 of 1)')).toBeInTheDocument(); }); it('Can query a different relationship', async () => { @@ -435,4 +435,56 @@ describe('MemberTab Test', () => { const displayedMemberNames = screen.queryAllByTestId('user-link'); expect(displayedMemberNames).toHaveLength(5); }); + + describe('Search', () => { + it('filters members by name', async () => { + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/catalog': rootRouteRef, + }, + }, + ); + + // type into the search field + await userEvent.type(screen.getByPlaceholderText(/search/i), 'Tara'); + + // Tara should show + expect(await screen.findByText('Tara MacGovern')).toBeInTheDocument(); + + // Someone else not in results should be missing + expect(screen.queryByText('Nigel Manning')).not.toBeInTheDocument(); + }); + + it('shows no members message when search yields no results', async () => { + await renderInTestApp( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + '/catalog': rootRouteRef, + }, + }, + ); + + await userEvent.type( + screen.getByPlaceholderText(/search/i), + 'XYZDoesNotExist', + ); + + expect( + screen.getByText(/This group has no members./i), + ).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx index 07119ba85b..ee2f37ea9c 100644 --- a/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx +++ b/plugins/org/src/components/Cards/Group/MembersList/MembersListCard.tsx @@ -31,7 +31,7 @@ import Switch from '@material-ui/core/Switch'; import Typography from '@material-ui/core/Typography'; import { createStyles, makeStyles, Theme } from '@material-ui/core/styles'; import Pagination from '@material-ui/lab/Pagination'; -import { useState, ChangeEvent } from 'react'; +import { useState, useEffect, ChangeEvent } from 'react'; import useAsync from 'react-use/esm/useAsync'; import { @@ -50,6 +50,7 @@ import { import { EntityRelationAggregation } from '../../types'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { orgTranslationRef } from '../../../../translation'; +import TextField from '@material-ui/core/TextField'; /** @public */ export type MemberComponentClassKey = 'card' | 'avatar'; @@ -190,6 +191,12 @@ export const MembersListCard = (props: { relationAggregation === 'aggregated', ); + const [searchTerm, setSearchTerm] = useState(''); + + useEffect(() => { + setPage(1); + }, [searchTerm]); + const { loading: loadingDescendantMembers, value: descendantMembers } = useAsync(async () => { if (!showAggregateMembers) { @@ -257,13 +264,31 @@ export const MembersListCard = (props: { /> ); + const filteredMembers = members.filter(member => { + const fields = [ + member.metadata.name, + member.metadata.title, + member.spec?.profile?.displayName, + member.spec?.profile?.email, + ]; + return fields.some(val => + val + ?.toLocaleLowerCase('en-US') + .includes(searchTerm.toLocaleLowerCase('en-US')), + ); + }); + + const membersToRender = searchTerm ? filteredMembers : members; + let memberList: JSX.Element; - if (members && members.length > 0) { + if (membersToRender && membersToRender.length > 0) { memberList = ( - {members.slice(pageSize * (page - 1), pageSize * page).map(member => ( - - ))} + {membersToRender + .slice(pageSize * (page - 1), pageSize * page) + .map(member => ( + + ))} ); } else { @@ -277,9 +302,7 @@ export const MembersListCard = (props: { return ( ) : ( - memberList + <> + ) => + setSearchTerm(e.target.value) + } + /> + {memberList} + )}