diff --git a/cypress/integration/users_test.spec.ts b/cypress/integration/users_test.spec.ts index 7a9442050a..c1e6d93cfa 100644 --- a/cypress/integration/users_test.spec.ts +++ b/cypress/integration/users_test.spec.ts @@ -3,18 +3,31 @@ import LoginPage from "../support/pages/LoginPage"; import CreateUserPage from "../support/pages/admin_console/manage/users/CreateUserPage"; import Masthead from "../support/pages/admin_console/Masthead"; import ListingPage from "../support/pages/admin_console/ListingPage"; +import UserDetailsPage from "../support/pages/admin_console/manage/users/UserDetailsPage"; +import ModalUtils from "../support/util/ModalUtils"; describe("Users test", () => { const loginPage = new LoginPage(); const sidebarPage = new SidebarPage(); const createUserPage = new CreateUserPage(); const masthead = new Masthead(); + const modalUtils = new ModalUtils(); const listingPage = new ListingPage(); + const userDetailsPage = new UserDetailsPage(); let itemId = "user_crud"; describe("User creation", () => { - beforeEach(function () { + beforeEach(() => { + /* + Prevent unpredictable 401 errors from failing individual tests. + These are most often occurring during the login process: + GET /admin/serverinfo/ + GET /admin/master/console/whoami + */ + cy.on("uncaught:exception", () => { + return false; + }); cy.visit(""); loginPage.logIn(); sidebarPage.goToUsers(); @@ -39,12 +52,36 @@ describe("Users test", () => { createUserPage.goToCreateUser(); - createUserPage.fillRealmRoleData(itemId).save(); + createUserPage.createUser(itemId).save(); masthead.checkNotificationMessage("The user has been created"); sidebarPage.goToUsers(); + }); + + it("Go to user details test", function () { + cy.wait(1000); listingPage.searchItem(itemId).itemExist(itemId); + + cy.wait(1000); + listingPage.goToItemDetails(itemId); + + userDetailsPage.fillUserData().save(); + + masthead.checkNotificationMessage("The user has been saved"); + + sidebarPage.goToUsers(); + listingPage.searchItem(itemId).itemExist(itemId); + + // Delete + cy.wait(1000); + listingPage.deleteItem(itemId); + + modalUtils.checkModalTitle("Delete user?").confirmModal(); + + masthead.checkNotificationMessage("The user has been deleted"); + + listingPage.itemExist(itemId, false); }); }); }); diff --git a/cypress/support/pages/admin_console/manage/users/CreateUserPage.ts b/cypress/support/pages/admin_console/manage/users/CreateUserPage.ts index 451027f911..c0abe02d6e 100644 --- a/cypress/support/pages/admin_console/manage/users/CreateUserPage.ts +++ b/cypress/support/pages/admin_console/manage/users/CreateUserPage.ts @@ -3,21 +3,23 @@ export default class CreateUserPage { usersEmptyState: string; emptyStateCreateUserBtn: string; searchPgCreateUserBtn: string; + addUserBtn: string; saveBtn: string; cancelBtn: string; constructor() { this.usernameInput = "#kc-username"; - this.usersEmptyState = "[data-testid=empty-state]"; - this.emptyStateCreateUserBtn = "[data-testid=empty-primary-action]"; - this.searchPgCreateUserBtn = "[data-testid=create-new-user]"; - this.saveBtn = "[data-testid=create-user]"; - this.cancelBtn = "[data-testid=cancel-create-user]"; + this.usersEmptyState = "empty-state"; + this.emptyStateCreateUserBtn = "empty-primary-action"; + this.searchPgCreateUserBtn = "create-new-user"; + this.addUserBtn = "add-user"; + this.saveBtn = "create-user"; + this.cancelBtn = "cancel-create-user"; } //#region General Settings - fillRealmRoleData(username: string) { + createUser(username: string) { cy.get(this.usernameInput).clear(); if (username) { @@ -30,12 +32,12 @@ export default class CreateUserPage { goToCreateUser() { cy.wait(100); cy.get("body").then((body) => { - if (body.find(this.usersEmptyState).length > 0) { - cy.get(this.emptyStateCreateUserBtn).click(); + if (body.find("[data-testid=empty-state]").length > 0) { + cy.getId(this.emptyStateCreateUserBtn).click(); } else if (body.find("[data-testid=search-users-title]").length > 0) { - cy.get(this.searchPgCreateUserBtn).click(); + cy.getId(this.searchPgCreateUserBtn).click(); } else { - cy.get("[data-testid=add-user]").click(); + cy.getId(this.addUserBtn).click(); } }); @@ -43,13 +45,13 @@ export default class CreateUserPage { } save() { - cy.get(this.saveBtn).click(); + cy.getId(this.saveBtn).click(); return this; } cancel() { - cy.get(this.cancelBtn).click(); + cy.getId(this.cancelBtn).click(); return this; } diff --git a/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts b/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts new file mode 100644 index 0000000000..613a2d0590 --- /dev/null +++ b/cypress/support/pages/admin_console/manage/users/UserDetailsPage.ts @@ -0,0 +1,54 @@ +import { RequiredActionAlias } from "keycloak-admin/lib/defs/requiredActionProviderRepresentation"; + +export default class UserDetailsPage { + saveBtn: string; + cancelBtn: string; + emailInput: string; + emailValue: string; + firstNameInput: string; + firstNameValue: string; + lastNameInput: string; + lastNameValue: string; + enabledSwitch: string; + enabledValue: boolean + requiredUserActions: RequiredActionAlias[]; + + constructor() { + this.saveBtn = "save-user"; + this.cancelBtn = "cancel-create-user"; + this.emailInput = "email-input" + this.emailValue = "example" + "_" + (Math.random() + 1).toString(36).substring(7) + "@example.com"; + this.firstNameInput = "firstName-input" + this.firstNameValue = "firstname"; + this.lastNameInput = "lastName-input" + this.lastNameValue = "lastname"; + this.enabledSwitch = "user-enabled-switch" + this.enabledValue = true; + this.requiredUserActions = [RequiredActionAlias.UPDATE_PASSWORD] + } + + + fillUserData() { + + cy.getId(this.emailInput).type(this.emailValue); + cy.getId(this.firstNameInput).type(this.firstNameValue); + cy.getId(this.lastNameInput).type(this.lastNameValue); + cy.getId(this.enabledSwitch).check({ force: true }); + + + return this; + } + + save() { + cy.getId(this.saveBtn).click(); + + return this; + } + + cancel() { + cy.getId(this.cancelBtn).click(); + + return this; + } + } + \ No newline at end of file diff --git a/src/realm-roles/AssociatedRolesModal.tsx b/src/realm-roles/AssociatedRolesModal.tsx index 45a0121109..a6748dc82d 100644 --- a/src/realm-roles/AssociatedRolesModal.tsx +++ b/src/realm-roles/AssociatedRolesModal.tsx @@ -45,9 +45,6 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { const [name, setName] = useState(""); const adminClient = useAdminClient(); const [selectedRows, setSelectedRows] = useState([]); - const [allClientRoles, setAllClientRoles] = useState( - [] - ); const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); const [filterType, setFilterType] = useState("roles"); @@ -116,9 +113,6 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { id, }); - setAllClientRoles(rolesList); - console.log(allClientRoles); - return alphabetize(rolesList).filter((role: RoleRepresentation) => { return ( existingAdditionalRoles.find( diff --git a/src/route-config.ts b/src/route-config.ts index 60440520c9..c74a656118 100644 --- a/src/route-config.ts +++ b/src/route-config.ts @@ -145,6 +145,12 @@ export const routes: RoutesFn = (t: TFunction) => [ breadcrumb: t("users:createUser"), access: "manage-users", }, + { + path: "/:realm/users/:id/:tab", + component: UsersTabs, + breadcrumb: t("users:userDetails"), + access: "manage-users", + }, { path: "/:realm/sessions", component: SessionsSection, diff --git a/src/user/UserForm.tsx b/src/user/UserForm.tsx index 615da7db3d..2476e4c289 100644 --- a/src/user/UserForm.tsx +++ b/src/user/UserForm.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { useEffect, useState } from "react"; import { ActionGroup, Button, @@ -10,20 +10,26 @@ import { } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; import { Controller, UseFormMethods } from "react-hook-form"; -import { useHistory } from "react-router-dom"; +import { useHistory, useParams } from "react-router-dom"; import { FormAccess } from "../components/form-access/FormAccess"; import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation"; import { HelpItem } from "../components/help-enabler/HelpItem"; import { useRealm } from "../context/realm-context/RealmContext"; +import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient"; +import { useErrorHandler } from "react-error-boundary"; +import moment from "moment"; export type UserFormProps = { form: UseFormMethods; save: (user: UserRepresentation) => void; + editMode: boolean; + timestamp?: number; }; export const UserForm = ({ - form: { handleSubmit, register, errors, watch, control }, + form: { handleSubmit, register, errors, watch, control, setValue, reset }, save, + editMode, }: UserFormProps) => { const { t } = useTranslation("users"); const { realm } = useRealm(); @@ -32,33 +38,56 @@ export const UserForm = ({ isRequiredUserActionsDropdownOpen, setRequiredUserActionsDropdownOpen, ] = useState(false); - const [selected, setSelected] = useState([]); const history = useHistory(); + const adminClient = useAdminClient(); + const { id } = useParams<{ id: string }>(); + const handleError = useErrorHandler(); const watchUsernameInput = watch("username"); + const [timestamp, setTimestamp] = useState(null); + + useEffect(() => { + if (editMode) { + return asyncStateFetch( + () => adminClient.users.find({ username: id }), + (user) => { + setupForm(user[0]); + }, + handleError + ); + } + }, []); + + const setupForm = (user: UserRepresentation) => { + reset(); + Object.entries(user).map((entry) => { + console.log(entry[0], entry[1]); + if (entry[0] == "createdTimestamp") { + setTimestamp(entry[1]); + } else { + setValue(entry[0], entry[1]); + } + }); + }; const emailRegexPattern = /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/; const requiredUserActionsOptions = [ - + {t("configureOTP")} , - + {t("updatePassword")} , - + {t("updateProfile")} , - + {t("verifyEmail")} , - - {t("updateUserLocale")} - , ]; const clearSelection = () => { - setSelected([]); setRequiredUserActionsDropdownOpen(false); }; @@ -69,20 +98,56 @@ export const UserForm = ({ role="manage-users" className="pf-u-mt-lg" > - - - + {editMode ? ( + <> + + + + + + + + ) : ( + + + + )} @@ -112,15 +178,16 @@ export const UserForm = ({ } > ( onChange([`${value}`])} - isChecked={value[0] === "true"} + onChange={(value) => onChange(value)} + isChecked={value} label={t("common:on")} labelOff={t("common:off")} /> @@ -135,6 +202,7 @@ export const UserForm = ({ > ( onChange([`${value}`])} - isChecked={value[0] === "true"} + onChange={(value) => onChange(value)} + isChecked={value} label={t("common:on")} labelOff={t("common:off")} /> @@ -194,12 +264,13 @@ export const UserForm = ({ } > ( + render={({ onChange, value }) => ( )} - > + />