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}
+ >
)}