Users: Adds user details tab and save/update functionality (#426)
* userDetails tab done, save function wip * userDetails update working * add cypress test * fix default value * PR feedback from Erik
This commit is contained in:
parent
b035143781
commit
28f1fdfda4
9 changed files with 273 additions and 81 deletions
|
@ -3,18 +3,31 @@ import LoginPage from "../support/pages/LoginPage";
|
||||||
import CreateUserPage from "../support/pages/admin_console/manage/users/CreateUserPage";
|
import CreateUserPage from "../support/pages/admin_console/manage/users/CreateUserPage";
|
||||||
import Masthead from "../support/pages/admin_console/Masthead";
|
import Masthead from "../support/pages/admin_console/Masthead";
|
||||||
import ListingPage from "../support/pages/admin_console/ListingPage";
|
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", () => {
|
describe("Users test", () => {
|
||||||
const loginPage = new LoginPage();
|
const loginPage = new LoginPage();
|
||||||
const sidebarPage = new SidebarPage();
|
const sidebarPage = new SidebarPage();
|
||||||
const createUserPage = new CreateUserPage();
|
const createUserPage = new CreateUserPage();
|
||||||
const masthead = new Masthead();
|
const masthead = new Masthead();
|
||||||
|
const modalUtils = new ModalUtils();
|
||||||
const listingPage = new ListingPage();
|
const listingPage = new ListingPage();
|
||||||
|
const userDetailsPage = new UserDetailsPage();
|
||||||
|
|
||||||
let itemId = "user_crud";
|
let itemId = "user_crud";
|
||||||
|
|
||||||
describe("User creation", () => {
|
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("");
|
cy.visit("");
|
||||||
loginPage.logIn();
|
loginPage.logIn();
|
||||||
sidebarPage.goToUsers();
|
sidebarPage.goToUsers();
|
||||||
|
@ -39,12 +52,36 @@ describe("Users test", () => {
|
||||||
|
|
||||||
createUserPage.goToCreateUser();
|
createUserPage.goToCreateUser();
|
||||||
|
|
||||||
createUserPage.fillRealmRoleData(itemId).save();
|
createUserPage.createUser(itemId).save();
|
||||||
|
|
||||||
masthead.checkNotificationMessage("The user has been created");
|
masthead.checkNotificationMessage("The user has been created");
|
||||||
|
|
||||||
sidebarPage.goToUsers();
|
sidebarPage.goToUsers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("Go to user details test", function () {
|
||||||
|
cy.wait(1000);
|
||||||
listingPage.searchItem(itemId).itemExist(itemId);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -3,21 +3,23 @@ export default class CreateUserPage {
|
||||||
usersEmptyState: string;
|
usersEmptyState: string;
|
||||||
emptyStateCreateUserBtn: string;
|
emptyStateCreateUserBtn: string;
|
||||||
searchPgCreateUserBtn: string;
|
searchPgCreateUserBtn: string;
|
||||||
|
addUserBtn: string;
|
||||||
saveBtn: string;
|
saveBtn: string;
|
||||||
cancelBtn: string;
|
cancelBtn: string;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.usernameInput = "#kc-username";
|
this.usernameInput = "#kc-username";
|
||||||
|
|
||||||
this.usersEmptyState = "[data-testid=empty-state]";
|
this.usersEmptyState = "empty-state";
|
||||||
this.emptyStateCreateUserBtn = "[data-testid=empty-primary-action]";
|
this.emptyStateCreateUserBtn = "empty-primary-action";
|
||||||
this.searchPgCreateUserBtn = "[data-testid=create-new-user]";
|
this.searchPgCreateUserBtn = "create-new-user";
|
||||||
this.saveBtn = "[data-testid=create-user]";
|
this.addUserBtn = "add-user";
|
||||||
this.cancelBtn = "[data-testid=cancel-create-user]";
|
this.saveBtn = "create-user";
|
||||||
|
this.cancelBtn = "cancel-create-user";
|
||||||
}
|
}
|
||||||
|
|
||||||
//#region General Settings
|
//#region General Settings
|
||||||
fillRealmRoleData(username: string) {
|
createUser(username: string) {
|
||||||
cy.get(this.usernameInput).clear();
|
cy.get(this.usernameInput).clear();
|
||||||
|
|
||||||
if (username) {
|
if (username) {
|
||||||
|
@ -30,12 +32,12 @@ export default class CreateUserPage {
|
||||||
goToCreateUser() {
|
goToCreateUser() {
|
||||||
cy.wait(100);
|
cy.wait(100);
|
||||||
cy.get("body").then((body) => {
|
cy.get("body").then((body) => {
|
||||||
if (body.find(this.usersEmptyState).length > 0) {
|
if (body.find("[data-testid=empty-state]").length > 0) {
|
||||||
cy.get(this.emptyStateCreateUserBtn).click();
|
cy.getId(this.emptyStateCreateUserBtn).click();
|
||||||
} else if (body.find("[data-testid=search-users-title]").length > 0) {
|
} else if (body.find("[data-testid=search-users-title]").length > 0) {
|
||||||
cy.get(this.searchPgCreateUserBtn).click();
|
cy.getId(this.searchPgCreateUserBtn).click();
|
||||||
} else {
|
} else {
|
||||||
cy.get("[data-testid=add-user]").click();
|
cy.getId(this.addUserBtn).click();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -43,13 +45,13 @@ export default class CreateUserPage {
|
||||||
}
|
}
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
cy.get(this.saveBtn).click();
|
cy.getId(this.saveBtn).click();
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel() {
|
cancel() {
|
||||||
cy.get(this.cancelBtn).click();
|
cy.getId(this.cancelBtn).click();
|
||||||
|
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -45,9 +45,6 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
const [name, setName] = useState("");
|
const [name, setName] = useState("");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
|
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
|
||||||
const [allClientRoles, setAllClientRoles] = useState<RoleRepresentation[]>(
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
|
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
|
||||||
const [filterType, setFilterType] = useState("roles");
|
const [filterType, setFilterType] = useState("roles");
|
||||||
|
@ -116,9 +113,6 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
|
||||||
id,
|
id,
|
||||||
});
|
});
|
||||||
|
|
||||||
setAllClientRoles(rolesList);
|
|
||||||
console.log(allClientRoles);
|
|
||||||
|
|
||||||
return alphabetize(rolesList).filter((role: RoleRepresentation) => {
|
return alphabetize(rolesList).filter((role: RoleRepresentation) => {
|
||||||
return (
|
return (
|
||||||
existingAdditionalRoles.find(
|
existingAdditionalRoles.find(
|
||||||
|
|
|
@ -145,6 +145,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
||||||
breadcrumb: t("users:createUser"),
|
breadcrumb: t("users:createUser"),
|
||||||
access: "manage-users",
|
access: "manage-users",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/:realm/users/:id/:tab",
|
||||||
|
component: UsersTabs,
|
||||||
|
breadcrumb: t("users:userDetails"),
|
||||||
|
access: "manage-users",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/:realm/sessions",
|
path: "/:realm/sessions",
|
||||||
component: SessionsSection,
|
component: SessionsSection,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import {
|
import {
|
||||||
ActionGroup,
|
ActionGroup,
|
||||||
Button,
|
Button,
|
||||||
|
@ -10,20 +10,26 @@ import {
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Controller, UseFormMethods } from "react-hook-form";
|
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 { FormAccess } from "../components/form-access/FormAccess";
|
||||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||||
import { useRealm } from "../context/realm-context/RealmContext";
|
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 = {
|
export type UserFormProps = {
|
||||||
form: UseFormMethods<UserRepresentation>;
|
form: UseFormMethods<UserRepresentation>;
|
||||||
save: (user: UserRepresentation) => void;
|
save: (user: UserRepresentation) => void;
|
||||||
|
editMode: boolean;
|
||||||
|
timestamp?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const UserForm = ({
|
export const UserForm = ({
|
||||||
form: { handleSubmit, register, errors, watch, control },
|
form: { handleSubmit, register, errors, watch, control, setValue, reset },
|
||||||
save,
|
save,
|
||||||
|
editMode,
|
||||||
}: UserFormProps) => {
|
}: UserFormProps) => {
|
||||||
const { t } = useTranslation("users");
|
const { t } = useTranslation("users");
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
|
@ -32,33 +38,56 @@ export const UserForm = ({
|
||||||
isRequiredUserActionsDropdownOpen,
|
isRequiredUserActionsDropdownOpen,
|
||||||
setRequiredUserActionsDropdownOpen,
|
setRequiredUserActionsDropdownOpen,
|
||||||
] = useState(false);
|
] = useState(false);
|
||||||
const [selected, setSelected] = useState<string[]>([]);
|
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
const adminClient = useAdminClient();
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
const handleError = useErrorHandler();
|
||||||
|
|
||||||
const watchUsernameInput = watch("username");
|
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 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 = [
|
const requiredUserActionsOptions = [
|
||||||
<SelectOption key={0} value="Configure OTP">
|
<SelectOption key={0} value="CONFIGURE_TOTP">
|
||||||
{t("configureOTP")}
|
{t("configureOTP")}
|
||||||
</SelectOption>,
|
</SelectOption>,
|
||||||
<SelectOption key={1} value="Update Password">
|
<SelectOption key={1} value="UPDATE_PASSWORD">
|
||||||
{t("updatePassword")}
|
{t("updatePassword")}
|
||||||
</SelectOption>,
|
</SelectOption>,
|
||||||
<SelectOption key={2} value="Update Profile">
|
<SelectOption key={2} value="UPDATE_PROFILE">
|
||||||
{t("updateProfile")}
|
{t("updateProfile")}
|
||||||
</SelectOption>,
|
</SelectOption>,
|
||||||
<SelectOption key={3} value="Verify Email">
|
<SelectOption key={3} value="VERIFY_EMAIL">
|
||||||
{t("verifyEmail")}
|
{t("verifyEmail")}
|
||||||
</SelectOption>,
|
</SelectOption>,
|
||||||
<SelectOption key={4} value="Update User Locale">
|
|
||||||
{t("updateUserLocale")}
|
|
||||||
</SelectOption>,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const clearSelection = () => {
|
const clearSelection = () => {
|
||||||
setSelected([]);
|
|
||||||
setRequiredUserActionsDropdownOpen(false);
|
setRequiredUserActionsDropdownOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -69,20 +98,56 @@ export const UserForm = ({
|
||||||
role="manage-users"
|
role="manage-users"
|
||||||
className="pf-u-mt-lg"
|
className="pf-u-mt-lg"
|
||||||
>
|
>
|
||||||
<FormGroup
|
{editMode ? (
|
||||||
label={t("username")}
|
<>
|
||||||
fieldId="kc-username"
|
<FormGroup
|
||||||
isRequired
|
label={t("id")}
|
||||||
validated={errors.username ? "error" : "default"}
|
fieldId="kc-id"
|
||||||
helperTextInvalid={t("common:required")}
|
isRequired
|
||||||
>
|
validated={errors.id ? "error" : "default"}
|
||||||
<TextInput
|
helperTextInvalid={t("common:required")}
|
||||||
ref={register({ required: true })}
|
>
|
||||||
type="text"
|
<TextInput
|
||||||
id="kc-username"
|
ref={register({ required: !editMode })}
|
||||||
name="username"
|
type="text"
|
||||||
/>
|
id="kc-id"
|
||||||
</FormGroup>
|
name="id"
|
||||||
|
isReadOnly={editMode}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
<FormGroup
|
||||||
|
label={t("createdAt")}
|
||||||
|
fieldId="kc-created-at"
|
||||||
|
isRequired
|
||||||
|
validated={errors.createdTimestamp ? "error" : "default"}
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
value={moment(timestamp).format("MM/DD/YY hh:MM:ss A")}
|
||||||
|
type="text"
|
||||||
|
id="kc-created-at"
|
||||||
|
name="createdTimestamp"
|
||||||
|
isReadOnly={editMode}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<FormGroup
|
||||||
|
label={t("username")}
|
||||||
|
fieldId="kc-username"
|
||||||
|
isRequired
|
||||||
|
validated={errors.username ? "error" : "default"}
|
||||||
|
helperTextInvalid={t("common:required")}
|
||||||
|
>
|
||||||
|
<TextInput
|
||||||
|
ref={register()}
|
||||||
|
type="text"
|
||||||
|
id="kc-username"
|
||||||
|
name="username"
|
||||||
|
isReadOnly={editMode}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
<FormGroup
|
<FormGroup
|
||||||
label={t("email")}
|
label={t("email")}
|
||||||
fieldId="kc-description"
|
fieldId="kc-description"
|
||||||
|
@ -96,6 +161,7 @@ export const UserForm = ({
|
||||||
type="email"
|
type="email"
|
||||||
id="kc-email"
|
id="kc-email"
|
||||||
name="email"
|
name="email"
|
||||||
|
data-testid="email-input"
|
||||||
aria-label={t("emailInput")}
|
aria-label={t("emailInput")}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -112,15 +178,16 @@ export const UserForm = ({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="user-email-verified"
|
name="emailVerified"
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="email-verified-switch"
|
||||||
id={"kc-user-email-verified"}
|
id={"kc-user-email-verified"}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
onChange={(value) => onChange([`${value}`])}
|
onChange={(value) => onChange(value)}
|
||||||
isChecked={value[0] === "true"}
|
isChecked={value}
|
||||||
label={t("common:on")}
|
label={t("common:on")}
|
||||||
labelOff={t("common:off")}
|
labelOff={t("common:off")}
|
||||||
/>
|
/>
|
||||||
|
@ -135,6 +202,7 @@ export const UserForm = ({
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={register()}
|
ref={register()}
|
||||||
|
data-testid="firstName-input"
|
||||||
type="text"
|
type="text"
|
||||||
id="kc-firstname"
|
id="kc-firstname"
|
||||||
name="firstName"
|
name="firstName"
|
||||||
|
@ -147,6 +215,7 @@ export const UserForm = ({
|
||||||
>
|
>
|
||||||
<TextInput
|
<TextInput
|
||||||
ref={register()}
|
ref={register()}
|
||||||
|
data-testid="lastName-input"
|
||||||
type="text"
|
type="text"
|
||||||
id="kc-lastname"
|
id="kc-lastname"
|
||||||
name="lastName"
|
name="lastName"
|
||||||
|
@ -165,15 +234,16 @@ export const UserForm = ({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="user-enabled"
|
name="enabled"
|
||||||
defaultValue={false}
|
defaultValue={false}
|
||||||
control={control}
|
control={control}
|
||||||
render={({ onChange, value }) => (
|
render={({ onChange, value }) => (
|
||||||
<Switch
|
<Switch
|
||||||
|
data-testid="user-enabled-switch"
|
||||||
id={"kc-user-enabled"}
|
id={"kc-user-enabled"}
|
||||||
isDisabled={false}
|
isDisabled={false}
|
||||||
onChange={(value) => onChange([`${value}`])}
|
onChange={(value) => onChange(value)}
|
||||||
isChecked={value[0] === "true"}
|
isChecked={value}
|
||||||
label={t("common:on")}
|
label={t("common:on")}
|
||||||
labelOff={t("common:off")}
|
labelOff={t("common:off")}
|
||||||
/>
|
/>
|
||||||
|
@ -194,12 +264,13 @@ export const UserForm = ({
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<Controller
|
<Controller
|
||||||
name="required-user-actions"
|
name="requiredActions"
|
||||||
defaultValue={["0"]}
|
defaultValue={[]}
|
||||||
typeAheadAriaLabel="Select an action"
|
typeAheadAriaLabel="Select an action"
|
||||||
control={control}
|
control={control}
|
||||||
render={() => (
|
render={({ onChange, value }) => (
|
||||||
<Select
|
<Select
|
||||||
|
data-testid="required-actions-select"
|
||||||
placeholderText="Select action"
|
placeholderText="Select action"
|
||||||
toggleId="kc-required-user-actions"
|
toggleId="kc-required-user-actions"
|
||||||
onToggle={() =>
|
onToggle={() =>
|
||||||
|
@ -208,13 +279,13 @@ export const UserForm = ({
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
isOpen={isRequiredUserActionsDropdownOpen}
|
isOpen={isRequiredUserActionsDropdownOpen}
|
||||||
selections={selected}
|
selections={value}
|
||||||
onSelect={(_, value) => {
|
onSelect={(_, v) => {
|
||||||
const option = value as string;
|
const option = v as string;
|
||||||
if (selected.includes(option)) {
|
if (value.includes(option)) {
|
||||||
setSelected(selected.filter((item) => item !== option));
|
onChange(value.filter((item: string) => item !== option));
|
||||||
} else {
|
} else {
|
||||||
setSelected([...selected, option]);
|
onChange([...value, option]);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onClear={clearSelection}
|
onClear={clearSelection}
|
||||||
|
@ -223,16 +294,16 @@ export const UserForm = ({
|
||||||
{requiredUserActionsOptions}
|
{requiredUserActionsOptions}
|
||||||
</Select>
|
</Select>
|
||||||
)}
|
)}
|
||||||
></Controller>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
<ActionGroup>
|
<ActionGroup>
|
||||||
<Button
|
<Button
|
||||||
data-testid="create-user"
|
data-testid={!editMode ? "create-user" : "save-user"}
|
||||||
isDisabled={!watchUsernameInput}
|
isDisabled={!editMode && !watchUsernameInput}
|
||||||
variant="primary"
|
variant="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
{t("common:Create")}
|
{!editMode ? t("common:Create") : t("common:Save")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
data-testid="cancel-create-user"
|
data-testid="cancel-create-user"
|
||||||
|
|
|
@ -28,7 +28,7 @@ import { emptyFormatter } from "../util";
|
||||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||||
|
|
||||||
import "./user-section.css";
|
import "./user-section.css";
|
||||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
import { Link, useHistory, useRouteMatch } from "react-router-dom";
|
||||||
|
|
||||||
type BruteUser = UserRepresentation & {
|
type BruteUser = UserRepresentation & {
|
||||||
brute?: Record<string, object>;
|
brute?: Record<string, object>;
|
||||||
|
@ -71,6 +71,14 @@ export const UsersSection = () => {
|
||||||
);
|
);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const UserDetailLink = (user: UserRepresentation) => (
|
||||||
|
<>
|
||||||
|
<Link key={user.username} to={`${url}/${user.username}/details`}>
|
||||||
|
{user.username}
|
||||||
|
</Link>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
const loader = async (first?: number, max?: number, search?: string) => {
|
const loader = async (first?: number, max?: number, search?: string) => {
|
||||||
const params: { [name: string]: string | number } = {
|
const params: { [name: string]: string | number } = {
|
||||||
first: first!,
|
first: first!,
|
||||||
|
@ -221,6 +229,7 @@ export const UsersSection = () => {
|
||||||
{
|
{
|
||||||
name: "username",
|
name: "username",
|
||||||
displayKey: "users:username",
|
displayKey: "users:username",
|
||||||
|
cellRenderer: UserDetailLink,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "email",
|
name: "email",
|
||||||
|
|
|
@ -1,5 +1,10 @@
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { AlertVariant, PageSection } from "@patternfly/react-core";
|
import {
|
||||||
|
AlertVariant,
|
||||||
|
PageSection,
|
||||||
|
Tab,
|
||||||
|
TabTitleText,
|
||||||
|
} from "@patternfly/react-core";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
|
@ -8,7 +13,8 @@ import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||||
import { UserForm } from "./UserForm";
|
import { UserForm } from "./UserForm";
|
||||||
import { useAlerts } from "../components/alert/Alerts";
|
import { useAlerts } from "../components/alert/Alerts";
|
||||||
import { useAdminClient } from "../context/auth/AdminClient";
|
import { useAdminClient } from "../context/auth/AdminClient";
|
||||||
import { useHistory, useRouteMatch } from "react-router-dom";
|
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
||||||
|
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||||
|
|
||||||
export const UsersTabs = () => {
|
export const UsersTabs = () => {
|
||||||
const { t } = useTranslation("roles");
|
const { t } = useTranslation("roles");
|
||||||
|
@ -18,20 +24,18 @@ export const UsersTabs = () => {
|
||||||
|
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
||||||
|
const { id } = useParams<{ id: string }>();
|
||||||
|
|
||||||
const save = async (user: UserRepresentation) => {
|
const save = async (user: UserRepresentation) => {
|
||||||
try {
|
try {
|
||||||
await adminClient.users.create({
|
if (id) {
|
||||||
username: user!.username,
|
await adminClient.users.update({ id: user.id! }, user);
|
||||||
email: user!.email,
|
addAlert(t("users:userSaved"), AlertVariant.success);
|
||||||
emailVerified: user!.emailVerified,
|
} else {
|
||||||
firstName: user!.firstName,
|
await adminClient.users.create(user);
|
||||||
lastName: user!.lastName,
|
addAlert(t("users:userCreated"), AlertVariant.success);
|
||||||
enabled: user!.enabled,
|
history.push(url.substr(0, url.lastIndexOf("/")));
|
||||||
requiredActions: user!.requiredActions,
|
}
|
||||||
});
|
|
||||||
addAlert(t("users:userCreated"), AlertVariant.success);
|
|
||||||
history.push(url.substr(0, url.lastIndexOf("/")));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addAlert(
|
addAlert(
|
||||||
t("users:userCreateError", {
|
t("users:userCreateError", {
|
||||||
|
@ -45,12 +49,23 @@ export const UsersTabs = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ViewHeader
|
<ViewHeader
|
||||||
titleKey={t("users:createUser")}
|
titleKey={id! || t("users:createUser")}
|
||||||
subKey=""
|
subKey=""
|
||||||
dividerComponent="div"
|
dividerComponent="div"
|
||||||
/>
|
/>
|
||||||
<PageSection variant="light">
|
<PageSection variant="light">
|
||||||
<UserForm form={form} save={save} />
|
{id && (
|
||||||
|
<KeycloakTabs isBox>
|
||||||
|
<Tab
|
||||||
|
eventKey="details"
|
||||||
|
data-testid="user-details-tab"
|
||||||
|
title={<TabTitleText>{t("details")}</TabTitleText>}
|
||||||
|
>
|
||||||
|
<UserForm form={form} save={save} editMode={true} />
|
||||||
|
</Tab>
|
||||||
|
</KeycloakTabs>
|
||||||
|
)}
|
||||||
|
{!id && <UserForm form={form} save={save} editMode={false} />}
|
||||||
</PageSection>
|
</PageSection>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
|
@ -8,6 +8,8 @@
|
||||||
"noUsersFound": "No users found",
|
"noUsersFound": "No users found",
|
||||||
"noUsersFoundError": "No users found due to {{error}}",
|
"noUsersFoundError": "No users found due to {{error}}",
|
||||||
"emptyInstructions": "Change your search criteria or add a user",
|
"emptyInstructions": "Change your search criteria or add a user",
|
||||||
|
"id": "ID",
|
||||||
|
"createdAt": "Created at",
|
||||||
"username": "Username",
|
"username": "Username",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"emailVerified": "Email verified",
|
"emailVerified": "Email verified",
|
||||||
|
@ -28,6 +30,8 @@
|
||||||
"deleteConfirmDialog": "Are you sure you want to permanently delete {{count}} selected user",
|
"deleteConfirmDialog": "Are you sure you want to permanently delete {{count}} selected user",
|
||||||
"deleteConfirmDialog_plural": "Are you sure you want to permanently delete {{count}} selected users",
|
"deleteConfirmDialog_plural": "Are you sure you want to permanently delete {{count}} selected users",
|
||||||
"userCreated": "The user has been created",
|
"userCreated": "The user has been created",
|
||||||
|
"userSaved": "The user has been saved",
|
||||||
|
"userDetails": "User details",
|
||||||
"userCreateError": "Could not create user: {{error}}",
|
"userCreateError": "Could not create user: {{error}}",
|
||||||
"userDeletedSuccess": "The user has been deleted",
|
"userDeletedSuccess": "The user has been deleted",
|
||||||
"userDeletedError": "The user could not be deleted {{error}}",
|
"userDeletedError": "The user could not be deleted {{error}}",
|
||||||
|
|
Loading…
Reference in a new issue