Added rename group and adding members to a group (#448)
* users can now rename groups * add members to a group * added cypress test * remove rename and delete when no group is selected * added test * keep selected rows form other pages * fixed empty first page and cancel button
This commit is contained in:
parent
236e89dc63
commit
50920b3df2
15 changed files with 531 additions and 190 deletions
|
@ -83,8 +83,7 @@ describe("Clients test", function () {
|
|||
|
||||
it("Initial access token", () => {
|
||||
const initialAccessTokenTab = new InitialAccessTokenTab();
|
||||
listingPage.goToInitialAccessTokenTab();
|
||||
initialAccessTokenTab.shouldBeEmpty();
|
||||
initialAccessTokenTab.goToInitialAccessTokenTab().shouldBeEmpty();
|
||||
initialAccessTokenTab.createNewToken(1, 1).save();
|
||||
|
||||
modalUtils.checkModalTitle("Initial access token details").closeModal();
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import ListingPage from "../support/pages/admin_console/ListingPage";
|
||||
import CreateGroupModal from "../support/pages/admin_console/manage/groups/CreateGroupModal";
|
||||
import GroupModal from "../support/pages/admin_console/manage/groups/GroupModal";
|
||||
import GroupDetailPage from "../support/pages/admin_console/manage/groups/GroupDetailPage";
|
||||
import { SearchGroupPage } from "../support/pages/admin_console/manage/groups/SearchGroup";
|
||||
import Masthead from "../support/pages/admin_console/Masthead";
|
||||
|
@ -15,7 +15,7 @@ describe("Group test", () => {
|
|||
const sidebarPage = new SidebarPage();
|
||||
const listingPage = new ListingPage();
|
||||
const viewHeaderPage = new ViewHeaderPage();
|
||||
const createGroupModal = new CreateGroupModal();
|
||||
const groupModal = new GroupModal();
|
||||
|
||||
let groupName = "group";
|
||||
|
||||
|
@ -29,7 +29,7 @@ describe("Group test", () => {
|
|||
it("Group CRUD test", () => {
|
||||
groupName += "_" + (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
createGroupModal
|
||||
groupModal
|
||||
.open("empty-primary-action")
|
||||
.fillGroupForm(groupName)
|
||||
.clickCreate();
|
||||
|
@ -44,6 +44,23 @@ describe("Group test", () => {
|
|||
masthead.checkNotificationMessage("Group deleted");
|
||||
});
|
||||
|
||||
it("Should rename group", () => {
|
||||
groupModal
|
||||
.open("empty-primary-action")
|
||||
.fillGroupForm(groupName)
|
||||
.clickCreate();
|
||||
listingPage.goToItemDetails(groupName);
|
||||
viewHeaderPage.clickAction("renameGroupAction");
|
||||
|
||||
const newName = "Renamed group";
|
||||
groupModal.fillGroupForm(newName).clickRename();
|
||||
masthead.checkNotificationMessage("Group updated");
|
||||
|
||||
sidebarPage.goToGroups();
|
||||
listingPage.searchItem(newName, false).itemExist(newName);
|
||||
listingPage.deleteItem(newName);
|
||||
});
|
||||
|
||||
const searchGroupPage = new SearchGroupPage();
|
||||
it("Group search", () => {
|
||||
viewHeaderPage.clickAction("searchGroup");
|
||||
|
@ -63,6 +80,7 @@ describe("Group test", () => {
|
|||
const username = "user" + i;
|
||||
client.createUserInGroup(username, createdGroups[i % 3].id);
|
||||
}
|
||||
client.createUser({ username: "new", enabled: true });
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
|
@ -80,7 +98,7 @@ describe("Group test", () => {
|
|||
detailPage.checkListSubGroup([groups[1]]);
|
||||
|
||||
const added = "addedGroup";
|
||||
createGroupModal.open().fillGroupForm(added).clickCreate();
|
||||
groupModal.open().fillGroupForm(added).clickCreate();
|
||||
|
||||
detailPage.checkListSubGroup([added, groups[1]]);
|
||||
});
|
||||
|
@ -93,6 +111,18 @@ describe("Group test", () => {
|
|||
.checkListMembers(["user0", "user3", "user1", "user4", "user2"]);
|
||||
});
|
||||
|
||||
it("Should add members", () => {
|
||||
listingPage.goToItemDetails(groups[0]);
|
||||
detailPage
|
||||
.clickMembersTab()
|
||||
.clickAddMembers()
|
||||
.checkSelectableMembers(["user1", "user4"]);
|
||||
detailPage.selectUsers(["new"]).clickAdd();
|
||||
|
||||
masthead.checkNotificationMessage("1 user added to the group");
|
||||
detailPage.checkListMembers(["new", "user0", "user3"]);
|
||||
});
|
||||
|
||||
it("Attributes CRUD test", () => {
|
||||
listingPage.goToItemDetails(groups[0]);
|
||||
detailPage
|
||||
|
|
|
@ -7,7 +7,6 @@ export default class ListingPage {
|
|||
searchBtn: string;
|
||||
createBtn: string;
|
||||
importBtn: string;
|
||||
initialAccessTokenTab = "initialAccessToken";
|
||||
|
||||
constructor() {
|
||||
this.searchInput = '.pf-c-toolbar__item [type="search"]';
|
||||
|
@ -35,11 +34,6 @@ export default class ListingPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
goToInitialAccessTokenTab() {
|
||||
cy.getId(this.initialAccessTokenTab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
searchItem(searchValue: string, wait = true) {
|
||||
if (wait) {
|
||||
const searchUrl = `/auth/admin/realms/master/*${searchValue}*`;
|
||||
|
|
|
@ -1,10 +1,17 @@
|
|||
export default class InitialAccessTokenTab {
|
||||
private initialAccessTokenTab = "initialAccessToken";
|
||||
|
||||
private emptyAction = "empty-primary-action";
|
||||
|
||||
private expirationInput = "expiration";
|
||||
private countInput = "count";
|
||||
private saveBtn = "save";
|
||||
|
||||
goToInitialAccessTokenTab() {
|
||||
cy.getId(this.initialAccessTokenTab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
shouldBeEmpty() {
|
||||
cy.getId(this.emptyAction).should("exist");
|
||||
return this;
|
||||
|
|
|
@ -6,6 +6,9 @@ export default class GroupDetailPage {
|
|||
private attributesTab = "attributes";
|
||||
private memberNameColumn = 'tbody > tr > [data-label="Name"]';
|
||||
private includeSubGroupsCheck = "includeSubGroupsCheck";
|
||||
private addMembers = "addMember";
|
||||
private addMember = "add";
|
||||
private memberUsernameColumn = 'tbody > tr > [data-label="Username"]';
|
||||
|
||||
private keyInput = '[name="attributes[0].key"]';
|
||||
private valueInput = '[name="attributes[0].value"]';
|
||||
|
@ -37,6 +40,31 @@ export default class GroupDetailPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
checkSelectableMembers(members: string[]) {
|
||||
cy.get(this.memberUsernameColumn).should((member) => {
|
||||
for (const user of members) {
|
||||
expect(member).to.contain(user);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
selectUsers(users: string[]) {
|
||||
for (const user of users) {
|
||||
cy.get(this.memberUsernameColumn)
|
||||
.contains(user)
|
||||
.parent()
|
||||
.find("input")
|
||||
.click();
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
clickAdd() {
|
||||
cy.getId(this.addMember).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickIncludeSubGroups() {
|
||||
cy.getId(this.includeSubGroupsCheck).click();
|
||||
return this;
|
||||
|
@ -47,6 +75,11 @@ export default class GroupDetailPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
clickAddMembers() {
|
||||
cy.getId(this.addMembers).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
fillAttribute(key: string, value: string) {
|
||||
cy.get(this.keyInput).type(key).get(this.valueInput).type(value);
|
||||
return this;
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
export default class CreateGroupModal {
|
||||
export default class GroupModal {
|
||||
private openButton = "openCreateGroupModal";
|
||||
private nameInput = "groupNameInput";
|
||||
private createButton = "createGroup";
|
||||
private renameButton = "renameGroup";
|
||||
|
||||
open(name?: string) {
|
||||
cy.getId(name || this.openButton).click();
|
||||
|
@ -18,4 +19,9 @@ export default class CreateGroupModal {
|
|||
cy.getId(this.createButton).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
clickRename() {
|
||||
cy.getId(this.renameButton).click();
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
import KeycloakAdminClient from "keycloak-admin";
|
||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
|
||||
export default class AdminClient {
|
||||
private client: KeycloakAdminClient;
|
||||
|
@ -57,9 +58,14 @@ export default class AdminClient {
|
|||
}
|
||||
}
|
||||
|
||||
async createUser(user: UserRepresentation) {
|
||||
await this.login();
|
||||
return await this.client.users.create(user);
|
||||
}
|
||||
|
||||
async createUserInGroup(username: string, groupId: string) {
|
||||
await this.login();
|
||||
const user = await this.client.users.create({ username, enabled: true });
|
||||
const user = await this.createUser({ username, enabled: true });
|
||||
await this.client.users.addToGroup({ id: user.id!, groupId });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -133,6 +133,7 @@ export function KeycloakDataTable<T>({
|
|||
emptyState,
|
||||
}: DataListProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState<T[]>([]);
|
||||
const [rows, setRows] = useState<Row<T>[]>();
|
||||
const [filteredData, setFilteredData] = useState<Row<T>[]>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
@ -154,7 +155,9 @@ export function KeycloakDataTable<T>({
|
|||
const result = data!.map((value) => {
|
||||
return {
|
||||
data: value,
|
||||
selected: false,
|
||||
selected: !!selected.find(
|
||||
(v) => (v as any).id === (value as any).id
|
||||
),
|
||||
cells: columns.map((col) => {
|
||||
if (col.cellRenderer) {
|
||||
return col.cellRenderer(value);
|
||||
|
@ -233,7 +236,17 @@ export function KeycloakDataTable<T>({
|
|||
rows![rowIndex].selected = isSelected;
|
||||
setRows([...rows!]);
|
||||
}
|
||||
onSelect!(rows!.filter((row) => row.selected).map((row) => row.data));
|
||||
const difference = _.differenceBy(
|
||||
selected,
|
||||
rows!.map((row) => row.data),
|
||||
"id"
|
||||
);
|
||||
const selectedRows = [
|
||||
...difference,
|
||||
...rows!.filter((row) => row.selected).map((row) => row.data),
|
||||
];
|
||||
setSelected(selectedRows);
|
||||
onSelect!(selectedRows);
|
||||
};
|
||||
|
||||
return (
|
||||
|
|
|
@ -19,7 +19,7 @@ import { useAlerts } from "../components/alert/Alerts";
|
|||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { GroupsCreateModal } from "./GroupsCreateModal";
|
||||
import { GroupsModal } from "./GroupsModal";
|
||||
import { getLastId } from "./groupIdUtils";
|
||||
|
||||
type GroupTableData = GroupRepresentation & {
|
||||
|
@ -195,7 +195,7 @@ export const GroupTable = () => {
|
|||
}
|
||||
/>
|
||||
{isCreateModalOpen && (
|
||||
<GroupsCreateModal
|
||||
<GroupsModal
|
||||
id={id}
|
||||
handleModalToggle={handleModalToggle}
|
||||
refresh={refresh}
|
||||
|
|
|
@ -1,97 +0,0 @@
|
|||
import React from "react";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
TextInput,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
|
||||
type GroupsCreateModalProps = {
|
||||
id?: string;
|
||||
handleModalToggle: () => void;
|
||||
refresh: () => void;
|
||||
};
|
||||
|
||||
export const GroupsCreateModal = ({
|
||||
id,
|
||||
handleModalToggle,
|
||||
refresh,
|
||||
}: GroupsCreateModalProps) => {
|
||||
const { t } = useTranslation("groups");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
const { register, errors, handleSubmit } = useForm();
|
||||
|
||||
const submitForm = async (group: GroupRepresentation) => {
|
||||
try {
|
||||
if (!id) {
|
||||
await adminClient.groups.create({ name: group.name });
|
||||
} else {
|
||||
await adminClient.groups.setOrCreateChild({ id }, { name: group.name });
|
||||
}
|
||||
|
||||
refresh();
|
||||
handleModalToggle();
|
||||
addAlert(t("groupCreated"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(t("couldNotCreateGroup", { error }), AlertVariant.danger);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t("createAGroup")}
|
||||
isOpen={true}
|
||||
onClose={handleModalToggle}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="createGroup"
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
form="group-form"
|
||||
>
|
||||
{t("create")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form id="group-form" isHorizontal onSubmit={handleSubmit(submitForm)}>
|
||||
<FormGroup
|
||||
name="create-modal-group"
|
||||
label={t("common:name")}
|
||||
fieldId="group-id"
|
||||
helperTextInvalid={t("common:required")}
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<TextInput
|
||||
data-testid="groupNameInput"
|
||||
ref={register({ required: true })}
|
||||
autoFocus
|
||||
type="text"
|
||||
id="create-group-name"
|
||||
name="name"
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
};
|
104
src/groups/GroupsModal.tsx
Normal file
104
src/groups/GroupsModal.tsx
Normal file
|
@ -0,0 +1,104 @@
|
|||
import React from "react";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
Form,
|
||||
FormGroup,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
TextInput,
|
||||
ValidatedOptions,
|
||||
} from "@patternfly/react-core";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
|
||||
type GroupsModalProps = {
|
||||
id?: string;
|
||||
rename?: string;
|
||||
handleModalToggle: () => void;
|
||||
refresh: (group?: GroupRepresentation) => void;
|
||||
};
|
||||
|
||||
export const GroupsModal = ({
|
||||
id,
|
||||
rename,
|
||||
handleModalToggle,
|
||||
refresh,
|
||||
}: GroupsModalProps) => {
|
||||
const { t } = useTranslation("groups");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
const { register, errors, handleSubmit } = useForm({
|
||||
defaultValues: { name: rename },
|
||||
});
|
||||
|
||||
const submitForm = async (group: GroupRepresentation) => {
|
||||
try {
|
||||
if (!id) {
|
||||
await adminClient.groups.create(group);
|
||||
} else if (rename) {
|
||||
await adminClient.groups.update({ id }, group);
|
||||
} else {
|
||||
await adminClient.groups.setOrCreateChild({ id }, group);
|
||||
}
|
||||
|
||||
refresh(rename ? group : undefined);
|
||||
handleModalToggle();
|
||||
addAlert(
|
||||
t(rename ? "groupUpdated" : "groupCreated"),
|
||||
AlertVariant.success
|
||||
);
|
||||
} catch (error) {
|
||||
addAlert(t("couldNotCreateGroup", { error }), AlertVariant.danger);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.small}
|
||||
title={t(rename ? "renameAGroup" : "createAGroup")}
|
||||
isOpen={true}
|
||||
onClose={handleModalToggle}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid={`${rename ? "rename" : "create"}Group`}
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
form="group-form"
|
||||
>
|
||||
{t(rename ? "rename" : "create")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form id="group-form" isHorizontal onSubmit={handleSubmit(submitForm)}>
|
||||
<FormGroup
|
||||
name="create-modal-group"
|
||||
label={t("common:name")}
|
||||
fieldId="group-id"
|
||||
helperTextInvalid={t("common:required")}
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
isRequired
|
||||
>
|
||||
<TextInput
|
||||
data-testid="groupNameInput"
|
||||
ref={register({ required: true })}
|
||||
autoFocus
|
||||
type="text"
|
||||
id="create-group-name"
|
||||
name="name"
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -23,6 +23,7 @@ import { GroupTable } from "./GroupTable";
|
|||
import { getId, getLastId } from "./groupIdUtils";
|
||||
import { Members } from "./Members";
|
||||
import { GroupAttributes } from "./GroupAttributes";
|
||||
import { GroupsModal } from "./GroupsModal";
|
||||
|
||||
import "./GroupsSection.css";
|
||||
|
||||
|
@ -31,11 +32,13 @@ export const GroupsSection = () => {
|
|||
const [activeTab, setActiveTab] = useState(0);
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const { subGroups, setSubGroups } = useSubGroups();
|
||||
const { subGroups, setSubGroups, currentGroup } = useSubGroups();
|
||||
const { addAlert } = useAlerts();
|
||||
const { realm } = useRealm();
|
||||
const errorHandler = useErrorHandler();
|
||||
|
||||
const [rename, setRename] = useState<string>();
|
||||
|
||||
const history = useHistory();
|
||||
const location = useLocation();
|
||||
const id = getLastId(location.pathname);
|
||||
|
@ -85,34 +88,60 @@ export const GroupsSection = () => {
|
|||
[id]
|
||||
);
|
||||
|
||||
const SearchDropdown = (
|
||||
<DropdownItem
|
||||
data-testid="searchGroup"
|
||||
key="searchGroup"
|
||||
onClick={() => history.push(`/${realm}/groups/search`)}
|
||||
>
|
||||
{t("searchGroup")}
|
||||
</DropdownItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{rename && (
|
||||
<GroupsModal
|
||||
id={id}
|
||||
rename={rename}
|
||||
refresh={(group) =>
|
||||
setSubGroups([...subGroups.slice(0, subGroups.length - 1), group!])
|
||||
}
|
||||
handleModalToggle={() => setRename(undefined)}
|
||||
/>
|
||||
)}
|
||||
<ViewHeader
|
||||
titleKey="groups:groups"
|
||||
subKey="groups:groupsDescription"
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
data-testid="searchGroup"
|
||||
key="searchGroup"
|
||||
onClick={() => history.push(`/${realm}/groups/search`)}
|
||||
>
|
||||
{t("searchGroup")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
data-testid="renameGroup"
|
||||
key="renameGroup"
|
||||
onClick={() => addAlert("Not implemented")}
|
||||
>
|
||||
{t("renameGroup")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
data-testid="deleteGroup"
|
||||
key="deleteGroup"
|
||||
onClick={() => deleteGroup({ id })}
|
||||
>
|
||||
{t("deleteGroup")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
dropdownItems={
|
||||
id
|
||||
? [
|
||||
SearchDropdown,
|
||||
<DropdownItem
|
||||
data-testid="renameGroupAction"
|
||||
key="renameGroup"
|
||||
onClick={() => setRename(currentGroup().name)}
|
||||
>
|
||||
{t("renameGroup")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
data-testid="deleteGroup"
|
||||
key="deleteGroup"
|
||||
onClick={() => {
|
||||
deleteGroup({ id });
|
||||
history.push(
|
||||
location.pathname.substr(
|
||||
0,
|
||||
location.pathname.lastIndexOf("/")
|
||||
)
|
||||
);
|
||||
}}
|
||||
>
|
||||
{t("deleteGroup")}
|
||||
</DropdownItem>,
|
||||
]
|
||||
: [SearchDropdown]
|
||||
}
|
||||
/>
|
||||
<PageSection variant={PageSectionVariants.light}>
|
||||
{subGroups.length > 0 && (
|
||||
|
|
|
@ -2,16 +2,27 @@ import React, { useEffect, useState } from "react";
|
|||
import { useLocation } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import _ from "lodash";
|
||||
import { Button, Checkbox, ToolbarItem } from "@patternfly/react-core";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
Checkbox,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
KebabToggle,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { emptyFormatter } from "../util";
|
||||
|
||||
import { getLastId } from "./groupIdUtils";
|
||||
import { useSubGroups } from "./SubGroupsContext";
|
||||
import { MemberModal } from "./MembersModal";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
|
||||
type MembersOf = UserRepresentation & {
|
||||
membership: GroupRepresentation[];
|
||||
|
@ -20,10 +31,14 @@ type MembersOf = UserRepresentation & {
|
|||
export const Members = () => {
|
||||
const { t } = useTranslation("groups");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
const location = useLocation();
|
||||
const id = getLastId(location.pathname);
|
||||
const [includeSubGroup, setIncludeSubGroup] = useState(false);
|
||||
const { currentGroup, subGroups } = useSubGroups();
|
||||
const [addMembers, setAddMembers] = useState(false);
|
||||
const [isKebabOpen, setIsKebabOpen] = useState(false);
|
||||
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
@ -81,55 +96,118 @@ export const Members = () => {
|
|||
};
|
||||
|
||||
return (
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
ariaLabelKey="groups:members"
|
||||
isPaginated
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button data-testid="addMember" variant="primary">
|
||||
{t("addMember")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
data-testid="includeSubGroupsCheck"
|
||||
label={t("includeSubGroups")}
|
||||
id="kc-include-sub-groups"
|
||||
isChecked={includeSubGroup}
|
||||
onChange={() => setIncludeSubGroup(!includeSubGroup)}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "username",
|
||||
displayKey: "common:name",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
displayKey: "groups:email",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "firstName",
|
||||
displayKey: "groups:firstName",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "lastName",
|
||||
displayKey: "groups:lastName",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "membership",
|
||||
displayKey: "groups:membership",
|
||||
cellRenderer: MemberOfRenderer,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<>
|
||||
{addMembers && (
|
||||
<MemberModal
|
||||
groupId={id!}
|
||||
onClose={() => {
|
||||
setAddMembers(false);
|
||||
refresh();
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
ariaLabelKey="groups:members"
|
||||
isPaginated
|
||||
canSelectAll
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
data-testid="addMember"
|
||||
variant="primary"
|
||||
onClick={() => setAddMembers(true)}
|
||||
>
|
||||
{t("addMember")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
data-testid="includeSubGroupsCheck"
|
||||
label={t("includeSubGroups")}
|
||||
id="kc-include-sub-groups"
|
||||
isChecked={includeSubGroup}
|
||||
onChange={() => setIncludeSubGroup(!includeSubGroup)}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Dropdown
|
||||
toggle={
|
||||
<KebabToggle onToggle={() => setIsKebabOpen(!isKebabOpen)} />
|
||||
}
|
||||
isOpen={isKebabOpen}
|
||||
isPlain
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key="action"
|
||||
component="button"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((user) =>
|
||||
adminClient.users.delFromGroup({
|
||||
id: user.id!,
|
||||
groupId: id!,
|
||||
})
|
||||
)
|
||||
);
|
||||
setIsKebabOpen(false);
|
||||
addAlert(
|
||||
t("usersLeft", { count: selectedRows.length }),
|
||||
AlertVariant.success
|
||||
);
|
||||
} catch (error) {
|
||||
addAlert(t("usersLeftError"), AlertVariant.danger);
|
||||
}
|
||||
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
{t("leave")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "username",
|
||||
displayKey: "common:name",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
displayKey: "groups:email",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "firstName",
|
||||
displayKey: "groups:firstName",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "lastName",
|
||||
displayKey: "groups:lastName",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "membership",
|
||||
displayKey: "groups:membership",
|
||||
cellRenderer: MemberOfRenderer,
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("users:noUsersFound")}
|
||||
instructions={t("users:emptyInstructions")}
|
||||
primaryActionText={t("addMember")}
|
||||
onPrimaryAction={() => setAddMembers(true)}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
130
src/groups/MembersModal.tsx
Normal file
130
src/groups/MembersModal.tsx
Normal file
|
@ -0,0 +1,130 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useRealm } from "../context/realm-context/RealmContext";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { emptyFormatter } from "../util";
|
||||
import _ from "lodash";
|
||||
|
||||
type MemberModalProps = {
|
||||
groupId: string;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
export const MemberModal = ({ groupId, onClose }: MemberModalProps) => {
|
||||
const { t } = useTranslation("groups");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert } = useAlerts();
|
||||
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
||||
|
||||
const history = useHistory();
|
||||
const { realm } = useRealm();
|
||||
const goToCreate = () => history.push(`/${realm}/users/add-user`);
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const members = await adminClient.groups.listMembers({ id: groupId });
|
||||
const params: { [name: string]: string | number } = {
|
||||
first: first!,
|
||||
max: max! + members.length,
|
||||
search: search || "",
|
||||
};
|
||||
|
||||
try {
|
||||
const users = await adminClient.users.find({ ...params });
|
||||
return _.xorBy(users, members, "id").slice(0, max);
|
||||
} catch (error) {
|
||||
addAlert(t("noUsersFoundError", { error }), AlertVariant.danger);
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.large}
|
||||
title={t("addMember")}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="add"
|
||||
key="confirm"
|
||||
variant="primary"
|
||||
onClick={async () => {
|
||||
try {
|
||||
await Promise.all(
|
||||
selectedRows.map((user) =>
|
||||
adminClient.users.addToGroup({ id: user.id!, groupId })
|
||||
)
|
||||
);
|
||||
onClose();
|
||||
addAlert(
|
||||
t("usersAdded", { count: selectedRows.length }),
|
||||
AlertVariant.success
|
||||
);
|
||||
} catch (error) {
|
||||
addAlert(t("usersAddedError"), AlertVariant.danger);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{t("common:add")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="users:title"
|
||||
searchPlaceholderKey="users:searchForUser"
|
||||
canSelectAll
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
emptyState={
|
||||
<ListEmptyState
|
||||
message={t("users:noUsersFound")}
|
||||
instructions={t("users:emptyInstructions")}
|
||||
primaryActionText={t("users:createNewUser")}
|
||||
onPrimaryAction={goToCreate}
|
||||
/>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "username",
|
||||
displayKey: "users:username",
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
displayKey: "users:email",
|
||||
},
|
||||
{
|
||||
name: "lastName",
|
||||
displayKey: "users:lastName",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "firstName",
|
||||
displayKey: "users:firstName",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -9,6 +9,13 @@
|
|||
"searchGroup": "Search group",
|
||||
"renameGroup": "Rename group",
|
||||
"deleteGroup": "Delete group",
|
||||
"leave": "Leave group",
|
||||
"usersLeft": "{{count}} user left the group",
|
||||
"usersLeft_plural": "{{count}} users left the group",
|
||||
"usersLeftError": "Could not remove users from the group",
|
||||
"usersAdded": "{{count}} user added to the group",
|
||||
"usersAdded_plural": "{{count}} users added to the group",
|
||||
"usersAddedError": "Could not add users to the group",
|
||||
"search": "Search",
|
||||
"members": "Members",
|
||||
"searchMembers": "Search members",
|
||||
|
@ -21,7 +28,9 @@
|
|||
"groupCreated": "Group created",
|
||||
"couldNotCreateGroup": "Could not create group {{error}}",
|
||||
"createAGroup": "Create a group",
|
||||
"renameAGroup": "Rename group",
|
||||
"create": "Create",
|
||||
"rename": "Rename",
|
||||
"email": "Email",
|
||||
"lastName": "Last name",
|
||||
"firstName": "First name",
|
||||
|
|
Loading…
Reference in a new issue