Merge pull request #542 from jenny-s51/updateUserForm

Users(Create user form): Update create user form with new design
This commit is contained in:
mfrances17 2021-04-21 10:17:48 -04:00 committed by GitHub
commit 4182f2bd58
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 241 additions and 48 deletions

View file

@ -16,7 +16,6 @@ describe("Group creation", () => {
const loginPage = new LoginPage(); const loginPage = new LoginPage();
const masthead = new Masthead(); const masthead = new Masthead();
const sidebarPage = new SidebarPage(); const sidebarPage = new SidebarPage();
const listingPage = new ListingPage();
const groupModal = new GroupModal(); const groupModal = new GroupModal();
beforeEach(function () { beforeEach(function () {
@ -25,33 +24,24 @@ describe("Group creation", () => {
sidebarPage.goToGroups(); sidebarPage.goToGroups();
}); });
function createNewGroup() {
groupName += "_" + (Math.random() + 1).toString(36).substring(7);
groupModal
.open("openCreateGroupModal")
.fillGroupForm(groupName)
.clickCreate();
groupsList = [...groupsList, groupName];
masthead.checkNotificationMessage("Group created");
sidebarPage.goToGroups();
}
it("Add groups to be joined", () => { it("Add groups to be joined", () => {
groupName += "_" + (Math.random() + 1).toString(36).substring(7); for (let i = 0; i <= 2; i++) {
createNewGroup();
groupModal }
.open("openCreateGroupModal")
.fillGroupForm(groupName)
.clickCreate();
groupsList = [...groupsList, groupName];
masthead.checkNotificationMessage("Group created");
sidebarPage.goToGroups();
listingPage.searchItem(groupName, false).itemExist(groupName);
groupName = "group";
groupName += "_" + (Math.random() + 1).toString(36).substring(7);
groupModal
.open("openCreateGroupModal")
.fillGroupForm(groupName)
.clickCreate();
groupsList = [...groupsList, groupName];
masthead.checkNotificationMessage("Group created");
sidebarPage.goToGroups();
listingPage.searchItem(groupName, false).itemExist(groupName);
}); });
}); });
@ -93,7 +83,21 @@ describe("Users test", () => {
createUserPage.goToCreateUser(); createUserPage.goToCreateUser();
createUserPage.createUser(itemId).save(); createUserPage.createUser(itemId);
createUserPage.toggleAddGroupModal();
const groupsListCopy = groupsList.slice(0, 1);
console.log(groupsList);
groupsListCopy.forEach((element) => {
cy.getId(`${element}-check`).click();
});
createUserPage.joinGroups();
createUserPage.save();
masthead.checkNotificationMessage("The user has been created"); masthead.checkNotificationMessage("The user has been created");
@ -126,15 +130,17 @@ describe("Users test", () => {
userGroupsPage.goToGroupsTab(); userGroupsPage.goToGroupsTab();
userGroupsPage.toggleAddGroupModal(); userGroupsPage.toggleAddGroupModal();
groupsList.forEach((element) => { cy.wait(1000);
const groupsListCopy = groupsList.slice(1, 2);
groupsListCopy.forEach((element) => {
cy.getId(`${element}-check`).click(); cy.getId(`${element}-check`).click();
}); });
userGroupsPage.joinGroup(); userGroupsPage.joinGroups();
cy.wait(1000); cy.wait(1000);
listingPage.itemExist(groupName);
}); });
it("Leave group test", function () { it("Leave group test", function () {
@ -142,7 +148,8 @@ describe("Users test", () => {
listingPage.goToItemDetails(itemId); listingPage.goToItemDetails(itemId);
// Go to user groups // Go to user groups
userGroupsPage.goToGroupsTab(); userGroupsPage.goToGroupsTab();
cy.getId(`leave-${groupName}`).click(); cy.wait(1000);
cy.contains("Leave").click();
cy.getId("modalConfirm").click(); cy.getId("modalConfirm").click();
}); });

View file

@ -4,6 +4,8 @@ export default class CreateUserPage {
emptyStateCreateUserBtn: string; emptyStateCreateUserBtn: string;
searchPgCreateUserBtn: string; searchPgCreateUserBtn: string;
addUserBtn: string; addUserBtn: string;
joinGroupsBtn: string;
joinBtn: string;
saveBtn: string; saveBtn: string;
cancelBtn: string; cancelBtn: string;
@ -14,6 +16,8 @@ export default class CreateUserPage {
this.emptyStateCreateUserBtn = "empty-primary-action"; this.emptyStateCreateUserBtn = "empty-primary-action";
this.searchPgCreateUserBtn = "create-new-user"; this.searchPgCreateUserBtn = "create-new-user";
this.addUserBtn = "add-user"; this.addUserBtn = "add-user";
this.joinGroupsBtn = "join-groups-button";
this.joinBtn = "join-button";
this.saveBtn = "create-user"; this.saveBtn = "create-user";
this.cancelBtn = "cancel-create-user"; this.cancelBtn = "cancel-create-user";
} }
@ -44,6 +48,18 @@ export default class CreateUserPage {
return this; return this;
} }
toggleAddGroupModal() {
cy.getId(this.joinGroupsBtn).click();
return this;
}
joinGroups() {
cy.getId(this.joinBtn).click();
return this;
}
save() { save() {
cy.getId(this.saveBtn).click(); cy.getId(this.saveBtn).click();

View file

@ -6,7 +6,7 @@ export default class UserGroupsPage {
constructor() { constructor() {
this.userGroupsTab = "user-groups-tab"; this.userGroupsTab = "user-groups-tab";
this.addGroupButton = "add-group-button"; this.addGroupButton = "add-group-button";
this.joinGroupButton = "joinGroup"; this.joinGroupButton = "join-button";
} }
goToGroupsTab() { goToGroupsTab() {
@ -21,7 +21,7 @@ export default class UserGroupsPage {
return this; return this;
} }
joinGroup() { joinGroups() {
cy.getId(this.joinGroupButton).click(); cy.getId(this.joinGroupButton).click();
return this; return this;

View file

@ -31,8 +31,9 @@ export type JoinGroupDialogProps = {
open: boolean; open: boolean;
toggleDialog: () => void; toggleDialog: () => void;
onClose: () => void; onClose: () => void;
username: string; username?: string;
onConfirm: (newGroups: Group[]) => void; onConfirm: (newGroups: Group[]) => void;
chips?: any;
}; };
type Group = GroupRepresentation & { type Group = GroupRepresentation & {
@ -45,6 +46,7 @@ export const JoinGroupDialog = ({
toggleDialog, toggleDialog,
onConfirm, onConfirm,
username, username,
chips,
}: JoinGroupDialogProps) => { }: JoinGroupDialogProps) => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
@ -65,17 +67,23 @@ export const JoinGroupDialog = ({
() => () =>
asyncStateFetch( asyncStateFetch(
async () => { async () => {
const existingUserGroups = await adminClient.users.listGroups({ id });
const allGroups = await adminClient.groups.find(); const allGroups = await adminClient.groups.find();
if (groupId) { if (groupId) {
const group = await adminClient.groups.findOne({ id: groupId }); const group = await adminClient.groups.findOne({ id: groupId });
return { group, groups: group.subGroups! }; return { group, groups: group.subGroups! };
} else { } else if (id) {
const existingUserGroups = await adminClient.users.listGroups({
id,
});
return { return {
groups: _.differenceBy(allGroups, existingUserGroups, "id"), groups: _.differenceBy(allGroups, existingUserGroups, "id"),
}; };
} } else
return {
groups: allGroups,
};
}, },
async ({ group: selectedGroup, groups }) => { async ({ group: selectedGroup, groups }) => {
if (selectedGroup) { if (selectedGroup) {
@ -85,7 +93,9 @@ export const JoinGroupDialog = ({
groups.forEach((group: Group) => { groups.forEach((group: Group) => {
group.checked = !!selectedRows.find((r) => r.id === group.id); group.checked = !!selectedRows.find((r) => r.id === group.id);
}); });
setGroups(groups); id
? setGroups(groups)
: setGroups([...groups.filter((row) => !chips.includes(row.name))]);
}, },
errorHandler errorHandler
), ),
@ -95,12 +105,14 @@ export const JoinGroupDialog = ({
return ( return (
<Modal <Modal
variant={ModalVariant.small} variant={ModalVariant.small}
title={`Join groups for user ${username}`} title={
username ? t("users:joinGroupsFor") + username : t("users:selectGroups")
}
isOpen={open} isOpen={open}
onClose={onClose} onClose={onClose}
actions={[ actions={[
<Button <Button
data-testid="joinGroup" data-testid="join-button"
key="confirm" key="confirm"
variant="primary" variant="primary"
form="group-form" form="group-form"

View file

@ -1,8 +1,12 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { import {
ActionGroup, ActionGroup,
AlertVariant,
Button, Button,
Chip,
ChipGroup,
FormGroup, FormGroup,
InputGroup,
Select, Select,
SelectOption, SelectOption,
Switch, Switch,
@ -18,18 +22,23 @@ import { useRealm } from "../context/realm-context/RealmContext";
import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient"; import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient";
import { useErrorHandler } from "react-error-boundary"; import { useErrorHandler } from "react-error-boundary";
import moment from "moment"; import moment from "moment";
import { JoinGroupDialog } from "./JoinGroupDialog";
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
import { useAlerts } from "../components/alert/Alerts";
export type UserFormProps = { export type UserFormProps = {
form: UseFormMethods<UserRepresentation>; form: UseFormMethods<UserRepresentation>;
save: (user: UserRepresentation) => void; save: (user: UserRepresentation) => void;
editMode: boolean; editMode: boolean;
timestamp?: number; timestamp?: number;
onGroupsUpdate: (groups: GroupRepresentation[]) => void;
}; };
export const UserForm = ({ export const UserForm = ({
form: { handleSubmit, register, errors, watch, control, setValue, reset }, form: { handleSubmit, register, errors, watch, control, setValue, reset },
save, save,
editMode, editMode,
onGroupsUpdate,
}: UserFormProps) => { }: UserFormProps) => {
const { t } = useTranslation("users"); const { t } = useTranslation("users");
const { realm } = useRealm(); const { realm } = useRealm();
@ -45,6 +54,14 @@ export const UserForm = ({
const watchUsernameInput = watch("username"); const watchUsernameInput = watch("username");
const [timestamp, setTimestamp] = useState(null); const [timestamp, setTimestamp] = useState(null);
const [chips, setChips] = useState<(string | undefined)[]>([]);
const [selectedGroups, setSelectedGroups] = useState<GroupRepresentation[]>(
[]
);
const { addAlert } = useAlerts();
const [open, setOpen] = useState(false);
useEffect(() => { useEffect(() => {
if (editMode) { if (editMode) {
@ -56,7 +73,7 @@ export const UserForm = ({
handleError handleError
); );
} }
}, []); }, [chips]);
const setupForm = (user: UserRepresentation) => { const setupForm = (user: UserRepresentation) => {
reset(); reset();
@ -90,6 +107,50 @@ export const UserForm = ({
setRequiredUserActionsDropdownOpen(false); setRequiredUserActionsDropdownOpen(false);
}; };
const deleteItem = (id: string) => {
const copyOfChips = chips;
const copyOfGroups = selectedGroups;
setChips(copyOfChips.filter((item) => item !== id));
setSelectedGroups(copyOfGroups.filter((item) => item.name !== id));
onGroupsUpdate(selectedGroups);
};
const addChips = async (groups: GroupRepresentation[]): Promise<void> => {
const newSelectedGroups = groups;
const newGroupNames: (string | undefined)[] = newSelectedGroups!.map(
(item) => item.name
);
setChips([...chips!, ...newGroupNames]);
setSelectedGroups([...selectedGroups!, ...newSelectedGroups]);
};
onGroupsUpdate(selectedGroups);
const addGroups = async (groups: GroupRepresentation[]): Promise<void> => {
const newGroups = groups;
newGroups.forEach(async (group) => {
try {
await adminClient.users.addToGroup({
id: id,
groupId: group.id!,
});
addAlert(t("users:addedGroupMembership"), AlertVariant.success);
} catch (error) {
addAlert(
t("users:addedGroupMembershipError", { error }),
AlertVariant.danger
);
}
});
};
const toggleModal = () => {
setOpen(!open);
};
return ( return (
<FormAccess <FormAccess
isHorizontal isHorizontal
@ -97,6 +158,15 @@ export const UserForm = ({
role="manage-users" role="manage-users"
className="pf-u-mt-lg" className="pf-u-mt-lg"
> >
{open && (
<JoinGroupDialog
open={open}
onClose={() => setOpen(!open)}
onConfirm={editMode ? addGroups : addChips}
toggleDialog={() => toggleModal()}
chips={chips}
/>
)}
{editMode ? ( {editMode ? (
<> <>
<FormGroup <FormGroup
@ -295,6 +365,53 @@ export const UserForm = ({
)} )}
/> />
</FormGroup> </FormGroup>
{!editMode && (
<FormGroup
label={t("common:groups")}
fieldId="kc-groups"
validated={errors.requiredActions ? "error" : "default"}
helperTextInvalid={t("common:required")}
labelIcon={
<HelpItem
helpText={t("requiredUserActionsHelpText")}
forLabel={t("requiredUserActions")}
forID="required-user-actions-label"
/>
}
>
<Controller
name="groups"
defaultValue={[]}
typeAheadAriaLabel="Select an action"
control={control}
render={() => (
<>
<InputGroup>
<ChipGroup categoryName={" "}>
{chips.map((currentChip) => (
<Chip
key={currentChip}
onClick={() => deleteItem(currentChip!)}
>
{currentChip}
</Chip>
))}
</ChipGroup>
<Button
id="kc-join-groups-button"
onClick={toggleModal}
variant="secondary"
data-testid="join-groups-button"
>
{t("users:joinGroups")}
</Button>
</InputGroup>
</>
)}
/>
</FormGroup>
)}
<ActionGroup> <ActionGroup>
<Button <Button
data-testid={!editMode ? "create-user" : "save-user"} data-testid={!editMode ? "create-user" : "save-user"}

View file

@ -17,6 +17,7 @@ import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { UserGroups } from "./UserGroups"; import { UserGroups } from "./UserGroups";
import { UserConsents } from "./UserConsents"; import { UserConsents } from "./UserConsents";
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
export const UsersTabs = () => { export const UsersTabs = () => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
@ -28,6 +29,7 @@ export const UsersTabs = () => {
const userForm = useForm<UserRepresentation>({ mode: "onChange" }); const userForm = useForm<UserRepresentation>({ mode: "onChange" });
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const [user, setUser] = useState(""); const [user, setUser] = useState("");
const [addedGroups, setAddedGroups] = useState<GroupRepresentation[]>([]);
useEffect(() => { useEffect(() => {
const update = async () => { const update = async () => {
@ -39,13 +41,25 @@ export const UsersTabs = () => {
setTimeout(update, 100); setTimeout(update, 100);
}, []); }, []);
const updateGroups = (groups: GroupRepresentation[]) => {
setAddedGroups(groups);
};
const save = async (user: UserRepresentation) => { const save = async (user: UserRepresentation) => {
try { try {
if (id) { if (id) {
await adminClient.users.update({ id: user.id! }, user); await adminClient.users.update({ id: user.id! }, user);
addAlert(t("users:userSaved"), AlertVariant.success); addAlert(t("users:userSaved"), AlertVariant.success);
} else { } else {
await adminClient.users.create(user); const getNewUserId = await adminClient.users.create(user);
addedGroups.forEach(async (group) => {
await adminClient.users.addToGroup({
id: getNewUserId.id!,
groupId: group.id!,
});
});
addAlert(t("users:userCreated"), AlertVariant.success); addAlert(t("users:userCreated"), AlertVariant.success);
history.push(url.substr(0, url.lastIndexOf("/"))); history.push(url.substr(0, url.lastIndexOf("/")));
} }
@ -70,7 +84,12 @@ export const UsersTabs = () => {
data-testid="user-details-tab" data-testid="user-details-tab"
title={<TabTitleText>{t("details")}</TabTitleText>} title={<TabTitleText>{t("details")}</TabTitleText>}
> >
<UserForm form={userForm} save={save} editMode={true} /> <UserForm
onGroupsUpdate={updateGroups}
form={userForm}
save={save}
editMode={true}
/>
</Tab> </Tab>
<Tab <Tab
eventKey="groups" eventKey="groups"
@ -88,7 +107,14 @@ export const UsersTabs = () => {
</Tab> </Tab>
</KeycloakTabs> </KeycloakTabs>
)} )}
{!id && <UserForm form={userForm} save={save} editMode={false} />} {!id && (
<UserForm
onGroupsUpdate={updateGroups}
form={userForm}
save={save}
editMode={false}
/>
)}
</PageSection> </PageSection>
</> </>
); );

View file

@ -10,6 +10,9 @@
"noGroups": "No groups", "noGroups": "No groups",
"noGroupsText": "You haven't added this user to any groups. Join a group to get started.", "noGroupsText": "You haven't added this user to any groups. Join a group to get started.",
"joinGroup": "Join Group", "joinGroup": "Join Group",
"joinGroups": "Join Groups",
"joinGroupsFor": "Join groups for user ",
"selectGroups": "Select groups to join",
"searchForGroups": "Search for groups", "searchForGroups": "Search for groups",
"leave": "Leave", "leave": "Leave",
"leaveGroup": "Leave group {{name}}?", "leaveGroup": "Leave group {{name}}?",
@ -56,7 +59,5 @@
"consents": "Consents", "consents": "Consents",
"noConsents": "No consents", "noConsents": "No consents",
"noConsentsText": "The consents will only be recorded when users try to access a client that is configured to require consent. In that case, users will get a consent page which asks them to grant access to the client." "noConsentsText": "The consents will only be recorded when users try to access a client that is configured to require consent. In that case, users will get a consent page which asks them to grant access to the client."
} }
} }

View file

@ -6,3 +6,17 @@ button.pf-c-button.pf-m-primary.kc-join-group-button {
margin-left: var(--pf-global--spacer--md); margin-left: var(--pf-global--spacer--md);
margin-right: var(--pf-global--spacer--xl); margin-right: var(--pf-global--spacer--xl);
} }
.pf-c-chip-group,
.pf-c-chip-group__list {
width: 100%;
}
button#kc-join-groups-button {
height: min-content;
}
.pf-c-chip-group.pf-m-category {
margin-right: var(--pf-global--spacer--md);
padding: var(--pf-global--spacer--xs);
}