Credentials Reset Flow (#1699)
* Add credential reset modal * Add i18n labels * Refactor to align with marvelapp mockup TODO: tests * Add e2e tests * Implement code review change requests Add menuAppendTo to CredentialResetActionMultiSelect -> Select component Add optional menuAppendTo prop to TimeSelectorComponent Refactor CredentialsPage constructor
This commit is contained in:
parent
fd6e433c9c
commit
2276311334
9 changed files with 562 additions and 168 deletions
|
@ -10,6 +10,7 @@ import { keycloakBefore } from "../support/util/keycloak_before";
|
|||
import GroupModal from "../support/pages/admin_console/manage/groups/GroupModal";
|
||||
import UserGroupsPage from "../support/pages/admin_console/manage/users/UserGroupsPage";
|
||||
import AdminClient from "../support/util/AdminClient";
|
||||
import CredentialsPage from "../support/pages/admin_console/manage/users/CredentialsPage";
|
||||
|
||||
let groupName = "group";
|
||||
let groupsList: string[] = [];
|
||||
|
@ -54,9 +55,11 @@ describe("Users test", () => {
|
|||
const modalUtils = new ModalUtils();
|
||||
const listingPage = new ListingPage();
|
||||
const userDetailsPage = new UserDetailsPage();
|
||||
const credentialsPage = new CredentialsPage();
|
||||
const attributesTab = new AttributesTab();
|
||||
|
||||
let itemId = "user_crud";
|
||||
let itemIdWithCred = "user_crud_cred";
|
||||
|
||||
describe("User creation", () => {
|
||||
beforeEach(() => {
|
||||
|
@ -106,6 +109,34 @@ describe("Users test", () => {
|
|||
sidebarPage.goToUsers();
|
||||
});
|
||||
|
||||
it("Create user with credentials test", () => {
|
||||
itemIdWithCred += "_" + (Math.random() + 1).toString(36).substring(7);
|
||||
|
||||
createUserPage.goToCreateUser();
|
||||
|
||||
createUserPage.createUser(itemIdWithCred);
|
||||
|
||||
createUserPage.save();
|
||||
|
||||
masthead.checkNotificationMessage("The user has been created");
|
||||
|
||||
sidebarPage.goToUsers();
|
||||
|
||||
listingPage.goToItemDetails(itemIdWithCred);
|
||||
|
||||
userDetailsPage.fillUserData().save();
|
||||
masthead.checkNotificationMessage("The user has been saved");
|
||||
|
||||
credentialsPage
|
||||
.goToCredentialsTab()
|
||||
.clickEmptyStatePasswordBtn()
|
||||
.fillPasswordForm()
|
||||
.clickConfirmationBtn()
|
||||
.clickSetPasswordBtn();
|
||||
|
||||
sidebarPage.goToUsers();
|
||||
});
|
||||
|
||||
it("User details test", () => {
|
||||
cy.wait("@brute-force");
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
|
@ -199,6 +230,27 @@ describe("Users test", () => {
|
|||
cy.findByTestId("empty-state").contains("No consents");
|
||||
});
|
||||
|
||||
it("Reset credential of User with empty state", () => {
|
||||
cy.wait("@brute-force");
|
||||
listingPage.goToItemDetails(itemId);
|
||||
credentialsPage
|
||||
.goToCredentialsTab()
|
||||
.clickEmptyStateResetBtn()
|
||||
.fillResetCredentialForm();
|
||||
masthead.checkNotificationMessage("Failed to send email to user.");
|
||||
});
|
||||
|
||||
it("Reset credential of User with existing credentials", () => {
|
||||
cy.wait("@brute-force");
|
||||
listingPage.goToItemDetails(itemIdWithCred);
|
||||
credentialsPage
|
||||
.goToCredentialsTab()
|
||||
.clickResetBtn()
|
||||
.fillResetCredentialForm();
|
||||
|
||||
masthead.checkNotificationMessage("Failed to send email to user.");
|
||||
});
|
||||
|
||||
it("Delete user test", () => {
|
||||
// Delete
|
||||
listingPage.deleteItem(itemId);
|
||||
|
@ -209,5 +261,16 @@ describe("Users test", () => {
|
|||
|
||||
listingPage.itemExist(itemId, false);
|
||||
});
|
||||
|
||||
it("Delete user with credential test", () => {
|
||||
// Delete
|
||||
listingPage.deleteItem(itemIdWithCred);
|
||||
|
||||
modalUtils.checkModalTitle("Delete user?").confirmModal();
|
||||
|
||||
masthead.checkNotificationMessage("The user has been deleted");
|
||||
|
||||
listingPage.itemExist(itemIdWithCred, false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
export default class CredentialsPage {
|
||||
private readonly credentialsTab = "credentials";
|
||||
private readonly emptyStatePasswordBtn = "no-credentials-empty-action";
|
||||
private readonly emptyStateResetBtn = "credential-reset-empty-action";
|
||||
private readonly resetBtn = "credentialResetBtn";
|
||||
private readonly setPasswordBtn = "setPasswordBtn";
|
||||
private readonly credentialResetModal = "credential-reset-modal";
|
||||
private readonly resetModalActionsToggleBtn =
|
||||
"[data-testid=credential-reset-modal] #actions";
|
||||
private readonly passwordField =
|
||||
".kc-password > .pf-c-input-group > .pf-c-form-control";
|
||||
private readonly passwordConfirmationField =
|
||||
".kc-passwordConfirmation > .pf-c-input-group > .pf-c-form-control";
|
||||
private readonly resetActions = [
|
||||
"VERIFY_EMAIL-option",
|
||||
"UPDATE_PROFILE-option",
|
||||
"CONFIGURE_TOTP-option",
|
||||
"UPDATE_PASSWORD-option",
|
||||
"terms_and_conditions-option",
|
||||
];
|
||||
private readonly confirmationButton = "okBtn";
|
||||
|
||||
goToCredentialsTab() {
|
||||
cy.findByTestId(this.credentialsTab).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
clickEmptyStatePasswordBtn() {
|
||||
cy.findByTestId(this.emptyStatePasswordBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clickEmptyStateResetBtn() {
|
||||
cy.findByTestId(this.emptyStateResetBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clickResetBtn() {
|
||||
cy.findByTestId(this.resetBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clickResetModalActionsToggleBtn() {
|
||||
cy.get(this.resetModalActionsToggleBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clickResetModalAction(index: number) {
|
||||
cy.findByTestId(this.resetActions[index]).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clickConfirmationBtn() {
|
||||
cy.findByTestId(this.confirmationButton).dblclick();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fillPasswordForm() {
|
||||
cy.get(this.passwordField).type("test");
|
||||
cy.get(this.passwordConfirmationField).type("test");
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
fillResetCredentialForm() {
|
||||
cy.findByTestId(this.credentialResetModal);
|
||||
this.clickResetModalActionsToggleBtn()
|
||||
.clickResetModalAction(2)
|
||||
.clickResetModalAction(3)
|
||||
.clickConfirmationBtn();
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
clickSetPasswordBtn() {
|
||||
cy.findByTestId(this.setPasswordBtn).click();
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -4,7 +4,7 @@ export default class UserDetailsPage {
|
|||
saveBtn: string;
|
||||
cancelBtn: string;
|
||||
emailInput: string;
|
||||
emailValue: string;
|
||||
emailValue: () => string;
|
||||
firstNameInput: string;
|
||||
firstNameValue: string;
|
||||
lastNameInput: string;
|
||||
|
@ -17,7 +17,7 @@ export default class UserDetailsPage {
|
|||
this.saveBtn = "save-user";
|
||||
this.cancelBtn = "cancel-create-user";
|
||||
this.emailInput = "email-input";
|
||||
this.emailValue =
|
||||
this.emailValue = () =>
|
||||
"example" +
|
||||
"_" +
|
||||
(Math.random() + 1).toString(36).substring(7) +
|
||||
|
@ -32,7 +32,7 @@ export default class UserDetailsPage {
|
|||
}
|
||||
|
||||
fillUserData() {
|
||||
cy.findByTestId(this.emailInput).type(this.emailValue);
|
||||
cy.findByTestId(this.emailInput).type(this.emailValue());
|
||||
cy.findByTestId(this.firstNameInput).type(this.firstNameValue);
|
||||
cy.findByTestId(this.lastNameInput).type(this.lastNameValue);
|
||||
cy.findByTestId(this.enabledSwitch).check({ force: true });
|
||||
|
|
|
@ -173,5 +173,8 @@ export default {
|
|||
ownerManagedAccess:
|
||||
"If enabled, the access to this resource can be managed by the resource owner.",
|
||||
resourceAttribute: "The attributes associated wth the resource.",
|
||||
resetActions:
|
||||
"Set of actions to execute when sending the user a Reset Actions Email. 'Verify email' sends an email to the user to verify their email address. 'Update profile' requires user to enter in new personal information. 'Update password' requires user to enter in a new password. 'Configure OTP' requires setup of a mobile password generator.",
|
||||
lifespan: "Maximum time before the action permit expires.",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -66,6 +66,9 @@ export const ListEmptyState = ({
|
|||
{secondaryActions.map((action) => (
|
||||
<Button
|
||||
key={action.text}
|
||||
data-testid={`${action.text
|
||||
.replace(/\W+/g, "-")
|
||||
.toLowerCase()}-empty-action`}
|
||||
variant={action.type || ButtonVariant.secondary}
|
||||
onClick={action.onClick}
|
||||
>
|
||||
|
|
|
@ -6,18 +6,20 @@ import {
|
|||
SplitItem,
|
||||
TextInput,
|
||||
TextInputProps,
|
||||
ToggleMenuBaseProps,
|
||||
} from "@patternfly/react-core";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
export type Unit = "seconds" | "minutes" | "hours" | "days";
|
||||
|
||||
export type TimeSelectorProps = TextInputProps & {
|
||||
value: number;
|
||||
units?: Unit[];
|
||||
onChange: (time: number | string) => void;
|
||||
className?: string;
|
||||
};
|
||||
export type TimeSelectorProps = TextInputProps &
|
||||
ToggleMenuBaseProps & {
|
||||
value: number;
|
||||
units?: Unit[];
|
||||
onChange: (time: number | string) => void;
|
||||
className?: string;
|
||||
};
|
||||
|
||||
export const TimeSelector = ({
|
||||
value,
|
||||
|
@ -25,6 +27,7 @@ export const TimeSelector = ({
|
|||
onChange,
|
||||
className,
|
||||
min,
|
||||
menuAppendTo,
|
||||
...rest
|
||||
}: TimeSelectorProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
|
@ -102,6 +105,7 @@ export const TimeSelector = ({
|
|||
updateTimeout(timeValue, value as number);
|
||||
setOpen(false);
|
||||
}}
|
||||
menuAppendTo={menuAppendTo}
|
||||
selections={[multiplier]}
|
||||
onToggle={() => {
|
||||
setOpen(!open);
|
||||
|
|
|
@ -12,6 +12,9 @@ import {
|
|||
KebabToggle,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Switch,
|
||||
Text,
|
||||
TextInput,
|
||||
|
@ -33,17 +36,20 @@ import {
|
|||
import { PencilAltIcon, CheckIcon, TimesIcon } from "@patternfly/react-icons";
|
||||
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { isEmpty } from "lodash/fp";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
|
||||
import { useWhoAmI } from "../context/whoami/WhoAmI";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { Controller, useForm, UseFormMethods, useWatch } from "react-hook-form";
|
||||
import { PasswordInput } from "../components/password-input/PasswordInput";
|
||||
import { HelpItem } from "../components/help-enabler/HelpItem";
|
||||
import "./user-section.css";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import type CredentialRepresentation from "@keycloak/keycloak-admin-client/lib/defs/credentialRepresentation";
|
||||
import { FormAccess } from "../components/form-access/FormAccess";
|
||||
import { RequiredActionAlias } from "@keycloak/keycloak-admin-client/lib/defs/requiredActionProviderRepresentation";
|
||||
import { TimeSelector } from "../components/time-selector/TimeSelector";
|
||||
|
||||
type UserCredentialsProps = {
|
||||
user: UserRepresentation;
|
||||
|
@ -55,12 +61,22 @@ type CredentialsForm = {
|
|||
temporaryPassword: boolean;
|
||||
};
|
||||
|
||||
type CredentialResetForm = {
|
||||
actions: RequiredActionAlias[];
|
||||
lifespan: number;
|
||||
};
|
||||
|
||||
const credFormDefaultValues: CredentialsForm = {
|
||||
password: "",
|
||||
passwordConfirmation: "",
|
||||
temporaryPassword: true,
|
||||
};
|
||||
|
||||
const credResetFormDefaultValues: CredentialResetForm = {
|
||||
actions: [],
|
||||
lifespan: 43200, // 12 hours
|
||||
};
|
||||
|
||||
type DisplayDialogProps = {
|
||||
titleKey: string;
|
||||
onClose: () => void;
|
||||
|
@ -92,6 +108,102 @@ const DisplayDialog: FunctionComponent<DisplayDialogProps> = ({
|
|||
);
|
||||
};
|
||||
|
||||
const CredentialsResetActionMultiSelect = (props: {
|
||||
form: UseFormMethods<CredentialResetForm>;
|
||||
}) => {
|
||||
const { t } = useTranslation("users");
|
||||
const [open, setOpen] = useState(false);
|
||||
const { form } = props;
|
||||
const { control } = form;
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t("resetActions")}
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:resetActions"
|
||||
fieldLabelId="resetActions"
|
||||
/>
|
||||
}
|
||||
fieldId="actions"
|
||||
>
|
||||
<Controller
|
||||
name="actions"
|
||||
defaultValue={[]}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="actions"
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
chipGroupProps={{
|
||||
numChips: 3,
|
||||
}}
|
||||
menuAppendTo="parent"
|
||||
onToggle={(open) => setOpen(open)}
|
||||
isOpen={open}
|
||||
selections={value.map((o: string) => o)}
|
||||
onSelect={(_, selectedValue) =>
|
||||
onChange(
|
||||
value.find((o: string) => o === selectedValue)
|
||||
? value.filter((item: string) => item !== selectedValue)
|
||||
: [...value, selectedValue]
|
||||
)
|
||||
}
|
||||
onClear={(event) => {
|
||||
event.stopPropagation();
|
||||
onChange([]);
|
||||
}}
|
||||
aria-label={t("resetActions")}
|
||||
>
|
||||
{Object.values(RequiredActionAlias).map((action, index) => (
|
||||
<SelectOption
|
||||
key={index}
|
||||
value={action}
|
||||
data-testid={`${action}-option`}
|
||||
>
|
||||
{t(action)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
const LifespanField = ({
|
||||
form: { control },
|
||||
}: {
|
||||
form: UseFormMethods<CredentialResetForm>;
|
||||
}) => {
|
||||
const { t } = useTranslation("users");
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
fieldId="lifespan"
|
||||
label={t("lifespan")}
|
||||
isStack
|
||||
labelIcon={
|
||||
<HelpItem helpText="clients-help:lifespan" fieldLabelId="lifespan" />
|
||||
}
|
||||
>
|
||||
<Controller
|
||||
name="lifespan"
|
||||
defaultValue={credResetFormDefaultValues.lifespan}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector
|
||||
value={value}
|
||||
units={["minutes", "hours", "days"]}
|
||||
onChange={onChange}
|
||||
menuAppendTo="parent"
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
||||
|
||||
export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
||||
const { t } = useTranslation("users");
|
||||
const { whoAmI } = useWhoAmI();
|
||||
|
@ -100,6 +212,7 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
const refresh = () => setKey(key + 1);
|
||||
const [open, setOpen] = useState(false);
|
||||
const [openSaveConfirm, setOpenSaveConfirm] = useState(false);
|
||||
const [openCredentialReset, setOpenCredentialReset] = useState(false);
|
||||
const [kebabOpen, setKebabOpen] = useState({
|
||||
status: false,
|
||||
rowKey: "",
|
||||
|
@ -108,16 +221,23 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
const form = useForm<CredentialsForm>({
|
||||
defaultValues: credFormDefaultValues,
|
||||
});
|
||||
const resetForm = useForm<CredentialResetForm>({
|
||||
defaultValues: credResetFormDefaultValues,
|
||||
});
|
||||
const userLabelForm = useForm<UserLabelForm>({
|
||||
defaultValues: userLabelDefaultValues,
|
||||
});
|
||||
const { control, errors, handleSubmit, register } = form;
|
||||
const { control: resetControl, handleSubmit: resetHandleSubmit } = resetForm;
|
||||
const {
|
||||
getValues: getValues1,
|
||||
handleSubmit: handleSubmit1,
|
||||
register: register1,
|
||||
} = userLabelForm;
|
||||
const [credentials, setCredentials] = useState<CredentialsForm>();
|
||||
const [credentialsReset, setCredentialReset] = useState<CredentialResetForm>(
|
||||
{} as CredentialResetForm
|
||||
);
|
||||
const [userCredentials, setUserCredentials] = useState<
|
||||
CredentialRepresentation[]
|
||||
>([]);
|
||||
|
@ -149,6 +269,11 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
name: "password",
|
||||
});
|
||||
|
||||
const resetActionWatcher = useWatch<CredentialResetForm["actions"]>({
|
||||
control: resetControl,
|
||||
name: "actions",
|
||||
});
|
||||
|
||||
const passwordConfirmationWatcher = useWatch<
|
||||
CredentialsForm["passwordConfirmation"]
|
||||
>({
|
||||
|
@ -159,10 +284,16 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
const isNotDisabled =
|
||||
passwordWatcher !== "" && passwordConfirmationWatcher !== "";
|
||||
|
||||
const resetIsNotDisabled = !isEmpty(resetActionWatcher);
|
||||
|
||||
const toggleModal = () => {
|
||||
setOpen(!open);
|
||||
};
|
||||
|
||||
const toggleCredentialsResetModal = () => {
|
||||
setOpenCredentialReset(!openCredentialReset);
|
||||
};
|
||||
|
||||
const toggleConfirmSaveModal = () => {
|
||||
setOpenSaveConfirm(!openSaveConfirm);
|
||||
};
|
||||
|
@ -212,6 +343,25 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
}
|
||||
};
|
||||
|
||||
const sendCredentialsResetEmail = async () => {
|
||||
if (isEmpty(credentialsReset.actions)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await adminClient.users.executeActionsEmail({
|
||||
id: user.id!,
|
||||
actions: credentialsReset.actions,
|
||||
lifespan: credentialsReset.lifespan,
|
||||
});
|
||||
refresh();
|
||||
addAlert(t("credentialResetEmailSuccess"), AlertVariant.success);
|
||||
setOpenCredentialReset(false);
|
||||
} catch (error) {
|
||||
addError(t("credentialResetEmailError"), error);
|
||||
}
|
||||
};
|
||||
|
||||
const resetPassword = () => {
|
||||
setIsResetPassword(true);
|
||||
setOpen(true);
|
||||
|
@ -387,7 +537,6 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
}
|
||||
fieldId="kc-temporaryPassword"
|
||||
>
|
||||
{" "}
|
||||
<Controller
|
||||
name="temporaryPassword"
|
||||
defaultValue={true}
|
||||
|
@ -453,6 +602,48 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
</Text>
|
||||
</Modal>
|
||||
)}
|
||||
{openCredentialReset && (
|
||||
<Modal
|
||||
variant={ModalVariant.medium}
|
||||
title={t("credentialReset")}
|
||||
isOpen
|
||||
onClose={() => {
|
||||
setOpenCredentialReset(false);
|
||||
}}
|
||||
data-testid="credential-reset-modal"
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="okBtn"
|
||||
key={`confirmBtn-${user.id}`}
|
||||
variant="primary"
|
||||
form="userCredentialsReset-form"
|
||||
onClick={() => {
|
||||
setCredentialReset(resetForm.getValues());
|
||||
resetHandleSubmit(sendCredentialsResetEmail)();
|
||||
}}
|
||||
isDisabled={!resetIsNotDisabled}
|
||||
>
|
||||
{t("credentialResetConfirm")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="cancelBtn"
|
||||
key={`cancelBtn-${user.id}`}
|
||||
variant="link"
|
||||
form="userCredentialsReset-form"
|
||||
onClick={() => {
|
||||
setOpenCredentialReset(false);
|
||||
}}
|
||||
>
|
||||
{t("cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Form id="userCredentialsReset-form" isHorizontal>
|
||||
<CredentialsResetActionMultiSelect form={resetForm} />
|
||||
<LifespanField form={resetForm} />
|
||||
</Form>
|
||||
</Modal>
|
||||
)}
|
||||
<DeleteConfirm />
|
||||
{showData && Object.keys(selectedCredential).length !== 0 && (
|
||||
<DisplayDialog
|
||||
|
@ -492,178 +683,190 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
</>
|
||||
)}
|
||||
{userCredentials.length !== 0 ? (
|
||||
<TableComposable aria-label="password-data-table" variant={"compact"}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<HelpItem
|
||||
helpText="users:userCredentialsHelpText"
|
||||
fieldLabelId="users:userCredentialsHelpTextLabel"
|
||||
/>
|
||||
</Th>
|
||||
<Th>{t("type")}</Th>
|
||||
<Th>{t("userLabel")}</Th>
|
||||
<Th>{t("data")}</Th>
|
||||
<Th />
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{userCredentials.map((credential) => (
|
||||
<Tr key={`table-${credential.id}`}>
|
||||
<>
|
||||
<Td
|
||||
draggableRow={{
|
||||
id: `draggable-row-${credential.id}`,
|
||||
}}
|
||||
<>
|
||||
{user.email && (
|
||||
<Button
|
||||
className="resetCredentialBtn-header"
|
||||
variant="primary"
|
||||
data-testid="credentialResetBtn"
|
||||
onClick={() => setOpenCredentialReset(true)}
|
||||
>
|
||||
{t("credentialResetBtn")}
|
||||
</Button>
|
||||
)}
|
||||
<TableComposable aria-label="password-data-table" variant={"compact"}>
|
||||
<Thead>
|
||||
<Tr>
|
||||
<Th>
|
||||
<HelpItem
|
||||
helpText="users:userCredentialsHelpText"
|
||||
fieldLabelId="users:userCredentialsHelpTextLabel"
|
||||
/>
|
||||
<Td
|
||||
key={`table-item-${credential.id}`}
|
||||
dataLabel={`columns-${credential.id}`}
|
||||
>
|
||||
{credential.type?.charAt(0).toUpperCase()! +
|
||||
credential.type?.slice(1)}
|
||||
</Td>
|
||||
<Td>
|
||||
<FormAccess isHorizontal role="view-users">
|
||||
<FormGroup
|
||||
fieldId="kc-userLabel"
|
||||
className="kc-userLabel-row"
|
||||
>
|
||||
<div className="kc-form-group-userLabel">
|
||||
{isUserLabelEdit?.status &&
|
||||
isUserLabelEdit.rowKey === credential.id ? (
|
||||
<>
|
||||
<TextInput
|
||||
name="userLabel"
|
||||
ref={register1()}
|
||||
type="text"
|
||||
className="kc-userLabel"
|
||||
aria-label={t("userLabel")}
|
||||
data-testid="user-label-fld"
|
||||
/>
|
||||
<div className="kc-userLabel-actionBtns">
|
||||
</Th>
|
||||
<Th>{t("type")}</Th>
|
||||
<Th>{t("userLabel")}</Th>
|
||||
<Th>{t("data")}</Th>
|
||||
<Th />
|
||||
<Th />
|
||||
</Tr>
|
||||
</Thead>
|
||||
<Tbody>
|
||||
{userCredentials.map((credential) => (
|
||||
<Tr key={`table-${credential.id}`}>
|
||||
<>
|
||||
<Td
|
||||
draggableRow={{
|
||||
id: `draggable-row-${credential.id}`,
|
||||
}}
|
||||
/>
|
||||
<Td
|
||||
key={`table-item-${credential.id}`}
|
||||
dataLabel={`columns-${credential.id}`}
|
||||
>
|
||||
{credential.type?.charAt(0).toUpperCase()! +
|
||||
credential.type?.slice(1)}
|
||||
</Td>
|
||||
<Td>
|
||||
<FormAccess isHorizontal role="view-users">
|
||||
<FormGroup
|
||||
fieldId="kc-userLabel"
|
||||
className="kc-userLabel-row"
|
||||
>
|
||||
<div className="kc-form-group-userLabel">
|
||||
{isUserLabelEdit?.status &&
|
||||
isUserLabelEdit.rowKey === credential.id ? (
|
||||
<>
|
||||
<TextInput
|
||||
name="userLabel"
|
||||
ref={register1()}
|
||||
type="text"
|
||||
className="kc-userLabel"
|
||||
aria-label={t("userLabel")}
|
||||
data-testid="user-label-fld"
|
||||
/>
|
||||
<div className="kc-userLabel-actionBtns">
|
||||
<Button
|
||||
key={`editUserLabel-accept-${credential.id}`}
|
||||
variant="link"
|
||||
className="kc-editUserLabel-acceptBtn"
|
||||
onClick={() => {
|
||||
handleSubmit1(saveUserLabel)();
|
||||
setIsUserLabelEdit({
|
||||
status: false,
|
||||
rowKey: credential.id!,
|
||||
});
|
||||
}}
|
||||
data-testid="editUserLabel-acceptBtn"
|
||||
icon={<CheckIcon />}
|
||||
/>
|
||||
<Button
|
||||
key={`editUserLabel-cancel-${credential.id}`}
|
||||
variant="link"
|
||||
className="kc-editUserLabel-cancelBtn"
|
||||
onClick={() =>
|
||||
setIsUserLabelEdit({
|
||||
status: false,
|
||||
rowKey: credential.id!,
|
||||
})
|
||||
}
|
||||
data-testid="editUserLabel-cancelBtn"
|
||||
icon={<TimesIcon />}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{credential.userLabel ?? ""}
|
||||
<Button
|
||||
key={`editUserLabel-accept-${credential.id}`}
|
||||
key={`editUserLabel-${credential.id}`}
|
||||
variant="link"
|
||||
className="kc-editUserLabel-acceptBtn"
|
||||
className="kc-editUserLabel-btn"
|
||||
onClick={() => {
|
||||
handleSubmit1(saveUserLabel)();
|
||||
setEditedUserCredential(credential);
|
||||
setIsUserLabelEdit({
|
||||
status: false,
|
||||
status: true,
|
||||
rowKey: credential.id!,
|
||||
});
|
||||
}}
|
||||
data-testid="editUserLabel-acceptBtn"
|
||||
icon={<CheckIcon />}
|
||||
data-testid="editUserLabelBtn"
|
||||
icon={<PencilAltIcon />}
|
||||
/>
|
||||
<Button
|
||||
key={`editUserLabel-cancel-${credential.id}`}
|
||||
variant="link"
|
||||
className="kc-editUserLabel-cancelBtn"
|
||||
onClick={() =>
|
||||
setIsUserLabelEdit({
|
||||
status: false,
|
||||
rowKey: credential.id!,
|
||||
})
|
||||
}
|
||||
data-testid="editUserLabel-cancelBtn"
|
||||
icon={<TimesIcon />}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{credential.userLabel ?? ""}
|
||||
<Button
|
||||
key={`editUserLabel-${credential.id}`}
|
||||
variant="link"
|
||||
className="kc-editUserLabel-btn"
|
||||
onClick={() => {
|
||||
setEditedUserCredential(credential);
|
||||
setIsUserLabelEdit({
|
||||
status: true,
|
||||
rowKey: credential.id!,
|
||||
});
|
||||
}}
|
||||
data-testid="editUserLabelBtn"
|
||||
icon={<PencilAltIcon />}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</FormGroup>
|
||||
</FormAccess>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
className="kc-showData-btn"
|
||||
variant="link"
|
||||
data-testid="showDataBtn"
|
||||
onClick={() => {
|
||||
setShowData(true);
|
||||
setSelectedCredential(credential);
|
||||
}}
|
||||
>
|
||||
{t("showDataBtn")}
|
||||
</Button>
|
||||
</Td>
|
||||
{credential.type === "password" ? (
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</FormGroup>
|
||||
</FormAccess>
|
||||
</Td>
|
||||
<Td>
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-testid="resetPasswordBtn"
|
||||
onClick={resetPassword}
|
||||
className="kc-showData-btn"
|
||||
variant="link"
|
||||
data-testid="showDataBtn"
|
||||
onClick={() => {
|
||||
setShowData(true);
|
||||
setSelectedCredential(credential);
|
||||
}}
|
||||
>
|
||||
{t("resetPasswordBtn")}
|
||||
{t("showDataBtn")}
|
||||
</Button>
|
||||
</Td>
|
||||
) : (
|
||||
<Td />
|
||||
)}
|
||||
<Td>
|
||||
<Dropdown
|
||||
isPlain
|
||||
position={DropdownPosition.right}
|
||||
toggle={
|
||||
<KebabToggle
|
||||
onToggle={(status) =>
|
||||
setKebabOpen({
|
||||
status,
|
||||
rowKey: credential.id!,
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
isOpen={
|
||||
kebabOpen.status && kebabOpen.rowKey === credential.id
|
||||
}
|
||||
onSelect={() => {
|
||||
setSelectedCredential(credential);
|
||||
}}
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key={`delete-dropdown-item-${credential.id}`}
|
||||
data-testid="deleteDropdownItem"
|
||||
component="button"
|
||||
onClick={() => {
|
||||
toggleDeleteDialog();
|
||||
setKebabOpen({
|
||||
status: false,
|
||||
rowKey: credential.id!,
|
||||
});
|
||||
}}
|
||||
{credential.type === "password" ? (
|
||||
<Td>
|
||||
<Button
|
||||
variant="secondary"
|
||||
data-testid="resetPasswordBtn"
|
||||
onClick={resetPassword}
|
||||
>
|
||||
{t("deleteBtn")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
</Td>
|
||||
</>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
{t("resetPasswordBtn")}
|
||||
</Button>
|
||||
</Td>
|
||||
) : (
|
||||
<Td />
|
||||
)}
|
||||
<Td>
|
||||
<Dropdown
|
||||
isPlain
|
||||
position={DropdownPosition.right}
|
||||
toggle={
|
||||
<KebabToggle
|
||||
onToggle={(status) =>
|
||||
setKebabOpen({
|
||||
status,
|
||||
rowKey: credential.id!,
|
||||
})
|
||||
}
|
||||
/>
|
||||
}
|
||||
isOpen={
|
||||
kebabOpen.status && kebabOpen.rowKey === credential.id
|
||||
}
|
||||
onSelect={() => {
|
||||
setSelectedCredential(credential);
|
||||
}}
|
||||
dropdownItems={[
|
||||
<DropdownItem
|
||||
key={`delete-dropdown-item-${credential.id}`}
|
||||
data-testid="deleteDropdownItem"
|
||||
component="button"
|
||||
onClick={() => {
|
||||
toggleDeleteDialog();
|
||||
setKebabOpen({
|
||||
status: false,
|
||||
rowKey: credential.id!,
|
||||
});
|
||||
}}
|
||||
>
|
||||
{t("deleteBtn")}
|
||||
</DropdownItem>,
|
||||
]}
|
||||
/>
|
||||
</Td>
|
||||
</>
|
||||
</Tr>
|
||||
))}
|
||||
</Tbody>
|
||||
</TableComposable>
|
||||
</>
|
||||
) : (
|
||||
<ListEmptyState
|
||||
hasIcon={true}
|
||||
|
@ -671,6 +874,17 @@ export const UserCredentials = ({ user }: UserCredentialsProps) => {
|
|||
instructions={t("noCredentialsText")}
|
||||
primaryActionText={t("setPassword")}
|
||||
onPrimaryAction={toggleModal}
|
||||
secondaryActions={
|
||||
user.email
|
||||
? [
|
||||
{
|
||||
text: t("credentialResetBtn"),
|
||||
onClick: toggleCredentialsResetModal,
|
||||
type: ButtonVariant.link,
|
||||
},
|
||||
]
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
|
|
|
@ -169,5 +169,21 @@ export default {
|
|||
updateCredentialUserLabelSuccess:
|
||||
"The user label has been changed successfully.",
|
||||
updateCredentialUserLabelError: "Error changing user label: {{error}}",
|
||||
credentialReset: "Credentials Reset",
|
||||
credentialResetBtn: "Credential Reset",
|
||||
resetActions: "Reset Actions",
|
||||
lifespan: "Expires In",
|
||||
VERIFY_EMAIL: "Verify Email (VERIFY_EMAIL)",
|
||||
UPDATE_PASSWORD: "Update password (UPDATE_PASSWORD)",
|
||||
UPDATE_PROFILE: "Update Profile (UPDATE_PROFILE)",
|
||||
CONFIGURE_TOTP: "Configure OTP (CONFIGURE_TOTP)",
|
||||
terms_and_conditions: "Terms and Conditions (terms_and_conditions)",
|
||||
hours: "Hours",
|
||||
minutes: "Minutes",
|
||||
seconds: "Seconds",
|
||||
credentialResetConfirm: "Send Email",
|
||||
credentialResetConfirmText: "Are you sure you want to send email to user",
|
||||
credentialResetEmailSuccess: "Email sent to user.",
|
||||
credentialResetEmailError: "Failed to send email to user.",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -182,3 +182,8 @@ article.pf-c-card.pf-m-flat.kc-available-idps > div > div > h1 {
|
|||
.setPasswordBtn-table {
|
||||
margin: 25px 0 25px 25px;
|
||||
}
|
||||
|
||||
.resetCredentialBtn-header {
|
||||
margin: 10px 25px 10px 0;
|
||||
float: right;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue