diff --git a/apps/admin-ui/public/resources/en/groups.json b/apps/admin-ui/public/resources/en/groups.json index 42e1276a53..011beedfc5 100644 --- a/apps/admin-ui/public/resources/en/groups.json +++ b/apps/admin-ui/public/resources/en/groups.json @@ -22,6 +22,7 @@ "usersAdded_other": "{{count}} users added to the group", "usersAddedError": "Could not add users to the group: {{error}}", "search": "Search", + "exactSearch": "Exact search", "members": "Members", "searchMembers": "Search members", "addMember": "Add member", diff --git a/apps/admin-ui/src/groups/components/GroupTree.tsx b/apps/admin-ui/src/groups/components/GroupTree.tsx index d864939129..772c2b484c 100644 --- a/apps/admin-ui/src/groups/components/GroupTree.tsx +++ b/apps/admin-ui/src/groups/components/GroupTree.tsx @@ -2,10 +2,12 @@ import { useState } from "react"; import { Link } from "react-router-dom-v5-compat"; import { useTranslation } from "react-i18next"; import { + Checkbox, Dropdown, DropdownItem, DropdownPosition, DropdownSeparator, + InputGroup, KebabToggle, TreeView, TreeViewDataItem, @@ -109,6 +111,7 @@ export const GroupTree = ({ refresh: viewRefresh }: GroupTreeProps) => { const [search, setSearch] = useState(""); const [max, setMax] = useState(20); const [first, setFirst] = useState(0); + const [exact, setExact] = useState(false); const [key, setKey] = useState(0); const refresh = () => { @@ -151,12 +154,13 @@ export const GroupTree = ({ refresh: viewRefresh }: GroupTreeProps) => { { first: `${first}`, max: `${max + 1}`, + exact: `${exact}`, }, search === "" ? null : { search } ) ), (groups) => setData(groups.map((g) => mapGroup(g, [], refresh))), - [key, first, max, search] + [key, first, max, search, exact] ); return data ? ( @@ -173,6 +177,18 @@ export const GroupTree = ({ refresh: viewRefresh }: GroupTreeProps) => { inputGroupName="searchForGroups" inputGroupPlaceholder={t("groups:searchForGroups")} inputGroupOnEnter={setSearch} + toolbarItem={ + + setExact(value)} + /> + {t("exactSearch")} + + } > {data.length > 0 && ( 0} hasGuides /> diff --git a/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java b/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java index b56a4ecba0..9a12d8124a 100644 --- a/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java +++ b/keycloak-theme/src/main/java/org/keycloak/admin/ui/rest/GroupsResource.java @@ -10,6 +10,7 @@ import javax.ws.rs.GET; import javax.ws.rs.Produces; import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; + import org.eclipse.microprofile.openapi.annotations.Operation; import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; import org.eclipse.microprofile.openapi.annotations.media.Content; @@ -53,22 +54,23 @@ public class GroupsResource { )} ) public final Stream listGroups(@QueryParam("search") @DefaultValue("") final String search, @QueryParam("first") - @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("global") @DefaultValue("true") boolean global) { + @DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("global") @DefaultValue("true") boolean global, + @QueryParam("exact") @DefaultValue("false") boolean exact) { this.auth.groups().requireList(); final Stream stream; if (!"".equals(search)) { if (global) { - stream = this.realm.searchForGroupByNameStream(search, first, max); + stream = session.groups().searchForGroupByNameStream(realm, search, exact, first, max); } else { stream = this.realm.getTopLevelGroupsStream().filter(g -> g.getName().contains(search)).skip(first).limit(max); } } else { stream = this.realm.getTopLevelGroupsStream(first, max); } - return stream.map(g -> toGroupHierarchy(g, search)); + return stream.map(g -> toGroupHierarchy(g, search, exact)); } - private GroupRepresentation toGroupHierarchy(GroupModel group, final String search) { + private GroupRepresentation toGroupHierarchy(GroupModel group, final String search, boolean exact) { GroupRepresentation rep = toRepresentation(group, true); rep.setAccess(auth.groups().getAccess(group)); rep.setSubGroups(group.getSubGroupsStream().filter(g -> @@ -76,9 +78,9 @@ public class GroupsResource { g, search ) ).map(subGroup -> - ModelToRepresentation.toGroupHierarchy( - subGroup, true, search - ) + ModelToRepresentation.toGroupHierarchy( + subGroup, true, search, exact + ) ).collect(Collectors.toList())); return rep;