refactor + use new composite role api (#1399)

* refactor + use new composite role api

* fixed types

* Update src/realm-roles/AssociatedRolesModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/realm-roles/AssociatedRolesModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/realm-roles/RealmRoleTabs.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* code review comments

* removed duplicate type

* route

* Update src/realm-roles/AssociatedRolesModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/realm-roles/routes/RealmRole.ts

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* Update src/realm-roles/AssociatedRolesModal.tsx

Co-authored-by: Jon Koops <jonkoops@gmail.com>

* fix unused import

* fixed test

* fixed merge errors

Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
Erik Jan de Wit 2021-11-01 13:29:52 +01:00 committed by GitHub
parent e22b27b471
commit 9c48bb4d90
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
17 changed files with 347 additions and 409 deletions

View file

@ -12,12 +12,8 @@ export default class AssociatedRolesPage {
addAssociatedRealmRole() { addAssociatedRealmRole() {
cy.findByTestId(this.actionDropdown).last().click(); cy.findByTestId(this.actionDropdown).last().click();
const load = "/auth/admin/realms/master/clients";
cy.intercept(load).as("load");
cy.findByTestId(this.addRolesDropdownItem).click(); cy.findByTestId(this.addRolesDropdownItem).click();
cy.wait(["@load"]);
cy.get(this.checkbox).eq(2).check(); cy.get(this.checkbox).eq(2).check();
cy.findByTestId(this.addAssociatedRolesModalButton).contains("Add").click(); cy.findByTestId(this.addAssociatedRolesModalButton).contains("Add").click();
@ -29,8 +25,6 @@ export default class AssociatedRolesPage {
"Composite" "Composite"
); );
cy.wait(["@load"]);
return this; return this;
} }

View file

@ -60,13 +60,12 @@ const SecuredRoute = ({ route }: SecuredRouteProps) => {
? hasAccess(...route.access) ? hasAccess(...route.access)
: hasAccess(route.access); : hasAccess(route.access);
if (accessAllowed) { if (accessAllowed)
return ( return (
<Suspense fallback={<Spinner />}> <Suspense fallback={<Spinner />}>
<route.component /> <route.component />
</Suspense> </Suspense>
); );
}
return <ForbiddenSection />; return <ForbiddenSection />;
}; };

View file

@ -12,14 +12,15 @@ import {
} from "@patternfly/react-table"; } from "@patternfly/react-table";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons"; import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import type { RoleRepresentation } from "../../model/role-model";
import { FormAccess } from "../form-access/FormAccess"; import { FormAccess } from "../form-access/FormAccess";
import "./attribute-form.css"; import "./attribute-form.css";
export type KeyValueType = { key: string; value: string }; export type KeyValueType = { key: string; value: string };
export type AttributeForm = { export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
attributes: KeyValueType[]; attributes?: KeyValueType[];
}; };
export type AttributesFormProps = { export type AttributesFormProps = {

View file

@ -47,7 +47,7 @@ export const GroupAttributes = () => {
const save = async (attributeForm: AttributeForm) => { const save = async (attributeForm: AttributeForm) => {
try { try {
const group = currentGroup(); const group = currentGroup();
const attributes = arrayToAttributes(attributeForm.attributes); const attributes = arrayToAttributes(attributeForm.attributes!);
await adminClient.groups.update({ id: id! }, { ...group, attributes }); await adminClient.groups.update({ id: id! }, { ...group, attributes });
setSubGroups([ setSubGroups([

View file

@ -21,8 +21,8 @@ import { useRealm } from "../../context/realm-context/RealmContext";
import { HelpItem } from "../../components/help-enabler/HelpItem"; import { HelpItem } from "../../components/help-enabler/HelpItem";
import { ViewHeader } from "../../components/view-header/ViewHeader"; import { ViewHeader } from "../../components/view-header/ViewHeader";
import { import {
AttributeForm,
AttributesForm, AttributesForm,
KeyValueType,
} from "../../components/attribute-form/AttributeForm"; } from "../../components/attribute-form/AttributeForm";
import { FormAccess } from "../../components/form-access/FormAccess"; import { FormAccess } from "../../components/form-access/FormAccess";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
@ -39,9 +39,7 @@ import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { groupBy } from "lodash"; import { groupBy } from "lodash";
export type IdPMapperRepresentationWithAttributes = export type IdPMapperRepresentationWithAttributes =
IdentityProviderMapperRepresentation & { IdentityProviderMapperRepresentation & AttributeForm;
attributes: KeyValueType[];
};
type Role = RoleRepresentation & { type Role = RoleRepresentation & {
clientId?: string; clientId?: string;
@ -86,7 +84,6 @@ export default function AddMapper() {
const [currentMapper, setCurrentMapper] = const [currentMapper, setCurrentMapper] =
useState<IdentityProviderMapperRepresentation>(); useState<IdentityProviderMapperRepresentation>();
const [roles, setRoles] = useState<RoleRepresentation[]>([]);
const [rolesModalOpen, setRolesModalOpen] = useState(false); const [rolesModalOpen, setRolesModalOpen] = useState(false);
@ -144,19 +141,14 @@ export default function AddMapper() {
Promise.all([ Promise.all([
id ? adminClient.identityProviders.findOneMapper({ alias, id }) : null, id ? adminClient.identityProviders.findOneMapper({ alias, id }) : null,
adminClient.identityProviders.findMapperTypes({ alias }), adminClient.identityProviders.findMapperTypes({ alias }),
!id ? adminClient.roles.find() : null,
]), ]),
([mapper, mapperTypes, roles]) => { ([mapper, mapperTypes]) => {
if (mapper) { if (mapper) {
setCurrentMapper(mapper); setCurrentMapper(mapper);
setupForm(mapper); setupForm(mapper);
} }
setMapperTypes(mapperTypes); setMapperTypes(mapperTypes);
if (roles) {
setRoles(roles);
}
}, },
[] []
); );
@ -257,17 +249,15 @@ export default function AddMapper() {
} }
divider divider
/> />
<AssociatedRolesModal {rolesModalOpen && (
onConfirm={(role: Role[]) => { <AssociatedRolesModal
setSelectedRole(role); onConfirm={(role) => setSelectedRole(role)}
}} omitComposites
allRoles={roles} isRadio
open={rolesModalOpen} isMapperId
omitComposites toggleDialog={toggleModal}
isRadio />
isMapperId )}
toggleDialog={toggleModal}
/>
<FormAccess <FormAccess
role="manage-identity-providers" role="manage-identity-providers"
isHorizontal isHorizontal

View file

@ -1,6 +1,7 @@
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useParams } from "react-router-dom"; import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { import {
AlertVariant,
Button, Button,
Dropdown, Dropdown,
DropdownItem, DropdownItem,
@ -8,6 +9,7 @@ import {
Label, Label,
Modal, Modal,
ModalVariant, ModalVariant,
Spinner,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFetch, useAdminClient } from "../context/auth/AdminClient"; import { useFetch, useAdminClient } from "../context/auth/AdminClient";
@ -15,76 +17,74 @@ import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/ro
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons"; import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons";
import _ from "lodash"; import { omit, sortBy } from "lodash";
import type { RealmRoleParams } from "./routes/RealmRole"; import { RealmRoleParams, toRealmRole } from "./routes/RealmRole";
import { useRealm } from "../context/realm-context/RealmContext";
import { useAlerts } from "../components/alert/Alerts";
import {
ClientRoleParams,
ClientRoleRoute,
toClientRole,
} from "./routes/ClientRole";
type Role = RoleRepresentation & { type Role = RoleRepresentation & {
clientId?: string; clientId?: string;
}; };
export type AssociatedRolesModalProps = { export type AssociatedRolesModalProps = {
open: boolean;
toggleDialog: () => void; toggleDialog: () => void;
onConfirm: (newReps: RoleRepresentation[]) => void; onConfirm?: (newReps: RoleRepresentation[]) => void;
existingCompositeRoles?: RoleRepresentation[];
allRoles?: RoleRepresentation[];
omitComposites?: boolean; omitComposites?: boolean;
isRadio?: boolean; isRadio?: boolean;
isMapperId?: boolean; isMapperId?: boolean;
}; };
export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => { type FilterType = "roles" | "clients";
export const AssociatedRolesModal = ({
toggleDialog,
onConfirm,
omitComposites,
isRadio,
isMapperId,
}: AssociatedRolesModalProps) => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
const [name, setName] = useState(""); const [name, setName] = useState("");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const { realm } = useRealm();
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]); const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
const [compositeRoles, setCompositeRoles] = useState<RoleRepresentation[]>();
const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false); const [isFilterDropdownOpen, setIsFilterDropdownOpen] = useState(false);
const [filterType, setFilterType] = useState("roles"); const [filterType, setFilterType] = useState<FilterType>("roles");
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(new Date().getTime());
const { id } = useParams<RealmRoleParams>(); const { id } = useParams<RealmRoleParams>();
const clientRoleRouteMatch = useRouteMatch<ClientRoleParams>(
ClientRoleRoute.path
);
const history = useHistory();
const alphabetize = (rolesList: RoleRepresentation[]) => { const alphabetize = (rolesList: RoleRepresentation[]) => {
return _.sortBy(rolesList, (role) => role.name?.toUpperCase()); return sortBy(rolesList, (role) => role.name?.toUpperCase());
}; };
const loader = async () => { const loader = async (first?: number, max?: number, search?: string) => {
const roles = await adminClient.roles.find(); const params: { [name: string]: string | number } = {
first: first!,
max: max!,
};
if (!props.omitComposites) { const searchParam = search || "";
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles(
{
id,
}
);
const allRoles = [...roles, ...existingAdditionalRoles];
const filterDupes: Role[] = allRoles.filter( if (searchParam) {
(thing, index, self) => params.search = searchParam;
index === self.findIndex((t) => t.name === thing.name)
);
const clients = await adminClient.clients.find();
filterDupes
.filter((role) => role.clientRole)
.map(
(role) =>
(role.clientId = clients.find(
(client) => client.id === role.containerId
)!.clientId!)
);
return alphabetize(filterDupes).filter((role: RoleRepresentation) => {
return (
props.existingCompositeRoles?.find(
(existing: RoleRepresentation) => existing.name === role.name
) === undefined && role.name !== name
);
});
} }
return alphabetize(roles);
return adminClient.roles.find(params);
}; };
const AliasRenderer = ({ id, name, clientId }: Role) => { const AliasRenderer = ({ id, name, clientId }: Role) => {
@ -100,45 +100,44 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
); );
}; };
/* this is still pretty expensive querying all client and then all roles */
const clientRolesLoader = async () => { const clientRolesLoader = async () => {
const clients = await adminClient.clients.find(); const clients = await adminClient.clients.find();
const clientRoles = await Promise.all(
clients.map(async (client) => {
const roles = await adminClient.clients.listRoles({ id: client.id! });
const clientIdArray = clients.map((client) => client.id); return roles.map<Role>((role) => ({
...role,
clientId: client.clientId,
}));
})
);
let rolesList: Role[] = []; return alphabetize(clientRoles.flat());
for (const id of clientIdArray) { };
const clientRolesList = await adminClient.clients.listRoles({
id: id as string,
});
rolesList = [...rolesList, ...clientRolesList];
}
rolesList const addComposites = async (composites: RoleRepresentation[]) => {
.filter((role) => role.clientRole) const compositeArray = composites;
.map(
(role) =>
(role.clientId = clients.find(
(client) => client.id === role.containerId
)!.clientId!)
);
if (!props.omitComposites) { const to = clientRoleRouteMatch
const existingAdditionalRoles = await adminClient.roles.getCompositeRoles( ? toClientRole({ ...clientRoleRouteMatch.params, tab: "AssociateRoles" })
{ : toRealmRole({
realm,
id, id,
} tab: "AssociatedRoles",
});
try {
await adminClient.roles.createComposite(
{ roleId: id, realm },
compositeArray
); );
history.push(to);
return alphabetize(rolesList).filter((role: RoleRepresentation) => { addAlert(t("addAssociatedRolesSuccess"), AlertVariant.success);
return ( } catch (error) {
existingAdditionalRoles.find( addError("roles:addAssociatedRolesError", error);
(existing: RoleRepresentation) => existing.name === role.name
) === undefined && role.name !== name
);
});
} }
return alphabetize(rolesList);
}; };
useEffect(() => { useEffect(() => {
@ -146,11 +145,18 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
}, [filterType]); }, [filterType]);
useFetch( useFetch(
() => async () => {
!props.isMapperId const [role, compositeRoles] = await Promise.all([
? adminClient.roles.findOneById({ id }) !isMapperId ? adminClient.roles.findOneById({ id }) : undefined,
: Promise.resolve(null), !omitComposites ? adminClient.roles.getCompositeRoles({ id }) : [],
(role) => setName(role ? role.name! : t("createRole")), ]);
return { role, compositeRoles };
},
({ role, compositeRoles }) => {
setName(role ? role.name! : t("createRole"));
setCompositeRoles(compositeRoles);
},
[] []
); );
@ -168,12 +174,20 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
setIsFilterDropdownOpen(!isFilterDropdownOpen); setIsFilterDropdownOpen(!isFilterDropdownOpen);
}; };
if (!compositeRoles) {
return (
<div className="pf-u-text-align-center">
<Spinner />
</div>
);
}
return ( return (
<Modal <Modal
data-testid="addAssociatedRole" data-testid="addAssociatedRole"
title={t("roles:associatedRolesModalTitle", { name })} title={t("roles:associatedRolesModalTitle", { name })}
isOpen={props.open} isOpen
onClose={props.toggleDialog} onClose={toggleDialog}
variant={ModalVariant.large} variant={ModalVariant.large}
actions={[ actions={[
<Button <Button
@ -182,8 +196,12 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
variant="primary" variant="primary"
isDisabled={!selectedRows.length} isDisabled={!selectedRows.length}
onClick={() => { onClick={() => {
props.toggleDialog(); toggleDialog();
props.onConfirm(selectedRows); if (onConfirm) {
onConfirm(selectedRows);
} else {
addComposites(selectedRows);
}
}} }}
> >
{t("common:add")} {t("common:add")}
@ -192,7 +210,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
key="cancel" key="cancel"
variant="link" variant="link"
onClick={() => { onClick={() => {
props.toggleDialog(); toggleDialog();
}} }}
> >
{t("common:cancel")} {t("common:cancel")}
@ -204,7 +222,9 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
loader={filterType === "roles" ? loader : clientRolesLoader} loader={filterType === "roles" ? loader : clientRolesLoader}
ariaLabelKey="roles:roleList" ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor" searchPlaceholderKey="roles:searchFor"
isRadio={props.isRadio} isRadio={isRadio}
isPaginated={filterType === "roles"}
isRowDisabled={(r) => compositeRoles.some((o) => o.name === r.name)}
searchTypeComponent={ searchTypeComponent={
<Dropdown <Dropdown
onSelect={() => onFilterDropdownSelect(filterType)} onSelect={() => onFilterDropdownSelect(filterType)}
@ -234,7 +254,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
} }
canSelectAll canSelectAll
onSelect={(rows) => { onSelect={(rows) => {
setSelectedRows([...rows]); setSelectedRows(rows.map((r) => omit(r, "clientId")));
}} }}
columns={[ columns={[
{ {

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
import { useHistory, useParams, useRouteMatch } from "react-router-dom"; import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { import {
@ -18,27 +18,22 @@ import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { emptyFormatter } from "../util"; import { emptyFormatter } from "../util";
import { AssociatedRolesModal } from "./AssociatedRolesModal"; import { AssociatedRolesModal } from "./AssociatedRolesModal";
import { useAdminClient } from "../context/auth/AdminClient"; import { useAdminClient } from "../context/auth/AdminClient";
import type { RoleFormType } from "./RealmRoleTabs"; import type { AttributeForm } from "../components/attribute-form/AttributeForm";
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import _ from "lodash";
type AssociatedRolesTabProps = { type AssociatedRolesTabProps = {
additionalRoles: Role[]; parentRole: AttributeForm;
addComposites: (newReps: RoleRepresentation[]) => void;
parentRole: RoleFormType;
onRemove: (newReps: RoleRepresentation[]) => void;
client?: ClientRepresentation; client?: ClientRepresentation;
refresh: () => void;
}; };
type Role = RoleRepresentation & { type Role = RoleRepresentation & {
clientId?: string; inherited?: string;
}; };
export const AssociatedRolesTab = ({ export const AssociatedRolesTab = ({
additionalRoles,
addComposites,
parentRole, parentRole,
onRemove, refresh: refreshParent,
}: AssociatedRolesTabProps) => { }: AssociatedRolesTabProps) => {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
const history = useHistory(); const history = useHistory();
@ -49,100 +44,65 @@ export const AssociatedRolesTab = ({
const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]); const [selectedRows, setSelectedRows] = useState<RoleRepresentation[]>([]);
const [isInheritedHidden, setIsInheritedHidden] = useState(false); const [isInheritedHidden, setIsInheritedHidden] = useState(false);
const [allRoles, setAllRoles] = useState<RoleRepresentation[]>([]); const [count, setCount] = useState(0);
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { id } = useParams<{ id: string }>(); const { id } = useParams<{ id: string }>();
const inheritanceMap = React.useRef<{ [key: string]: string }>({});
const getSubRoles = async (role: Role, allRoles: Role[]): Promise<Role[]> => { const subRoles = async (result: Role[], roles: Role[]): Promise<Role[]> => {
// Fetch all composite roles const promises = roles.map(async (r) => {
const allCompositeRoles = await adminClient.roles.getCompositeRoles({ if (result.find((o) => o.id === r.id)) return result;
id: role.id!, result.push(r);
if (r.composite) {
const subList = (await adminClient.roles.getCompositeRoles({
id: r.id!,
})) as Role[];
subList.map((o) => (o.inherited = r.name));
result.concat(await subRoles(result, subList));
}
return result;
}); });
await Promise.all(promises);
// Need to ensure we don't get into an infinite loop, do not add any role that is already there or the starting role return [...result];
const newRoles: Promise<RoleRepresentation[]> = allCompositeRoles.reduce(
async (acc: Promise<RoleRepresentation[]>, newRole) => {
const resolvedRoles = await acc;
if (!allRoles.find((ar) => ar.id === newRole.id)) {
inheritanceMap.current[newRole.id!] = role.name!;
resolvedRoles.push(newRole);
const subRoles = await getSubRoles(newRole, [
...allRoles,
...resolvedRoles,
]);
resolvedRoles.push(...subRoles);
}
return acc;
},
Promise.resolve([] as RoleRepresentation[])
);
return newRoles;
}; };
const loader = async () => { const loader = async (first?: number, max?: number, search?: string) => {
const alphabetize = (rolesList: Role[]) => { const compositeRoles = await adminClient.roles.getCompositeRoles({
return _.sortBy(rolesList, (role) => role.name?.toUpperCase()); id: parentRole.id!,
}; first: first,
const clients = await adminClient.clients.find(); max: max!,
search: search,
});
setCount(compositeRoles.length);
if (isInheritedHidden) { if (!isInheritedHidden) {
setAllRoles(additionalRoles); const children = await subRoles([], compositeRoles);
return alphabetize( compositeRoles.splice(0, compositeRoles.length);
additionalRoles.filter( compositeRoles.push(...children);
(role) =>
role.containerId === "master" && !inheritanceMap.current[role.id!]
)
);
} }
const fetchedRoles: Promise<Role[]> = additionalRoles.reduce( await Promise.all(
async (acc: Promise<Role[]>, role) => { compositeRoles.map(async (role) => {
const resolvedRoles = await acc; if (role.clientRole) {
resolvedRoles.push(role); role.containerId = (
const subRoles = await getSubRoles(role, resolvedRoles); await adminClient.clients.findOne({
resolvedRoles.push(...subRoles); id: role.containerId!,
return acc; })
}, )?.clientId;
Promise.resolve([] as Role[]) }
})
); );
return fetchedRoles.then((results: Role[]) => { return compositeRoles;
const filterDupes = results.filter(
(thing, index, self) =>
index === self.findIndex((t) => t.name === thing.name)
);
filterDupes
.filter((role) => role.clientRole)
.map(
(role) =>
(role.clientId = clients.find(
(client) => client.id === role.containerId
)!.clientId!)
);
return alphabetize(additionalRoles);
});
}; };
useEffect(() => { const AliasRenderer = ({ id, name, clientRole, containerId }: Role) => {
refresh();
}, [additionalRoles, isInheritedHidden]);
const InheritedRoleName = (role: RoleRepresentation) =>
inheritanceMap.current[role.id!];
const AliasRenderer = ({ id, name, clientId }: Role) => {
return ( return (
<> <>
{clientId && ( {clientRole && (
<Label color="blue" key={`label-${id}`}> <Label color="blue" key={`label-${id}`}>
{clientId} {containerId}
</Label> </Label>
)}{" "} )}{" "}
{name} {name}
@ -150,7 +110,20 @@ export const AssociatedRolesTab = ({
); );
}; };
const toggleModal = () => setOpen(!open); const toggleModal = () => {
setOpen(!open);
refresh();
};
const reload = () => {
if (selectedRows.length >= count) {
refreshParent();
const loc = url.replace(/\/AssociatedRoles/g, "/details");
history.push(loc);
} else {
refresh();
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleRemoveAssociatedRoleConfirm", titleKey: "roles:roleRemoveAssociatedRoleConfirm",
@ -163,7 +136,7 @@ export const AssociatedRolesTab = ({
onConfirm: async () => { onConfirm: async () => {
try { try {
await adminClient.roles.delCompositeRoles({ id }, selectedRows); await adminClient.roles.delCompositeRoles({ id }, selectedRows);
onRemove(selectedRows); reload();
setSelectedRows([]); setSelectedRows([]);
addAlert(t("associatedRolesRemoved"), AlertVariant.success); addAlert(t("associatedRolesRemoved"), AlertVariant.success);
@ -177,21 +150,15 @@ export const AssociatedRolesTab = ({
useConfirmDialog({ useConfirmDialog({
titleKey: t("roles:removeAssociatedRoles") + "?", titleKey: t("roles:removeAssociatedRoles") + "?",
messageKey: t("roles:removeAllAssociatedRolesConfirmDialog", { messageKey: t("roles:removeAllAssociatedRolesConfirmDialog", {
name: parentRole?.name || t("createRole"), name: parentRole.name || t("createRole"),
}), }),
continueButtonLabel: "common:remove", continueButtonLabel: "common:remove",
continueButtonVariant: ButtonVariant.danger, continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => { onConfirm: async () => {
try { try {
if (selectedRows.length >= allRoles.length) {
onRemove(selectedRows);
const loc = url.replace(/\/AssociatedRoles/g, "/details");
history.push(loc);
}
onRemove(selectedRows);
await adminClient.roles.delCompositeRoles({ id }, selectedRows); await adminClient.roles.delCompositeRoles({ id }, selectedRows);
addAlert(t("associatedRolesRemoved"), AlertVariant.success); addAlert(t("associatedRolesRemoved"), AlertVariant.success);
refresh(); reload();
} catch (error) { } catch (error) {
addError("roles:roleDeleteError", error); addError("roles:roleDeleteError", error);
} }
@ -203,20 +170,21 @@ export const AssociatedRolesTab = ({
<PageSection variant="light" padding={{ default: "noPadding" }}> <PageSection variant="light" padding={{ default: "noPadding" }}>
<DeleteConfirm /> <DeleteConfirm />
<DeleteAssociatedRolesConfirm /> <DeleteAssociatedRolesConfirm />
<AssociatedRolesModal {open && <AssociatedRolesModal toggleDialog={toggleModal} />}
onConfirm={addComposites}
existingCompositeRoles={additionalRoles}
open={open}
toggleDialog={() => setOpen(!open)}
/>
<KeycloakDataTable <KeycloakDataTable
key={key} key={key}
loader={loader} loader={loader}
ariaLabelKey="roles:roleList" ariaLabelKey="roles:roleList"
searchPlaceholderKey="roles:searchFor" searchPlaceholderKey="roles:searchFor"
canSelectAll canSelectAll
isPaginated
onSelect={(rows) => { onSelect={(rows) => {
setSelectedRows([...rows]); setSelectedRows([
...rows.map((r) => {
delete r.inherited;
return r;
}),
]);
}} }}
toolbarItem={ toolbarItem={
<> <>
@ -225,7 +193,10 @@ export const AssociatedRolesTab = ({
label="Hide inherited roles" label="Hide inherited roles"
key="associated-roles-check" key="associated-roles-check"
id="kc-hide-inherited-roles-checkbox" id="kc-hide-inherited-roles-checkbox"
onChange={() => setIsInheritedHidden(!isInheritedHidden)} onChange={() => {
setIsInheritedHidden(!isInheritedHidden);
refresh();
}}
isChecked={isInheritedHidden} isChecked={isInheritedHidden}
/> />
</ToolbarItem> </ToolbarItem>
@ -269,9 +240,8 @@ export const AssociatedRolesTab = ({
cellFormatters: [emptyFormatter()], cellFormatters: [emptyFormatter()],
}, },
{ {
name: "containerId", name: "inherited",
displayKey: "roles:inheritedFrom", displayKey: "roles:inheritedFrom",
cellRenderer: InheritedRoleName,
cellFormatters: [emptyFormatter()], cellFormatters: [emptyFormatter()],
}, },
{ {

View file

@ -3,17 +3,18 @@ import {
ActionGroup, ActionGroup,
Button, Button,
FormGroup, FormGroup,
PageSection,
TextArea, TextArea,
TextInput, TextInput,
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import type { UseFormMethods } from "react-hook-form"; import type { UseFormMethods } from "react-hook-form";
import type { RoleFormType } from "./RealmRoleTabs";
import { FormAccess } from "../components/form-access/FormAccess"; import { FormAccess } from "../components/form-access/FormAccess";
import type { AttributeForm } from "../components/attribute-form/AttributeForm";
export type RealmRoleFormProps = { export type RealmRoleFormProps = {
form: UseFormMethods<RoleFormType>; form: UseFormMethods<AttributeForm>;
save: () => void; save: () => void;
editMode: boolean; editMode: boolean;
reset: () => void; reset: () => void;
@ -28,66 +29,70 @@ export const RealmRoleForm = ({
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
return ( return (
<FormAccess <PageSection variant="light">
isHorizontal <FormAccess
onSubmit={handleSubmit(save)} isHorizontal
role="manage-realm" onSubmit={handleSubmit(save)}
className="pf-u-mt-lg" role="manage-realm"
> className="pf-u-mt-lg"
<FormGroup
label={t("roleName")}
fieldId="kc-name"
isRequired
validated={errors.name ? "error" : "default"}
helperTextInvalid={t("common:required")}
> >
<TextInput <FormGroup
ref={register({ required: !editMode })} label={t("roleName")}
type="text" fieldId="kc-name"
id="kc-name" isRequired
name="name" validated={errors.name ? "error" : "default"}
isReadOnly={editMode} helperTextInvalid={t("common:required")}
/> >
</FormGroup> <TextInput
<FormGroup ref={register({ required: !editMode })}
label={t("common:description")} type="text"
fieldId="kc-description" id="kc-name"
validated={ name="name"
errors.description ? ValidatedOptions.error : ValidatedOptions.default isReadOnly={editMode}
} />
helperTextInvalid={errors.description?.message} </FormGroup>
> <FormGroup
<TextArea label={t("common:description")}
name="description" fieldId="kc-description"
aria-label="description"
isDisabled={getValues().name?.includes("default-roles")}
ref={register({
maxLength: {
value: 255,
message: t("common:maxLength", { length: 255 }),
},
})}
type="text"
validated={ validated={
errors.description errors.description
? ValidatedOptions.error ? ValidatedOptions.error
: ValidatedOptions.default : ValidatedOptions.default
} }
id="kc-role-description" helperTextInvalid={errors.description?.message}
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
onClick={save}
data-testid="realm-roles-save-button"
> >
{t("common:save")} <TextArea
</Button> name="description"
<Button onClick={() => reset()} variant="link"> aria-label="description"
{editMode ? t("common:revert") : t("common:cancel")} isDisabled={getValues().name?.includes("default-roles")}
</Button> ref={register({
</ActionGroup> maxLength: {
</FormAccess> value: 255,
message: t("common:maxLength", { length: 255 }),
},
})}
type="text"
validated={
errors.description
? ValidatedOptions.error
: ValidatedOptions.default
}
id="kc-role-description"
/>
</FormGroup>
<ActionGroup>
<Button
variant="primary"
onClick={save}
data-testid="realm-roles-save-button"
>
{t("common:save")}
</Button>
<Button onClick={() => reset()} variant="link">
{editMode ? t("common:revert") : t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>
</PageSection>
); );
}; };

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react"; import React, { useState } from "react";
import { useHistory, useParams, useRouteMatch } from "react-router-dom"; import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { import {
AlertVariant, AlertVariant,
@ -10,16 +10,16 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useFieldArray, useForm } from "react-hook-form"; import { useFieldArray, useForm } from "react-hook-form";
import { omit } from "lodash";
import { useAlerts } from "../components/alert/Alerts"; import { useAlerts } from "../components/alert/Alerts";
import { useAdminClient, useFetch } from "../context/auth/AdminClient"; import { useAdminClient, useFetch } from "../context/auth/AdminClient";
import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation"; import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import type Composites from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import { import {
KeyValueType,
AttributesForm, AttributesForm,
attributesToArray, attributesToArray,
arrayToAttributes, arrayToAttributes,
AttributeForm,
} from "../components/attribute-form/AttributeForm"; } from "../components/attribute-form/AttributeForm";
import { ViewHeader } from "../components/view-header/ViewHeader"; import { ViewHeader } from "../components/view-header/ViewHeader";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
@ -31,10 +31,6 @@ import { AssociatedRolesTab } from "./AssociatedRolesTab";
import { UsersInRoleTab } from "./UsersInRoleTab"; import { UsersInRoleTab } from "./UsersInRoleTab";
import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation"; import type RealmRepresentation from "@keycloak/keycloak-admin-client/lib/defs/realmRepresentation";
export type RoleFormType = Omit<RoleRepresentation, "attributes"> & {
attributes: KeyValueType[];
};
type myRealmRepresentation = RealmRepresentation & { type myRealmRepresentation = RealmRepresentation & {
defaultRole?: { defaultRole?: {
id: string; id: string;
@ -44,11 +40,14 @@ type myRealmRepresentation = RealmRepresentation & {
export default function RealmRoleTabs() { export default function RealmRoleTabs() {
const { t } = useTranslation("roles"); const { t } = useTranslation("roles");
const form = useForm<RoleFormType>({ mode: "onChange" }); const form = useForm<AttributeForm>({
mode: "onChange",
});
const { control, setValue, getValues, trigger, reset } = form;
const history = useHistory(); const history = useHistory();
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const [role, setRole] = useState<RoleFormType>(); const [role, setRole] = useState<AttributeForm>();
const { id, clientId } = useParams<{ id: string; clientId: string }>(); const { id, clientId } = useParams<{ id: string; clientId: string }>();
@ -62,10 +61,6 @@ export default function RealmRoleTabs() {
setKey(`${new Date().getTime()}`); setKey(`${new Date().getTime()}`);
}; };
const [additionalRoles, setAdditionalRoles] = useState<RoleRepresentation[]>(
[]
);
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
@ -80,104 +75,93 @@ export default function RealmRoleTabs() {
const [realm, setRealm] = useState<myRealmRepresentation>(); const [realm, setRealm] = useState<myRealmRepresentation>();
useFetch( useFetch(
() => adminClient.realms.findOne({ realm: realmName }), async () => {
(realm) => { const realm = await adminClient.realms.findOne({ realm: realmName });
if (!realm) { if (!id) {
return { realm };
}
const role = await adminClient.roles.findOneById({ id });
return { realm, role };
},
({ realm, role }) => {
if (!realm || (!role && id)) {
throw new Error(t("common:notFound")); throw new Error(t("common:notFound"));
} }
if (role) {
const convertedRole = convert(role);
setRole(convertedRole);
Object.entries(convertedRole).map((entry) => {
setValue(entry[0], entry[1]);
});
}
setRealm(realm); setRealm(realm);
}, },
[key]
[]
); );
useEffect(() => {
const update = async () => {
if (id) {
const fetchedRole = await adminClient.roles.findOneById({ id });
if (!fetchedRole) {
throw new Error(t("common:notFound"));
}
const allAdditionalRoles = await adminClient.roles.getCompositeRoles({
id,
});
setAdditionalRoles(allAdditionalRoles);
const convertedRole = convert(fetchedRole);
Object.entries(convertedRole).map((entry) => {
form.setValue(entry[0], entry[1]);
});
setRole(convertedRole);
}
};
setTimeout(update, 100);
}, [key]);
const { fields, append, remove } = useFieldArray({ const { fields, append, remove } = useFieldArray({
control: form.control, control,
name: "attributes", name: "attributes",
}); });
const save = async () => { const save = async () => {
try { try {
const role = form.getValues(); const values = getValues();
if ( if (
role.attributes && values.attributes &&
role.attributes[role.attributes.length - 1].key === "" values.attributes[values.attributes.length - 1]?.key === ""
) { ) {
form.setValue( setValue(
"attributes", "attributes",
role.attributes.slice(0, role.attributes.length - 1) values.attributes.slice(0, values.attributes.length - 1)
); );
} }
if (!(await form.trigger())) { if (!(await trigger())) {
return; return;
} }
const { attributes, ...rest } = role; const { attributes, ...rest } = values;
const roleRepresentation: RoleRepresentation = rest; let roleRepresentation: RoleRepresentation = rest;
if (id) { if (id) {
if (attributes) { if (attributes) {
roleRepresentation.attributes = arrayToAttributes(attributes); roleRepresentation.attributes = arrayToAttributes(attributes);
} }
roleRepresentation = {
...omit(role!, "attributes"),
...roleRepresentation,
};
if (!clientId) { if (!clientId) {
await adminClient.roles.updateById({ id }, roleRepresentation); await adminClient.roles.updateById({ id }, roleRepresentation);
} else { } else {
await adminClient.clients.updateRole( await adminClient.clients.updateRole(
{ id: clientId, roleName: role.name! }, { id: clientId, roleName: values.name! },
roleRepresentation roleRepresentation
); );
} }
await adminClient.roles.createComposite( setRole(convert(roleRepresentation));
{ roleId: id, realm: realmName },
additionalRoles
);
setRole(role);
form.reset(role);
} else { } else {
let createdRole; let createdRole;
if (!clientId) { if (!clientId) {
await adminClient.roles.create(roleRepresentation); await adminClient.roles.create(roleRepresentation);
createdRole = await adminClient.roles.findOneByName({ createdRole = await adminClient.roles.findOneByName({
name: role.name!, name: values.name!,
}); });
} else { } else {
await adminClient.clients.createRole({ await adminClient.clients.createRole({
id: clientId, id: clientId,
name: role.name, name: values.name,
}); });
if (role.description) { if (values.description) {
await adminClient.clients.updateRole( await adminClient.clients.updateRole(
{ id: clientId, roleName: role.name! }, { id: clientId, roleName: values.name! },
roleRepresentation roleRepresentation
); );
} }
createdRole = await adminClient.clients.findRole({ createdRole = await adminClient.clients.findRole({
id: clientId, id: clientId,
roleName: role.name!, roleName: values.name!,
}); });
} }
if (!createdRole) { if (!createdRole) {
@ -195,23 +179,6 @@ export default function RealmRoleTabs() {
} }
}; };
const addComposites = async (composites: Composites[]): Promise<void> => {
const compositeArray = composites;
setAdditionalRoles([...additionalRoles, ...compositeArray]);
try {
await adminClient.roles.createComposite(
{ roleId: id, realm: realmName },
compositeArray
);
history.push(url.substr(0, url.lastIndexOf("/") + 1) + "AssociatedRoles");
refresh();
addAlert(t("addAssociatedRolesSuccess"), AlertVariant.success);
} catch (error) {
addError("roles:addAssociatedRolesError", error);
}
};
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "roles:roleDeleteConfirm", titleKey: "roles:roleDeleteConfirm",
messageKey: t("roles:roleDeleteConfirmDialog", { messageKey: t("roles:roleDeleteConfirmDialog", {
@ -297,6 +264,9 @@ export default function RealmRoleTabs() {
continueButtonVariant: ButtonVariant.danger, continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => { onConfirm: async () => {
try { try {
const additionalRoles = await adminClient.roles.getCompositeRoles({
id: role!.id!,
});
await adminClient.roles.delCompositeRoles({ id }, additionalRoles); await adminClient.roles.delCompositeRoles({ id }, additionalRoles);
addAlert( addAlert(
t("compositeRoleOff"), t("compositeRoleOff"),
@ -312,24 +282,22 @@ export default function RealmRoleTabs() {
}, },
}); });
const toggleModal = () => setOpen(!open); const toggleModal = () => {
setOpen(!open);
refresh();
};
return ( return (
<> <>
<DeleteConfirm /> <DeleteConfirm />
<DeleteAllAssociatedRolesConfirm /> <DeleteAllAssociatedRolesConfirm />
<AssociatedRolesModal {open && <AssociatedRolesModal toggleDialog={toggleModal} />}
onConfirm={addComposites}
existingCompositeRoles={additionalRoles}
open={open}
toggleDialog={() => setOpen(!open)}
/>
<ViewHeader <ViewHeader
titleKey={role?.name || t("createRole")} titleKey={role?.name || t("createRole")}
badges={[ badges={[
{ {
id: "composite-role-badge", id: "composite-role-badge",
text: additionalRoles.length > 0 ? t("composite") : "", text: role?.composite ? t("composite") : "",
readonly: true, readonly: true,
}, },
]} ]}
@ -340,38 +308,27 @@ export default function RealmRoleTabs() {
/> />
<PageSection variant="light" className="pf-u-p-0"> <PageSection variant="light" className="pf-u-p-0">
{id && ( {id && (
<KeycloakTabs isBox> <KeycloakTabs isBox mountOnEnter>
<Tab <Tab
eventKey="details" eventKey="details"
title={<TabTitleText>{t("common:details")}</TabTitleText>} title={<TabTitleText>{t("common:details")}</TabTitleText>}
> >
<PageSection variant="light"> <RealmRoleForm
<RealmRoleForm reset={() => reset(role)}
reset={() => form.reset(role)} form={form}
form={form} save={save}
save={save} editMode={true}
editMode={true} />
/>
</PageSection>
</Tab> </Tab>
{additionalRoles.length > 0 && ( {role?.composite && (
<Tab <Tab
eventKey="AssociatedRoles" eventKey="AssociatedRoles"
title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>} title={<TabTitleText>{t("associatedRolesText")}</TabTitleText>}
> >
<PageSection variant="light"> <AssociatedRolesTab parentRole={role} refresh={refresh} />
{role && (
<AssociatedRolesTab
additionalRoles={additionalRoles}
addComposites={addComposites}
parentRole={role}
onRemove={() => refresh()}
/>
)}
</PageSection>
</Tab> </Tab>
)} )}
{form.getValues().name !== realm?.defaultRole?.name && ( {role?.name !== realm?.defaultRole?.name && (
<Tab <Tab
eventKey="attributes" eventKey="attributes"
className="kc-attributes-tab" className="kc-attributes-tab"
@ -381,11 +338,11 @@ export default function RealmRoleTabs() {
form={form} form={form}
save={save} save={save}
array={{ fields, append, remove }} array={{ fields, append, remove }}
reset={() => form.reset(role)} reset={() => reset(role)}
/> />
</Tab> </Tab>
)} )}
{form.getValues().name !== realm?.defaultRole?.name && ( {role?.name !== realm?.defaultRole?.name && (
<Tab <Tab
eventKey="users-in-role" eventKey="users-in-role"
title={<TabTitleText>{t("usersInRole")}</TabTitleText>} title={<TabTitleText>{t("usersInRole")}</TabTitleText>}
@ -396,14 +353,12 @@ export default function RealmRoleTabs() {
</KeycloakTabs> </KeycloakTabs>
)} )}
{!id && ( {!id && (
<PageSection variant="light"> <RealmRoleForm
<RealmRoleForm reset={() => reset(role)}
reset={() => form.reset()} form={form}
form={form} save={save}
save={save} editMode={false}
editMode={false} />
/>
</PageSection>
)} )}
</PageSection> </PageSection>
</> </>

View file

@ -12,15 +12,15 @@ import {
} from "@patternfly/react-table"; } from "@patternfly/react-table";
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons"; import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
import type { AttributeForm } from "../components/attribute-form/AttributeForm";
import { FormAccess } from "../components/form-access/FormAccess"; import { FormAccess } from "../components/form-access/FormAccess";
import type { RoleFormType } from "./RealmRoleTabs";
import "./RealmRolesSection.css"; import "./RealmRolesSection.css";
export type KeyValueType = { key: string; value: string }; export type KeyValueType = { key: string; value: string };
type RoleAttributesProps = { type RoleAttributesProps = {
form: UseFormMethods<RoleFormType>; form: UseFormMethods<AttributeForm>;
save: () => void; save: () => void;
reset: () => void; reset: () => void;
array: { array: {

View file

@ -46,7 +46,7 @@ const RoleLink: FunctionComponent<RoleLinkProps> = ({ children, role }) => {
const clientRouteMatch = useRouteMatch<ClientParams>(ClientRoute.path); const clientRouteMatch = useRouteMatch<ClientParams>(ClientRoute.path);
const to = clientRouteMatch const to = clientRouteMatch
? toClientRole({ ...clientRouteMatch.params, id: role.id!, tab: "details" }) ? toClientRole({ ...clientRouteMatch.params, id: role.id!, tab: "details" })
: toRealmRole({ realm, id: role.id! }); : toRealmRole({ realm, id: role.id!, tab: "details" });
return ( return (
<Link key={role.id} to={to}> <Link key={role.id} to={to}>

View file

@ -1,5 +1,5 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react"; import { lazy } from "react";
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom"; import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config"; import type { RouteDef } from "../../route-config";

View file

@ -1,5 +1,5 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react"; import { lazy } from "react";
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom"; import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config"; import type { RouteDef } from "../../route-config";

View file

@ -1,9 +1,13 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react"; import { lazy } from "react";
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom"; import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config"; import type { RouteDef } from "../../route-config";
export type ClientRoleTab = "details" | "attributes" | "users-in-role"; export type ClientRoleTab =
| "details"
| "attributes"
| "users-in-role"
| "AssociateRoles";
export type ClientRoleParams = { export type ClientRoleParams = {
realm: string; realm: string;

View file

@ -1,6 +1,6 @@
import type { LocationDescriptorObject } from "history";
import { lazy } from "react"; import { lazy } from "react";
import { generatePath } from "react-router"; import { generatePath } from "react-router";
import type { LocationDescriptorObject } from "history";
import type { RouteDef } from "../../route-config"; import type { RouteDef } from "../../route-config";
export type RealmRoleTab = export type RealmRoleTab =

View file

@ -19,7 +19,7 @@ import userRoutes from "./user/routes";
export type RouteDef = { export type RouteDef = {
path: string; path: string;
component: ComponentType; component: ComponentType | React.LazyExoticComponent<() => JSX.Element>;
breadcrumb?: (t: TFunction) => string | ComponentType<any>; breadcrumb?: (t: TFunction) => string | ComponentType<any>;
access: AccessType | AccessType[]; access: AccessType | AccessType[];
matchOptions?: MatchOptions; matchOptions?: MatchOptions;

View file

@ -44,7 +44,7 @@ export const UserAttributes = ({ user }: UserAttributesProps) => {
const save = async (attributeForm: AttributeForm) => { const save = async (attributeForm: AttributeForm) => {
try { try {
const attributes = arrayToAttributes(attributeForm.attributes); const attributes = arrayToAttributes(attributeForm.attributes!);
await adminClient.users.update({ id: user.id! }, { ...user, attributes }); await adminClient.users.update({ id: user.id! }, { ...user, attributes });
form.setValue("attributes", convertAttributes(attributes)); form.setValue("attributes", convertAttributes(attributes));