United group filter and group search (#3259)
This commit is contained in:
parent
bd9dbfb78b
commit
3287927797
10 changed files with 138 additions and 279 deletions
|
@ -65,7 +65,7 @@ describe("Group test", () => {
|
||||||
.assertNoGroupsInThisRealmEmptyStateMessageExist(true)
|
.assertNoGroupsInThisRealmEmptyStateMessageExist(true)
|
||||||
.createGroup(groupName, true)
|
.createGroup(groupName, true)
|
||||||
.assertNotificationGroupCreated()
|
.assertNotificationGroupCreated()
|
||||||
.searchGroup(groupName)
|
.searchGroup(groupName, true)
|
||||||
.assertGroupItemExist(groupName, true);
|
.assertGroupItemExist(groupName, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -74,7 +74,7 @@ describe("Group test", () => {
|
||||||
.assertNoGroupsInThisRealmEmptyStateMessageExist(false)
|
.assertNoGroupsInThisRealmEmptyStateMessageExist(false)
|
||||||
.createGroup(groupName, false)
|
.createGroup(groupName, false)
|
||||||
.assertNotificationGroupCreated()
|
.assertNotificationGroupCreated()
|
||||||
.searchGroup(groupName)
|
.searchGroup(groupName, true)
|
||||||
.assertGroupItemExist(groupName, true);
|
.assertGroupItemExist(groupName, true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -84,7 +84,6 @@ describe("Group test", () => {
|
||||||
.createGroup(" ", false)
|
.createGroup(" ", false)
|
||||||
.assertNotificationCouldNotCreateGroupWithEmptyName();
|
.assertNotificationCouldNotCreateGroupWithEmptyName();
|
||||||
groupModal.closeModal();
|
groupModal.closeModal();
|
||||||
groupPage.searchGroup(" ").assertNoSearchResultsMessageExist(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Fail to create group with duplicated name", () => {
|
it("Fail to create group with duplicated name", () => {
|
||||||
|
@ -115,10 +114,9 @@ describe("Group test", () => {
|
||||||
|
|
||||||
it("Delete group from item bar", () => {
|
it("Delete group from item bar", () => {
|
||||||
groupPage
|
groupPage
|
||||||
.searchGroup(groupNames[0])
|
.searchGroup(groupNames[0], true)
|
||||||
.deleteGroupItem(groupNames[0])
|
.deleteGroupItem(groupNames[0])
|
||||||
.assertNotificationGroupDeleted()
|
.assertNotificationGroupDeleted()
|
||||||
.searchGroup(groupNames[0])
|
|
||||||
.assertNoSearchResultsMessageExist(true);
|
.assertNoSearchResultsMessageExist(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -162,9 +160,7 @@ describe("Group test", () => {
|
||||||
groupPage
|
groupPage
|
||||||
.goToGroupChildGroupsTab(predefinedGroups[0])
|
.goToGroupChildGroupsTab(predefinedGroups[0])
|
||||||
.searchGroup(predefinedGroups[1])
|
.searchGroup(predefinedGroups[1])
|
||||||
.goToGroupChildGroupsTab(predefinedGroups[1])
|
.assertGroupItemExist(predefinedGroups[1], true);
|
||||||
.searchGroup(predefinedGroups[2])
|
|
||||||
.assertGroupItemExist(predefinedGroups[2], true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Search non existing child group in group", () => {
|
it("Search non existing child group in group", () => {
|
||||||
|
@ -194,30 +190,7 @@ describe("Group test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("Search globally", () => {
|
describe("Search globally", () => {
|
||||||
it("Check empty state", () => {
|
|
||||||
groupPage
|
|
||||||
.headerActionsearchGroup()
|
|
||||||
.assertNoSearchResultsMessageExist(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Search with multiple words", () => {
|
|
||||||
groupPage.headerActionsearchGroup();
|
|
||||||
searchGroupPage
|
|
||||||
.searchGroup(
|
|
||||||
predefinedGroups[0].substr(0, predefinedGroups[0].length / 2)
|
|
||||||
)
|
|
||||||
.assertGroupItemExist(predefinedGroups[0], true)
|
|
||||||
.searchGroup(
|
|
||||||
predefinedGroups[0].substr(
|
|
||||||
predefinedGroups[0].length / 2,
|
|
||||||
predefinedGroups[0].length
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.assertGroupItemExist(predefinedGroups[0], true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("Navigate to parent group details", () => {
|
it("Navigate to parent group details", () => {
|
||||||
groupPage.headerActionsearchGroup();
|
|
||||||
searchGroupPage
|
searchGroupPage
|
||||||
.searchGroup(predefinedGroups[0])
|
.searchGroup(predefinedGroups[0])
|
||||||
.goToGroupChildGroupsTab(predefinedGroups[0])
|
.goToGroupChildGroupsTab(predefinedGroups[0])
|
||||||
|
@ -225,7 +198,6 @@ describe("Group test", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it("Navigate to sub-group details", () => {
|
it("Navigate to sub-group details", () => {
|
||||||
groupPage.headerActionsearchGroup();
|
|
||||||
searchGroupPage
|
searchGroupPage
|
||||||
.searchGroup(predefinedGroups[1])
|
.searchGroup(predefinedGroups[1])
|
||||||
.goToGroupChildGroupsTab(predefinedGroups[1])
|
.goToGroupChildGroupsTab(predefinedGroups[1])
|
||||||
|
|
|
@ -36,17 +36,8 @@ export default class GroupPage extends PageObject {
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
public searchGroup(groupName: string) {
|
public searchGroup(groupName: string, wait: boolean = false) {
|
||||||
listingPage.searchItem(groupName, false);
|
listingPage.searchItem(groupName, wait);
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public headerActionsearchGroup() {
|
|
||||||
super.openDropdownMenu("", cy.findByTestId(this.actionDrpDwnButton));
|
|
||||||
super.clickDropdownMenuItem(
|
|
||||||
"",
|
|
||||||
cy.findByTestId(this.actionDrpDwnItemSearchGroup)
|
|
||||||
);
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,7 +2,7 @@ import GroupPage from "./GroupPage";
|
||||||
|
|
||||||
export class SearchGroupPage extends GroupPage {
|
export class SearchGroupPage extends GroupPage {
|
||||||
private searchField = "group-search";
|
private searchField = "group-search";
|
||||||
private searchButton = "search-button";
|
private searchButton = "[data-testid='group-search'] > button";
|
||||||
|
|
||||||
public searchGroup(groupName: string) {
|
public searchGroup(groupName: string) {
|
||||||
this.typeSearchInput(groupName);
|
this.typeSearchInput(groupName);
|
||||||
|
@ -16,7 +16,7 @@ export class SearchGroupPage extends GroupPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
public clickSearchButton() {
|
public clickSearchButton() {
|
||||||
cy.findByTestId(this.searchButton).click();
|
cy.get(this.searchButton).click();
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -5,6 +5,9 @@
|
||||||
"createGroup": "Create group",
|
"createGroup": "Create group",
|
||||||
"groupName": "Group name",
|
"groupName": "Group name",
|
||||||
"searchForGroups": "Search for groups",
|
"searchForGroups": "Search for groups",
|
||||||
|
"searchFor": "Search for:",
|
||||||
|
"global": "Global",
|
||||||
|
"local": "Local",
|
||||||
"searchGroups": "Search groups",
|
"searchGroups": "Search groups",
|
||||||
"searchGroup": "Search group",
|
"searchGroup": "Search group",
|
||||||
"renameGroup": "Rename group",
|
"renameGroup": "Rename group",
|
||||||
|
|
|
@ -2,7 +2,13 @@ import { useState } from "react";
|
||||||
import { Link } from "react-router-dom-v5-compat";
|
import { Link } from "react-router-dom-v5-compat";
|
||||||
import { useLocation, useNavigate } from "react-router-dom-v5-compat";
|
import { useLocation, useNavigate } from "react-router-dom-v5-compat";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { cellWidth } from "@patternfly/react-table";
|
import {
|
||||||
|
Radio,
|
||||||
|
SearchInput,
|
||||||
|
Split,
|
||||||
|
SplitItem,
|
||||||
|
ToolbarItem,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
|
|
||||||
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
import { useAdminClient } from "../context/auth/AdminClient";
|
||||||
|
@ -19,11 +25,30 @@ import useToggle from "../utils/useToggle";
|
||||||
import { DeleteGroup } from "./components/DeleteGroup";
|
import { DeleteGroup } from "./components/DeleteGroup";
|
||||||
import { GroupToolbar, ViewType } from "./components/GroupToolbar";
|
import { GroupToolbar, ViewType } from "./components/GroupToolbar";
|
||||||
import { MoveDialog } from "./components/MoveDialog";
|
import { MoveDialog } from "./components/MoveDialog";
|
||||||
|
import { GroupPath } from "../components/group/GroupPath";
|
||||||
|
|
||||||
type GroupTableProps = {
|
type GroupTableProps = {
|
||||||
toggleView?: (viewType: ViewType) => void;
|
toggleView?: (viewType: ViewType) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SearchType = "global" | "local";
|
||||||
|
|
||||||
|
type SearchGroup = GroupRepresentation & {
|
||||||
|
link?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const flatten = (groups: GroupRepresentation[], id?: string): SearchGroup[] => {
|
||||||
|
let result: SearchGroup[] = [];
|
||||||
|
for (const group of groups) {
|
||||||
|
const link = `${id || ""}${id ? "/" : ""}${group.id}`;
|
||||||
|
result.push({ ...group, link });
|
||||||
|
if (group.subGroups) {
|
||||||
|
result = [...result, ...flatten(group.subGroups, link)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
const { t } = useTranslation("groups");
|
const { t } = useTranslation("groups");
|
||||||
|
|
||||||
|
@ -37,11 +62,15 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
const { subGroups, currentGroup, setSubGroups } = useSubGroups();
|
const { subGroups, currentGroup, setSubGroups } = useSubGroups();
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
const refresh = () => setKey(new Date().getTime());
|
const refresh = () => setKey(key + 1);
|
||||||
|
const [search, setSearch] = useState<string>();
|
||||||
|
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const id = getLastId(location.pathname);
|
const id = getLastId(location.pathname);
|
||||||
|
const [searchType, setSearchType] = useState<SearchType>(
|
||||||
|
id ? "local" : "global"
|
||||||
|
);
|
||||||
|
|
||||||
const { hasAccess } = useAccess();
|
const { hasAccess } = useAccess();
|
||||||
const isManager = hasAccess("manage-users") || currentGroup()?.access?.manage;
|
const isManager = hasAccess("manage-users") || currentGroup()?.access?.manage;
|
||||||
|
@ -49,7 +78,20 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
hasAccess("query-groups", "view-users") ||
|
hasAccess("query-groups", "view-users") ||
|
||||||
hasAccess("manage-users", "query-groups");
|
hasAccess("manage-users", "query-groups");
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async (
|
||||||
|
first?: number,
|
||||||
|
max?: number
|
||||||
|
): Promise<SearchGroup[]> => {
|
||||||
|
const params: Record<string, string> = {
|
||||||
|
search: search || "",
|
||||||
|
first: first?.toString() || "",
|
||||||
|
max: max?.toString() || "",
|
||||||
|
};
|
||||||
|
if (searchType === "global" && search) {
|
||||||
|
const result = await fetchAdminUI(adminClient, "admin-ui-groups", params);
|
||||||
|
return flatten(result);
|
||||||
|
}
|
||||||
|
|
||||||
let groupsData = undefined;
|
let groupsData = undefined;
|
||||||
if (id) {
|
if (id) {
|
||||||
const group = await adminClient.groups.findOne({ id });
|
const group = await adminClient.groups.findOne({ id });
|
||||||
|
@ -57,9 +99,14 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
throw new Error(t("common:notFound"));
|
throw new Error(t("common:notFound"));
|
||||||
}
|
}
|
||||||
|
|
||||||
groupsData = group.subGroups;
|
groupsData = !search
|
||||||
|
? group.subGroups
|
||||||
|
: group.subGroups?.filter((g) => g.name?.includes(search));
|
||||||
} else {
|
} else {
|
||||||
groupsData = await fetchAdminUI(adminClient, "admin-ui-groups");
|
groupsData = await fetchAdminUI(adminClient, "admin-ui-groups", {
|
||||||
|
...params,
|
||||||
|
global: "false",
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!groupsData) {
|
if (!groupsData) {
|
||||||
|
@ -92,6 +139,9 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
setIsCreateModalOpen(!isCreateModalOpen);
|
setIsCreateModalOpen(!isCreateModalOpen);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const Path = (group: SearchGroup) =>
|
||||||
|
group.link ? <GroupPath group={group} /> : undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteGroup
|
<DeleteGroup
|
||||||
|
@ -109,8 +159,23 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
canSelectAll
|
canSelectAll
|
||||||
loader={loader}
|
loader={loader}
|
||||||
ariaLabelKey="groups:groups"
|
ariaLabelKey="groups:groups"
|
||||||
searchPlaceholderKey="groups:searchForGroups"
|
isPaginated
|
||||||
|
isSearching={!!search}
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
|
<>
|
||||||
|
<ToolbarItem>
|
||||||
|
<SearchInput
|
||||||
|
data-testid="group-search"
|
||||||
|
placeholder={t("searchForGroups")}
|
||||||
|
value={search}
|
||||||
|
onChange={setSearch}
|
||||||
|
onSearch={refresh}
|
||||||
|
onClear={() => {
|
||||||
|
setSearch("");
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</ToolbarItem>
|
||||||
<GroupToolbar
|
<GroupToolbar
|
||||||
currentView={ViewType.Table}
|
currentView={ViewType.Table}
|
||||||
toggleView={toggleView}
|
toggleView={toggleView}
|
||||||
|
@ -118,6 +183,41 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
toggleDelete={toggleShowDelete}
|
toggleDelete={toggleShowDelete}
|
||||||
kebabDisabled={selectedRows!.length === 0}
|
kebabDisabled={selectedRows!.length === 0}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
subToolbar={
|
||||||
|
!!search &&
|
||||||
|
!id && (
|
||||||
|
<ToolbarItem>
|
||||||
|
<Split hasGutter>
|
||||||
|
<SplitItem>{t("searchFor")}</SplitItem>
|
||||||
|
<SplitItem>
|
||||||
|
<Radio
|
||||||
|
id="global"
|
||||||
|
isChecked={searchType === "global"}
|
||||||
|
onChange={() => {
|
||||||
|
setSearchType("global");
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
name="searchType"
|
||||||
|
label={t("global")}
|
||||||
|
/>
|
||||||
|
</SplitItem>
|
||||||
|
<SplitItem>
|
||||||
|
<Radio
|
||||||
|
id="local"
|
||||||
|
isChecked={searchType === "local"}
|
||||||
|
onChange={() => {
|
||||||
|
setSearchType("local");
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
name="searchType"
|
||||||
|
label={t("local")}
|
||||||
|
/>
|
||||||
|
</SplitItem>
|
||||||
|
</Split>
|
||||||
|
</ToolbarItem>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
actions={
|
actions={
|
||||||
!isManager
|
!isManager
|
||||||
|
@ -145,7 +245,11 @@ export const GroupTable = ({ toggleView }: GroupTableProps) => {
|
||||||
name: "name",
|
name: "name",
|
||||||
displayKey: "groups:groupName",
|
displayKey: "groups:groupName",
|
||||||
cellRenderer: GroupNameCell,
|
cellRenderer: GroupNameCell,
|
||||||
transforms: [cellWidth(90)],
|
},
|
||||||
|
{
|
||||||
|
name: "path",
|
||||||
|
displayKey: "groups:path",
|
||||||
|
cellRenderer: Path,
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
emptyState={
|
emptyState={
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Link } from "react-router-dom-v5-compat";
|
|
||||||
import { useLocation, useNavigate } from "react-router-dom-v5-compat";
|
import { useLocation, useNavigate } from "react-router-dom-v5-compat";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import {
|
import {
|
||||||
|
@ -23,7 +22,6 @@ import { Members } from "./Members";
|
||||||
import { GroupAttributes } from "./GroupAttributes";
|
import { GroupAttributes } from "./GroupAttributes";
|
||||||
import { GroupsModal } from "./GroupsModal";
|
import { GroupsModal } from "./GroupsModal";
|
||||||
import { toGroups } from "./routes/Groups";
|
import { toGroups } from "./routes/Groups";
|
||||||
import { toGroupsSearch } from "./routes/GroupsSearch";
|
|
||||||
import { GroupRoleMapping } from "./GroupRoleMapping";
|
import { GroupRoleMapping } from "./GroupRoleMapping";
|
||||||
import helpUrls from "../help-urls";
|
import helpUrls from "../help-urls";
|
||||||
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
|
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
|
||||||
|
@ -89,17 +87,6 @@ export default function GroupsSection() {
|
||||||
[id]
|
[id]
|
||||||
);
|
);
|
||||||
|
|
||||||
const SearchDropdown = (
|
|
||||||
<DropdownItem
|
|
||||||
key="searchGroup"
|
|
||||||
component={
|
|
||||||
<Link data-testid="searchGroup" to={toGroupsSearch({ realm })}>
|
|
||||||
{t("searchGroup")}
|
|
||||||
</Link>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<DeleteGroup
|
<DeleteGroup
|
||||||
|
@ -126,7 +113,6 @@ export default function GroupsSection() {
|
||||||
dropdownItems={
|
dropdownItems={
|
||||||
id && canManageGroup
|
id && canManageGroup
|
||||||
? [
|
? [
|
||||||
SearchDropdown,
|
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
data-testid="renameGroupAction"
|
data-testid="renameGroupAction"
|
||||||
key="renameGroup"
|
key="renameGroup"
|
||||||
|
@ -142,7 +128,7 @@ export default function GroupsSection() {
|
||||||
{t("deleteGroup")}
|
{t("deleteGroup")}
|
||||||
</DropdownItem>,
|
</DropdownItem>,
|
||||||
]
|
]
|
||||||
: [SearchDropdown]
|
: undefined
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
<PageSection variant={PageSectionVariants.light} className="pf-u-p-0">
|
<PageSection variant={PageSectionVariants.light} className="pf-u-p-0">
|
||||||
|
|
|
@ -1,183 +0,0 @@
|
||||||
import { useState } from "react";
|
|
||||||
import { Link } from "react-router-dom-v5-compat";
|
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
ButtonVariant,
|
|
||||||
Chip,
|
|
||||||
ChipGroup,
|
|
||||||
InputGroup,
|
|
||||||
PageSection,
|
|
||||||
PageSectionVariants,
|
|
||||||
Stack,
|
|
||||||
StackItem,
|
|
||||||
TextInput,
|
|
||||||
ToolbarItem,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import { SearchIcon } from "@patternfly/react-icons";
|
|
||||||
|
|
||||||
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
|
|
||||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
|
||||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
|
||||||
import { GroupPath } from "../components/group/GroupPath";
|
|
||||||
import { ViewHeader } from "../components/view-header/ViewHeader";
|
|
||||||
import { useAccess } from "../context/access/Access";
|
|
||||||
|
|
||||||
type SearchGroup = GroupRepresentation & {
|
|
||||||
link?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function SearchGroups() {
|
|
||||||
const { t } = useTranslation("groups");
|
|
||||||
const { adminClient } = useAdminClient();
|
|
||||||
const { realm } = useRealm();
|
|
||||||
|
|
||||||
const [searchTerm, setSearchTerm] = useState("");
|
|
||||||
const [searchTerms, setSearchTerms] = useState<string[]>([]);
|
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
|
||||||
const refresh = () => setKey(new Date().getTime());
|
|
||||||
|
|
||||||
const { hasAccess } = useAccess();
|
|
||||||
const isManager = hasAccess("manage-users", "query-clients");
|
|
||||||
|
|
||||||
const deleteTerm = (id: string) => {
|
|
||||||
const index = searchTerms.indexOf(id);
|
|
||||||
searchTerms.splice(index, 1);
|
|
||||||
setSearchTerms([...searchTerms]);
|
|
||||||
refresh();
|
|
||||||
};
|
|
||||||
|
|
||||||
const addTerm = () => {
|
|
||||||
if (searchTerm !== "") {
|
|
||||||
setSearchTerms([...searchTerms, searchTerm]);
|
|
||||||
setSearchTerm("");
|
|
||||||
refresh();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const GroupNameCell = (group: SearchGroup) => {
|
|
||||||
if (!isManager) return <span>{group.name}</span>;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Link key={group.id} to={`/${realm}/groups/search/${group.link}`}>
|
|
||||||
{group.name}
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const flatten = (
|
|
||||||
groups: GroupRepresentation[],
|
|
||||||
id?: string
|
|
||||||
): SearchGroup[] => {
|
|
||||||
let result: SearchGroup[] = [];
|
|
||||||
for (const group of groups) {
|
|
||||||
const link = `${id || ""}${id ? "/" : ""}${group.id}`;
|
|
||||||
result.push({ ...group, link });
|
|
||||||
if (group.subGroups) {
|
|
||||||
result = [...result, ...flatten(group.subGroups, link)];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const loader = async (first?: number, max?: number) => {
|
|
||||||
const params = {
|
|
||||||
first: first!,
|
|
||||||
max: max!,
|
|
||||||
};
|
|
||||||
|
|
||||||
let result: SearchGroup[] = [];
|
|
||||||
if (searchTerms[0]) {
|
|
||||||
result = await adminClient.groups.find({
|
|
||||||
...params,
|
|
||||||
search: searchTerms[0],
|
|
||||||
});
|
|
||||||
result = flatten(result);
|
|
||||||
for (const searchTerm of searchTerms) {
|
|
||||||
result = result.filter((group) => group.name?.includes(searchTerm));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
};
|
|
||||||
|
|
||||||
const Path = (group: GroupRepresentation) => <GroupPath group={group} />;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<ViewHeader titleKey="groups:searchGroups" />
|
|
||||||
<PageSection variant={PageSectionVariants.light} className="pf-u-p-0">
|
|
||||||
<KeycloakDataTable
|
|
||||||
key={key}
|
|
||||||
isSearching
|
|
||||||
toolbarItem={
|
|
||||||
<ToolbarItem>
|
|
||||||
<Stack>
|
|
||||||
<StackItem className="pf-u-mb-sm">
|
|
||||||
<InputGroup>
|
|
||||||
<TextInput
|
|
||||||
name="search"
|
|
||||||
data-testid="group-search"
|
|
||||||
type="search"
|
|
||||||
aria-label={t("search")}
|
|
||||||
placeholder={t("searchGroups")}
|
|
||||||
value={searchTerm}
|
|
||||||
onChange={(value) => setSearchTerm(value)}
|
|
||||||
onKeyDown={(event) => {
|
|
||||||
if (event.key === "Enter") {
|
|
||||||
addTerm();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
data-testid="search-button"
|
|
||||||
variant={ButtonVariant.control}
|
|
||||||
aria-label={t("search")}
|
|
||||||
onClick={addTerm}
|
|
||||||
>
|
|
||||||
<SearchIcon />
|
|
||||||
</Button>
|
|
||||||
</InputGroup>
|
|
||||||
</StackItem>
|
|
||||||
<StackItem>
|
|
||||||
<ChipGroup>
|
|
||||||
{searchTerms.map((term) => (
|
|
||||||
<Chip key={term} onClick={() => deleteTerm(term)}>
|
|
||||||
{term}
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
</StackItem>
|
|
||||||
</Stack>
|
|
||||||
</ToolbarItem>
|
|
||||||
}
|
|
||||||
ariaLabelKey="groups:groups"
|
|
||||||
isPaginated
|
|
||||||
loader={loader}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
name: "name",
|
|
||||||
displayKey: "groups:groupName",
|
|
||||||
cellRenderer: GroupNameCell,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "path",
|
|
||||||
displayKey: "groups:path",
|
|
||||||
cellRenderer: Path,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
emptyState={
|
|
||||||
<ListEmptyState
|
|
||||||
message={t("noSearchResults")}
|
|
||||||
instructions={t("noSearchResultsInstructions")}
|
|
||||||
hasIcon={false}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</PageSection>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,7 +1,6 @@
|
||||||
import type { RouteDef } from "../route-config";
|
import type { RouteDef } from "../route-config";
|
||||||
import { GroupsRoute, GroupsWithIdRoute } from "./routes/Groups";
|
import { GroupsRoute, GroupsWithIdRoute } from "./routes/Groups";
|
||||||
import { GroupsSearchRoute } from "./routes/GroupsSearch";
|
|
||||||
|
|
||||||
const routes: RouteDef[] = [GroupsSearchRoute, GroupsRoute, GroupsWithIdRoute];
|
const routes: RouteDef[] = [GroupsRoute, GroupsWithIdRoute];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|
|
@ -1,17 +0,0 @@
|
||||||
import { lazy } from "react";
|
|
||||||
import type { Path } from "react-router-dom-v5-compat";
|
|
||||||
import { generatePath } from "react-router-dom-v5-compat";
|
|
||||||
import type { RouteDef } from "../../route-config";
|
|
||||||
|
|
||||||
export type GroupsSearchParams = { realm: string };
|
|
||||||
|
|
||||||
export const GroupsSearchRoute: RouteDef = {
|
|
||||||
path: "/:realm/groups/search",
|
|
||||||
component: lazy(() => import("../SearchGroups")),
|
|
||||||
breadcrumb: (t) => t("groups:searchGroups"),
|
|
||||||
access: "query-groups",
|
|
||||||
};
|
|
||||||
|
|
||||||
export const toGroupsSearch = (params: GroupsSearchParams): Partial<Path> => ({
|
|
||||||
pathname: generatePath(GroupsSearchRoute.path, params),
|
|
||||||
});
|
|
|
@ -53,11 +53,15 @@ public class GroupsResource {
|
||||||
)}
|
)}
|
||||||
)
|
)
|
||||||
public final Stream<GroupRepresentation> listGroups(@QueryParam("search") @DefaultValue("") final String search, @QueryParam("first")
|
public final Stream<GroupRepresentation> listGroups(@QueryParam("search") @DefaultValue("") final String search, @QueryParam("first")
|
||||||
@DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max) {
|
@DefaultValue("0") int first, @QueryParam("max") @DefaultValue("10") int max, @QueryParam("global") @DefaultValue("true") boolean global) {
|
||||||
this.auth.groups().requireList();
|
this.auth.groups().requireList();
|
||||||
final Stream<GroupModel> stream;
|
final Stream<GroupModel> stream;
|
||||||
if ("".equals(search)) {
|
if (!"".equals(search)) {
|
||||||
|
if (global) {
|
||||||
stream = this.realm.searchForGroupByNameStream(search, first, max);
|
stream = this.realm.searchForGroupByNameStream(search, first, max);
|
||||||
|
} else {
|
||||||
|
stream = this.realm.getTopLevelGroupsStream().filter(g -> g.getName().contains(search)).skip(first).limit(max);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
stream = this.realm.getTopLevelGroupsStream(first, max);
|
stream = this.realm.getTopLevelGroupsStream(first, max);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue