Use react-form-hook v7 for user IDP modal (#3771)

This commit is contained in:
Jon Koops 2022-11-15 16:21:43 +01:00 committed by GitHub
parent 27d8b35d70
commit d0b224cae7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 89 additions and 116 deletions

View file

@ -11,7 +11,6 @@ export default class IdentityProviderLinksTab {
private linkAccountModalIdentityProviderInput = "idpNameInput"; private linkAccountModalIdentityProviderInput = "idpNameInput";
private linkAccountModalUserIdInput = "userIdInput"; private linkAccountModalUserIdInput = "userIdInput";
private linkAccountModalUsernameInput = "usernameInput"; private linkAccountModalUsernameInput = "usernameInput";
private linkAccountModalLinkBtn = "Link";
public clickLinkAccount(idpName: string) { public clickLinkAccount(idpName: string) {
cy.get(this.availableProvidersSection + " tr") cy.get(this.availableProvidersSection + " tr")
@ -47,7 +46,7 @@ export default class IdentityProviderLinksTab {
} }
public clickLinkAccountModalLinkBtn() { public clickLinkAccountModalLinkBtn() {
cy.findByTestId(this.linkAccountModalLinkBtn).click(); modalUtils.confirmModal();
cy.intercept("/admin/realms/master").as("load"); cy.intercept("/admin/realms/master").as("load");
cy.wait(["@load"]); cy.wait(["@load"]);
return this; return this;

View file

@ -1,3 +1,4 @@
import type FederatedIdentityRepresentation from "@keycloak/keycloak-admin-client/lib/defs/federatedIdentityRepresentation";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -8,28 +9,26 @@ import {
ModalVariant, ModalVariant,
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import { useAdminClient } from "../context/auth/AdminClient";
import { useAlerts } from "../components/alert/Alerts";
import { capitalize } from "lodash-es"; import { capitalize } from "lodash-es";
import { useParams } from "react-router-dom"; import { useForm } from "react-hook-form-v7";
import type FederatedIdentityRepresentation from "@keycloak/keycloak-admin-client/lib/defs/federatedIdentityRepresentation"; import { useTranslation } from "react-i18next";
import type { UserParams } from "./routes/User";
import { useAlerts } from "../components/alert/Alerts";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput"; import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import { useAdminClient } from "../context/auth/AdminClient";
type UserIdpModalProps = { type UserIdpModalProps = {
federatedId?: string; userId: string;
handleModalToggle: () => void; federatedId: string;
refresh: (group?: GroupRepresentation) => void; onClose: () => void;
onRefresh: () => void;
}; };
export const UserIdpModal = ({ export const UserIdpModal = ({
userId,
federatedId, federatedId,
handleModalToggle, onClose,
refresh, onRefresh,
}: UserIdpModalProps) => { }: UserIdpModalProps) => {
const { t } = useTranslation("users"); const { t } = useTranslation("users");
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
@ -38,22 +37,22 @@ export const UserIdpModal = ({
register, register,
handleSubmit, handleSubmit,
formState: { isValid, errors }, formState: { isValid, errors },
} = useForm({ } = useForm<FederatedIdentityRepresentation>({
mode: "onChange", mode: "onChange",
}); });
const { id } = useParams<UserParams>(); const onSubmit = async (
federatedIdentity: FederatedIdentityRepresentation
const submitForm = async (fedIdentity: FederatedIdentityRepresentation) => { ) => {
try { try {
await adminClient.users.addToFederatedIdentity({ await adminClient.users.addToFederatedIdentity({
id: id!, id: userId,
federatedIdentityId: federatedId!, federatedIdentityId: federatedId,
federatedIdentity: fedIdentity, federatedIdentity,
}); });
addAlert(t("users:idpLinkSuccess"), AlertVariant.success); addAlert(t("users:idpLinkSuccess"), AlertVariant.success);
handleModalToggle(); onClose();
refresh(); onRefresh();
} catch (error) { } catch (error) {
addError("users:couldNotLinkIdP", error); addError("users:couldNotLinkIdP", error);
} }
@ -65,12 +64,11 @@ export const UserIdpModal = ({
title={t("users:linkAccountTitle", { title={t("users:linkAccountTitle", {
provider: capitalize(federatedId), provider: capitalize(federatedId),
})} })}
isOpen={true} onClose={onClose}
onClose={handleModalToggle}
actions={[ actions={[
<Button <Button
data-testid={t("link")}
key="confirm" key="confirm"
data-testid="confirm"
variant="primary" variant="primary"
type="submit" type="submit"
form="group-form" form="group-form"
@ -79,51 +77,31 @@ export const UserIdpModal = ({
{t("link")} {t("link")}
</Button>, </Button>,
<Button <Button
id="modal-cancel"
data-testid="cancel"
key="cancel" key="cancel"
data-testid="cancel"
variant={ButtonVariant.link} variant={ButtonVariant.link}
onClick={() => { onClick={onClose}
handleModalToggle();
}}
> >
{t("common:cancel")} {t("common:cancel")}
</Button>, </Button>,
]} ]}
isOpen
> >
<Form id="group-form" onSubmit={handleSubmit(submitForm)}> <Form id="group-form" onSubmit={handleSubmit(onSubmit)}>
<FormGroup <FormGroup
name="idp-name-group"
label={t("users:identityProvider")} label={t("users:identityProvider")}
fieldId="idp-name" fieldId="identityProvider"
helperTextInvalid={t("common:required")}
validated={
errors.identityProvider
? ValidatedOptions.error
: ValidatedOptions.default
}
> >
<KeycloakTextInput <KeycloakTextInput
id="identityProvider"
data-testid="idpNameInput" data-testid="idpNameInput"
aria-label="Identity provider name input"
ref={register({ required: true })}
autoFocus
isReadOnly
type="text"
id="link-idp-name"
name="identityProvider"
value={capitalize(federatedId)} value={capitalize(federatedId)}
validated={ isReadOnly
errors.identityProvider
? ValidatedOptions.error
: ValidatedOptions.default
}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
name="user-id-group"
label={t("users:userID")} label={t("users:userID")}
fieldId="user-id" fieldId="userID"
helperText={t("users-help:userIdHelperText")} helperText={t("users-help:userIdHelperText")}
helperTextInvalid={t("common:required")} helperTextInvalid={t("common:required")}
validated={ validated={
@ -132,40 +110,34 @@ export const UserIdpModal = ({
isRequired isRequired
> >
<KeycloakTextInput <KeycloakTextInput
id="userID"
data-testid="userIdInput" data-testid="userIdInput"
aria-label="user ID input"
ref={register({ required: true })}
autoFocus
type="text"
id="link-idp-user-id"
name="userId"
validated={ validated={
errors.userId ? ValidatedOptions.error : ValidatedOptions.default errors.userId ? ValidatedOptions.error : ValidatedOptions.default
} }
autoFocus
{...register("userId", { required: true })}
/> />
</FormGroup> </FormGroup>
<FormGroup <FormGroup
name="username-group"
label={t("users:username")} label={t("users:username")}
fieldId="username" fieldId="username"
helperText={t("users-help:usernameHelperText")} helperText={t("users-help:usernameHelperText")}
helperTextInvalid={t("common:required")} helperTextInvalid={t("common:required")}
validated={ validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default errors.userName ? ValidatedOptions.error : ValidatedOptions.default
} }
isRequired isRequired
> >
<KeycloakTextInput <KeycloakTextInput
id="username"
data-testid="usernameInput" data-testid="usernameInput"
aria-label="username input"
ref={register({ required: true })}
autoFocus
type="text"
id="link-idp-username"
name="userName"
validated={ validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default errors.userName
? ValidatedOptions.error
: ValidatedOptions.default
} }
{...register("userName", { required: true })}
/> />
</FormGroup> </FormGroup>
</Form> </Form>

View file

@ -1,5 +1,5 @@
import { useState } from "react"; import type FederatedIdentityRepresentation from "@keycloak/keycloak-admin-client/lib/defs/federatedIdentityRepresentation";
import { useTranslation } from "react-i18next"; import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -9,40 +9,41 @@ import {
Text, Text,
TextContent, TextContent,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { cellWidth } from "@patternfly/react-table";
import { capitalize } from "lodash-es";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link } from "react-router-dom-v5-compat";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { FormPanel } from "../components/scroll-form/FormPanel"; import { FormPanel } from "../components/scroll-form/FormPanel";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { cellWidth } from "@patternfly/react-table";
import { useParams } from "react-router-dom";
import { Link } from "react-router-dom-v5-compat";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient } from "../context/auth/AdminClient";
import { emptyFormatter, upperCaseFormatter } from "../util";
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import type FederatedIdentityRepresentation from "@keycloak/keycloak-admin-client/lib/defs/federatedIdentityRepresentation";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { capitalize } from "lodash-es";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../components/alert/Alerts";
import { UserIdpModal } from "./UserIdPModal";
import { toIdentityProvider } from "../identity-providers/routes/IdentityProvider"; import { toIdentityProvider } from "../identity-providers/routes/IdentityProvider";
import { emptyFormatter, upperCaseFormatter } from "../util";
import { UserIdpModal } from "./UserIdPModal";
export const UserIdentityProviderLinks = () => { type UserIdentityProviderLinksProps = {
userId: string;
};
export const UserIdentityProviderLinks = ({
userId,
}: UserIdentityProviderLinksProps) => {
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const [federatedId, setFederatedId] = useState(""); const [federatedId, setFederatedId] = useState("");
const [isLinkIdPModalOpen, setIsLinkIdPModalOpen] = useState(false); const [isLinkIdPModalOpen, setIsLinkIdPModalOpen] = useState(false);
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
const { id } = useParams<{ id: string }>();
const { realm } = useRealm(); const { realm } = useRealm();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const { t } = useTranslation("users"); const { t } = useTranslation("users");
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(new Date().getTime());
const handleModalToggle = () => {
setIsLinkIdPModalOpen(!isLinkIdPModalOpen);
};
type WithProviderId = FederatedIdentityRepresentation & { type WithProviderId = FederatedIdentityRepresentation & {
providerId: string; providerId: string;
}; };
@ -53,7 +54,7 @@ export const UserIdentityProviderLinks = () => {
const allProviders = await adminClient.identityProviders.find(); const allProviders = await adminClient.identityProviders.find();
const allFedIds = (await adminClient.users.listFederatedIdentities({ const allFedIds = (await adminClient.users.listFederatedIdentities({
id, id: userId,
})) as WithProviderId[]; })) as WithProviderId[];
for (const element of allFedIds) { for (const element of allFedIds) {
element.providerId = allProviders.find( element.providerId = allProviders.find(
@ -94,7 +95,7 @@ export const UserIdentityProviderLinks = () => {
onConfirm: async () => { onConfirm: async () => {
try { try {
await adminClient.users.delFromFederatedIdentity({ await adminClient.users.delFromFederatedIdentity({
id, id: userId,
federatedIdentityId: federatedId, federatedIdentityId: federatedId,
}); });
addAlert(t("users:idpUnlinkSuccess"), AlertVariant.success); addAlert(t("users:idpUnlinkSuccess"), AlertVariant.success);
@ -180,9 +181,10 @@ export const UserIdentityProviderLinks = () => {
<> <>
{isLinkIdPModalOpen && ( {isLinkIdPModalOpen && (
<UserIdpModal <UserIdpModal
userId={userId}
federatedId={federatedId} federatedId={federatedId}
handleModalToggle={handleModalToggle} onClose={() => setIsLinkIdPModalOpen(false)}
refresh={refresh} onRefresh={refresh}
/> />
)} )}
<UnlinkConfirm /> <UnlinkConfirm />

View file

@ -1,4 +1,5 @@
import { useState } from "react"; import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import { import {
AlertVariant, AlertVariant,
ButtonVariant, ButtonVariant,
@ -7,31 +8,30 @@ import {
Tab, Tab,
TabTitleText, TabTitleText,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useTranslation } from "react-i18next"; import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form"; import { Controller, FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type UserRepresentation from "@keycloak/keycloak-admin-client/lib/defs/userRepresentation";
import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/groupRepresentation";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { BruteForced, UserForm } from "./UserForm";
import { useAlerts } from "../components/alert/Alerts";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useParams } from "react-router-dom"; import { useParams } from "react-router-dom";
import { useNavigate } from "react-router-dom-v5-compat"; import { useNavigate } from "react-router-dom-v5-compat";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { UserGroups } from "./UserGroups"; import { useAlerts } from "../components/alert/Alerts";
import { UserConsents } from "./UserConsents";
import { useRealm } from "../context/realm-context/RealmContext";
import { UserIdentityProviderLinks } from "./UserIdentityProviderLinks";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { toUser } from "./routes/User";
import { toUsers } from "./routes/Users";
import { UserRoleMapping } from "./UserRoleMapping";
import { UserAttributes } from "./UserAttributes";
import { UserCredentials } from "./UserCredentials";
import { UserSessions } from "./UserSessions";
import { useAccess } from "../context/access/Access";
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { useAccess } from "../context/access/Access";
import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import { toUser, UserParams } from "./routes/User";
import { toUsers } from "./routes/Users";
import { UserAttributes } from "./UserAttributes";
import { UserConsents } from "./UserConsents";
import { UserCredentials } from "./UserCredentials";
import { BruteForced, UserForm } from "./UserForm";
import { UserGroups } from "./UserGroups";
import { UserIdentityProviderLinks } from "./UserIdentityProviderLinks";
import { UserRoleMapping } from "./UserRoleMapping";
import { UserSessions } from "./UserSessions";
const UsersTabs = () => { const UsersTabs = () => {
const { t } = useTranslation("users"); const { t } = useTranslation("users");
@ -42,7 +42,7 @@ const UsersTabs = () => {
const { adminClient } = useAdminClient(); const { adminClient } = useAdminClient();
const userForm = useForm<UserRepresentation>({ mode: "onChange" }); const userForm = useForm<UserRepresentation>({ mode: "onChange" });
const { id } = useParams<{ id: string }>(); const { id } = useParams<UserParams>();
const [user, setUser] = useState<UserRepresentation>(); const [user, setUser] = useState<UserRepresentation>();
const [bruteForced, setBruteForced] = useState<BruteForced>(); const [bruteForced, setBruteForced] = useState<BruteForced>();
const [addedGroups, setAddedGroups] = useState<GroupRepresentation[]>([]); const [addedGroups, setAddedGroups] = useState<GroupRepresentation[]>([]);
@ -247,7 +247,7 @@ const UsersTabs = () => {
<TabTitleText>{t("identityProviderLinks")}</TabTitleText> <TabTitleText>{t("identityProviderLinks")}</TabTitleText>
} }
> >
<UserIdentityProviderLinks /> <UserIdentityProviderLinks userId={id} />
</Tab> </Tab>
)} )}
<Tab <Tab