Split out role form for realm and client in seperate components (#4141)

This commit is contained in:
Jon Koops 2023-01-09 13:57:40 +01:00 committed by GitHub
parent 84da38789d
commit b4fef326de
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 287 additions and 223 deletions

View file

@ -243,13 +243,6 @@ describe("Realm roles test", () => {
createRealmRolePage.checkDescription(updateDescription);
});
it("should revert realm role", () => {
listingPage.itemExist(editRoleName).goToItemDetails(editRoleName);
createRealmRolePage.checkDescription(updateDescription);
createRealmRolePage.updateDescription("going to revert").cancel();
createRealmRolePage.checkDescription(updateDescription);
});
const keyValue = new KeyValueInput("attributes");
it("should add attribute", () => {
listingPage.itemExist(editRoleName).goToItemDetails(editRoleName);

View file

@ -93,9 +93,8 @@ export default class ProviderPage {
private mappersTab = "ldap-mappers-tab";
private rolesTab = "rolesTab";
private createRoleBtn = "no-roles-for-this-client-empty-action";
private realmRolesSaveBtn = "realm-roles-save-button";
private roleSaveBtn = "save";
private roleNameField = "#kc-name";
private clientIdSelect = "#client\\.id-select-typeahead";
private groupName = "aa-uf-mappers-group";
private clientName = "aa-uf-mappers-client";
@ -313,7 +312,7 @@ export default class ProviderPage {
cy.wait(1000);
cy.get(this.roleNameField).clear().type(roleName);
cy.wait(1000);
cy.findByTestId(this.realmRolesSaveBtn).click();
cy.findByTestId(this.roleSaveBtn).click();
cy.wait(1000);
}

View file

@ -1,8 +1,8 @@
class CreateRealmRolePage {
private realmRoleNameInput = "#kc-name";
private realmRoleNameError = "#kc-name-helper";
private realmRoleDescriptionInput = "#kc-role-description";
private saveBtn = "realm-roles-save-button";
private realmRoleDescriptionInput = "#kc-description";
private saveBtn = "save";
private cancelBtn = "cancel";
//#region General Settings

View file

@ -0,0 +1,70 @@
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import { AlertVariant } from "@patternfly/react-core";
import { SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate, useParams } from "react-router-dom-v5-compat";
import { useAlerts } from "../../components/alert/Alerts";
import { AttributeForm } from "../../components/key-value-form/AttributeForm";
import { RoleForm } from "../../components/role-form/RoleForm";
import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { toClientRole } from "../../realm-roles/routes/ClientRole";
import { toClient } from "../routes/Client";
import { NewRoleParams } from "../routes/NewRole";
export default function CreateClientRole() {
const { t } = useTranslation("roles");
const form = useForm<AttributeForm>({ mode: "onChange" });
const navigate = useNavigate();
const { clientId } = useParams<NewRoleParams>();
const { adminClient } = useAdminClient();
const { realm } = useRealm();
const { addAlert, addError } = useAlerts();
const onSubmit: SubmitHandler<AttributeForm> = async (formValues) => {
const role: RoleRepresentation = {
...formValues,
name: formValues.name?.trim(),
attributes: {},
};
try {
await adminClient.clients.createRole({
id: clientId,
...role,
});
const createdRole = await adminClient.clients.findRole({
id: clientId!,
roleName: role.name!,
});
addAlert(t("roleCreated"), AlertVariant.success);
navigate(
toClientRole({
realm,
clientId: clientId!,
id: createdRole.id!,
tab: "details",
})
);
} catch (error) {
addError("roles:roleCreateError", error);
}
};
return (
<RoleForm
form={form}
onSubmit={onSubmit}
cancelLink={toClient({
realm,
clientId: clientId!,
tab: "roles",
})}
role="manage-clients"
editMode={false}
/>
);
}

View file

@ -12,6 +12,7 @@ import {
ResourceDetailsRoute,
ResourceDetailsWithResourceIdRoute,
} from "./routes/Resource";
import { NewRoleRoute } from "./routes/NewRole";
import { NewScopeRoute } from "./routes/NewScope";
import {
ScopeDetailsRoute,
@ -44,6 +45,7 @@ const routes: RouteDef[] = [
NewResourceRoute,
ResourceDetailsRoute,
ResourceDetailsWithResourceIdRoute,
NewRoleRoute,
NewScopeRoute,
ScopeDetailsRoute,
ScopeDetailsWithScopeIdRoute,

View file

@ -0,0 +1,17 @@
import { lazy } from "react";
import type { Path } from "react-router-dom-v5-compat";
import { generatePath } from "react-router-dom-v5-compat";
import type { RouteDef } from "../../route-config";
export type NewRoleParams = { realm: string; clientId: string };
export const NewRoleRoute: RouteDef = {
path: "/:realm/clients/:clientId/roles/new",
component: lazy(() => import("../roles/CreateClientRole")),
breadcrumb: (t) => t("roles:createRole"),
access: "manage-clients",
};
export const toCreateRole = (params: NewRoleParams): Partial<Path> => ({
pathname: generatePath(NewRoleRoute.path, params),
});

View file

@ -5,32 +5,32 @@ import {
PageSection,
ValidatedOptions,
} from "@patternfly/react-core";
import type { SubmitHandler, UseFormMethods } from "react-hook-form";
import { useTranslation } from "react-i18next";
import type { UseFormMethods } from "react-hook-form";
import { ViewHeader } from "../components/view-header/ViewHeader";
import { FormAccess } from "../components/form-access/FormAccess";
import type { AttributeForm } from "../components/key-value-form/AttributeForm";
import { KeycloakTextInput } from "../components/keycloak-text-input/KeycloakTextInput";
import { KeycloakTextArea } from "../components/keycloak-text-area/KeycloakTextArea";
import { useRealm } from "../context/realm-context/RealmContext";
import { useNavigate } from "react-router-dom-v5-compat";
import { Link, To } from "react-router-dom-v5-compat";
export type RealmRoleFormProps = {
import { FormAccess } from "../form-access/FormAccess";
import type { AttributeForm } from "../key-value-form/AttributeForm";
import { KeycloakTextArea } from "../keycloak-text-area/KeycloakTextArea";
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
import { ViewHeader } from "../view-header/ViewHeader";
export type RoleFormProps = {
form: UseFormMethods<AttributeForm>;
save: () => void;
onSubmit: SubmitHandler<AttributeForm>;
cancelLink: To;
role: "manage-realm" | "manage-clients";
editMode: boolean;
reset: () => void;
};
export const RealmRoleForm = ({
export const RoleForm = ({
form: { handleSubmit, errors, register, getValues },
save,
onSubmit,
cancelLink,
role,
editMode,
reset,
}: RealmRoleFormProps) => {
}: RoleFormProps) => {
const { t } = useTranslation("roles");
const navigate = useNavigate();
const { realm: realmName } = useRealm();
return (
<>
@ -38,26 +38,27 @@ export const RealmRoleForm = ({
<PageSection variant="light">
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-realm"
onSubmit={handleSubmit(onSubmit)}
role={role}
className="pf-u-mt-lg"
>
<FormGroup
label={t("roleName")}
fieldId="kc-name"
isRequired
validated={errors.name ? "error" : "default"}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
isRequired={!editMode}
>
<KeycloakTextInput
id="kc-name"
name="name"
ref={register({
required: !editMode,
validate: (value: string) =>
!!value.trim() || t("common:required").toString(),
})}
type="text"
id="kc-name"
name="name"
isReadOnly={editMode}
/>
</FormGroup>
@ -72,40 +73,32 @@ export const RealmRoleForm = ({
helperTextInvalid={errors.description?.message}
>
<KeycloakTextArea
id="kc-description"
name="description"
aria-label="description"
isDisabled={getValues().name?.includes("default-roles")}
ref={register({
maxLength: {
value: 255,
message: t("common:maxLength", { length: 255 }),
},
})}
type="text"
validated={
errors.description
? ValidatedOptions.error
: ValidatedOptions.default
}
id="kc-role-description"
isDisabled={getValues().name?.includes("default-roles")}
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
onClick={save}
data-testid="realm-roles-save-button"
>
<Button data-testid="save" type="submit" variant="primary">
{t("common:save")}
</Button>
<Button
data-testid="cancel"
onClick={() =>
editMode ? reset() : navigate(`/${realmName}/roles`)
}
variant="link"
component={(props) => <Link {...props} to={cancelLink} />}
>
{editMode ? t("common:revert") : t("common:cancel")}
{t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>

View file

@ -0,0 +1,57 @@
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import { AlertVariant } from "@patternfly/react-core";
import { SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useNavigate } from "react-router-dom-v5-compat";
import { useAlerts } from "../components/alert/Alerts";
import { AttributeForm } from "../components/key-value-form/AttributeForm";
import { RoleForm } from "../components/role-form/RoleForm";
import { useAdminClient } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import { toRealmRole } from "./routes/RealmRole";
import { toRealmRoles } from "./routes/RealmRoles";
export default function CreateRealmRole() {
const { t } = useTranslation("roles");
const form = useForm<AttributeForm>({ mode: "onChange" });
const navigate = useNavigate();
const { adminClient } = useAdminClient();
const { realm } = useRealm();
const { addAlert, addError } = useAlerts();
const onSubmit: SubmitHandler<AttributeForm> = async (formValues) => {
const role: RoleRepresentation = {
...formValues,
name: formValues.name?.trim(),
attributes: {},
};
try {
await adminClient.roles.create(role);
const createdRole = await adminClient.roles.findOneByName({
name: formValues.name!,
});
if (!createdRole) {
throw new Error(t("common:notFound"));
}
addAlert(t("roleCreated"), AlertVariant.success);
navigate(toRealmRole({ realm, id: createdRole.id!, tab: "details" }));
} catch (error) {
addError("roles:roleCreateError", error);
}
};
return (
<RoleForm
form={form}
onSubmit={onSubmit}
cancelLink={toRealmRoles({ realm })}
role="manage-realm"
editMode={false}
/>
);
}

View file

@ -10,11 +10,12 @@ import {
} from "@patternfly/react-core";
import { omit } from "lodash-es";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { SubmitHandler, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useRouteMatch } from "react-router-dom";
import { useNavigate } from "react-router-dom-v5-compat";
import { toClient } from "../clients/routes/Client";
import { useAlerts } from "../components/alert/Alerts";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import {
@ -28,6 +29,7 @@ import {
import { KeycloakSpinner } from "../components/keycloak-spinner/KeycloakSpinner";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import { PermissionsTab } from "../components/permission-tab/PermissionTab";
import { RoleForm } from "../components/role-form/RoleForm";
import { AddRoleMappingModal } from "../components/role-mapping/AddRoleMappingModal";
import { RoleMapping } from "../components/role-mapping/RoleMapping";
import { ViewHeader } from "../components/view-header/ViewHeader";
@ -35,13 +37,13 @@ import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import { useRealm } from "../context/realm-context/RealmContext";
import { useServerInfo } from "../context/server-info/ServerInfoProvider";
import { useParams } from "../utils/useParams";
import { RealmRoleForm } from "./RealmRoleForm";
import {
ClientRoleParams,
ClientRoleRoute,
toClientRole,
} from "./routes/ClientRole";
import { toRealmRole } from "./routes/RealmRole";
import { toRealmRoles } from "./routes/RealmRoles";
import { UsersInRoleTab } from "./UsersInRoleTab";
export default function RealmRoleTabs() {
@ -49,7 +51,7 @@ export default function RealmRoleTabs() {
const form = useForm<AttributeForm>({
mode: "onChange",
});
const { setValue, getValues, trigger, reset } = form;
const { setValue, reset } = form;
const navigate = useNavigate();
const { adminClient } = useAdminClient();
@ -84,104 +86,67 @@ export default function RealmRoleTabs() {
useFetch(
async () => {
const realm = await adminClient.realms.findOne({ realm: realmName });
if (!id) {
return { realm };
}
const role = await adminClient.roles.findOneById({ id });
const [realm, role] = await Promise.all([
adminClient.realms.findOne({ realm: realmName }),
adminClient.roles.findOneById({ id }),
]);
return { realm, role };
},
({ realm, role }) => {
if (!realm || (!role && id)) {
if (!realm || !role) {
throw new Error(t("common:notFound"));
}
setRealm(realm);
const convertedRole = convert(role);
if (role) {
const convertedRole = convert(role);
setRole(convertedRole);
Object.entries(convertedRole).map((entry) => {
setValue(entry[0], entry[1]);
});
}
setRealm(realm);
setRole(convertedRole);
Object.entries(convertedRole).map((entry) => {
setValue(entry[0], entry[1]);
});
},
[key, url]
);
const save = async () => {
const onSubmit: SubmitHandler<AttributeForm> = async (formValues) => {
try {
const values = getValues();
if (
values.attributes &&
values.attributes[values.attributes.length - 1]?.key === ""
formValues.attributes &&
formValues.attributes[formValues.attributes.length - 1]?.key === ""
) {
setValue(
"attributes",
values.attributes.slice(0, values.attributes.length - 1)
formValues.attributes.slice(0, formValues.attributes.length - 1)
);
}
if (!(await trigger())) {
return;
}
const { attributes, ...rest } = values;
const { attributes, ...rest } = formValues;
let roleRepresentation: RoleRepresentation = rest;
roleRepresentation.name = roleRepresentation.name?.trim();
if (id) {
if (attributes) {
roleRepresentation.attributes = keyValueToArray(attributes);
}
roleRepresentation = {
...omit(role!, "attributes"),
...roleRepresentation,
};
if (!clientId) {
await adminClient.roles.updateById({ id }, roleRepresentation);
} else {
await adminClient.clients.updateRole(
{ id: clientId, roleName: values.name! },
roleRepresentation
);
}
setRole(convert(roleRepresentation));
if (attributes) {
roleRepresentation.attributes = keyValueToArray(attributes);
}
roleRepresentation = {
...omit(role!, "attributes"),
...roleRepresentation,
};
if (!clientId) {
await adminClient.roles.updateById({ id }, roleRepresentation);
} else {
let createdRole;
if (!clientId) {
await adminClient.roles.create(roleRepresentation);
createdRole = await adminClient.roles.findOneByName({
name: values.name!,
});
} else {
await adminClient.clients.createRole({
id: clientId,
name: values.name,
});
if (values.description) {
await adminClient.clients.updateRole(
{ id: clientId, roleName: values.name! },
roleRepresentation
);
}
createdRole = await adminClient.clients.findRole({
id: clientId,
roleName: values.name!,
});
}
if (!createdRole) {
throw new Error(t("common:notFound"));
}
setRole(convert(createdRole));
navigate(
url.substr(0, url.lastIndexOf("/") + 1) + createdRole.id + "/details"
await adminClient.clients.updateRole(
{ id: clientId, roleName: formValues.name! },
roleRepresentation
);
}
addAlert(t(id ? "roleSaveSuccess" : "roleCreated"), AlertVariant.success);
setRole(convert(roleRepresentation));
addAlert(t("roleSaveSuccess"), AlertVariant.success);
} catch (error) {
addError(`roles:${id ? "roleSave" : "roleCreate"}Error`, error);
addError("roles:roleSaveError", error);
}
};
@ -199,7 +164,7 @@ export default function RealmRoleTabs() {
} else {
await adminClient.clients.delRole({
id: clientId,
roleName: role!.name as string,
roleName: role!.name!,
});
}
addAlert(t("roleDeletedSuccess"), AlertVariant.success);
@ -328,19 +293,9 @@ export default function RealmRoleTabs() {
const isDefaultRole = (name: string) => realm?.defaultRole!.name === name;
if (!realm) {
if (!realm || !role) {
return <KeycloakSpinner />;
}
if (!role) {
return (
<RealmRoleForm
reset={() => reset(role)}
form={form}
save={save}
editMode={false}
/>
);
}
return (
<>
@ -356,7 +311,7 @@ export default function RealmRoleTabs() {
/>
)}
<ViewHeader
titleKey={role.name || t("createRole")}
titleKey={role.name!}
badges={[
{
id: "composite-role-badge",
@ -364,73 +319,75 @@ export default function RealmRoleTabs() {
readonly: true,
},
]}
subKey={id ? "" : "roles:roleCreateExplain"}
actionsDropdownId="roles-actions-dropdown"
dropdownItems={dropdownItems}
divider={!id}
divider={false}
/>
<PageSection variant="light" className="pf-u-p-0">
{id && (
<KeycloakTabs isBox mountOnEnter>
<KeycloakTabs isBox mountOnEnter>
<Tab
eventKey="details"
title={<TabTitleText>{t("common:details")}</TabTitleText>}
>
<RoleForm
form={form}
onSubmit={onSubmit}
role={clientRoleRouteMatch ? "manage-clients" : "manage-realm"}
cancelLink={
clientRoleRouteMatch
? toClient({ realm: realmName, clientId, tab: "roles" })
: toRealmRoles({ realm: realmName })
}
editMode={true}
/>
</Tab>
{role.composite && (
<Tab
eventKey="details"
title={<TabTitleText>{t("common:details")}</TabTitleText>}
eventKey="associated-roles"
className="kc-associated-roles-tab"
title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>}
>
<RealmRoleForm
reset={() => reset(role)}
form={form}
save={save}
editMode={true}
<RoleMapping
name={role.name!}
id={role.id!}
type="roles"
isManager
save={(rows) => addComposites(rows.map((r) => r.role))}
/>
</Tab>
{role.composite && (
<Tab
eventKey="associated-roles"
className="kc-associated-roles-tab"
title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>}
>
<RoleMapping
name={role.name!}
id={role.id!}
type="roles"
isManager
save={(rows) => addComposites(rows.map((r) => r.role))}
/>
</Tab>
)}
{!isDefaultRole(role.name!) && (
<Tab
eventKey="attributes"
className="kc-attributes-tab"
title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
>
<AttributesForm
form={form}
save={save}
reset={() => reset(role)}
/>
</Tab>
)}
{!isDefaultRole(role.name!) && (
<Tab
eventKey="users-in-role"
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
>
<UsersInRoleTab data-cy="users-in-role-tab" />
</Tab>
)}
{!profileInfo?.disabledFeatures?.includes(
"ADMIN_FINE_GRAINED_AUTHZ"
) && (
<Tab
eventKey="permissions"
title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
>
<PermissionsTab id={role.id} type="roles" />
</Tab>
)}
</KeycloakTabs>
)}
)}
{!isDefaultRole(role.name!) && (
<Tab
eventKey="attributes"
className="kc-attributes-tab"
title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
>
<AttributesForm
form={form}
save={onSubmit}
reset={() => reset(role)}
/>
</Tab>
)}
{!isDefaultRole(role.name!) && (
<Tab
eventKey="users-in-role"
title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
>
<UsersInRoleTab data-cy="users-in-role-tab" />
</Tab>
)}
{!profileInfo?.disabledFeatures?.includes(
"ADMIN_FINE_GRAINED_AUTHZ"
) && (
<Tab
eventKey="permissions"
title={<TabTitleText>{t("common:permissions")}</TabTitleText>}
>
<PermissionsTab id={role.id} type="roles" />
</Tab>
)}
</KeycloakTabs>
</PageSection>
</>
);

View file

@ -116,7 +116,7 @@ export const RolesList = ({
},
});
const goToCreate = () => navigate(`${url}/add-role`);
const goToCreate = () => navigate(`${url}/new`);
if (!realm) {
return <KeycloakSpinner />;

View file

@ -1,12 +1,10 @@
import type { RouteDef } from "../route-config";
import { AddRoleRoute } from "./routes/AddRole";
import { AddRoleToClientRoute } from "./routes/AddRoleToClient";
import { ClientRoleRoute, ClientRoleRouteWithTab } from "./routes/ClientRole";
import { RealmRoleRoute, RealmRoleRouteWithTab } from "./routes/RealmRole";
import { RealmRolesRoute } from "./routes/RealmRoles";
const routes: RouteDef[] = [
AddRoleToClientRoute,
ClientRoleRoute,
ClientRoleRouteWithTab,
RealmRolesRoute,

View file

@ -6,8 +6,8 @@ import type { RouteDef } from "../../route-config";
export type AddRoleParams = { realm: string };
export const AddRoleRoute: RouteDef = {
path: "/:realm/roles/add-role",
component: lazy(() => import("../RealmRoleTabs")),
path: "/:realm/roles/new",
component: lazy(() => import("../CreateRealmRole")),
breadcrumb: (t) => t("roles:createRole"),
access: "manage-realm",
};

View file

@ -1,22 +0,0 @@
import { lazy } from "react";
import type { Path } from "react-router-dom-v5-compat";
import { generatePath } from "react-router-dom-v5-compat";
import type { RouteDef } from "../../route-config";
export type AddRoleToClientParams = {
realm: string;
clientId: string;
};
export const AddRoleToClientRoute: RouteDef = {
path: "/:realm/clients/:clientId/roles/add-role",
component: lazy(() => import("../RealmRoleTabs")),
breadcrumb: (t) => t("roles:createRole"),
access: "manage-realm",
};
export const toAddRoleToClient = (
params: AddRoleToClientParams
): Partial<Path> => ({
pathname: generatePath(AddRoleToClientRoute.path, params),
});