use group resource from context instead (#45883)

fixes: #45882

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2026-02-03 17:39:50 +01:00 committed by GitHub
parent 3e568fc81b
commit 52119f839e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 368 additions and 282 deletions

4
js/apps/.prettierrc Normal file
View file

@ -0,0 +1,4 @@
{
"tabWidth": 2,
"useTabs": false
}

View file

@ -8,6 +8,7 @@ import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../../../admin-client";
import { GroupPickerDialog } from "../../../components/group/GroupPickerDialog";
import { GroupResourceContext } from "../../../context/group-resource/GroupResourceContext";
type GroupForm = {
groups?: GroupValue[];
@ -67,25 +68,27 @@ export const Group = () => {
render={({ field }) => (
<>
{open && (
<GroupPickerDialog
type="selectMany"
text={{
title: "addGroupsToGroupPolicy",
ok: "add",
}}
onConfirm={(groups) => {
field.onChange([
...(field.value || []),
...(groups || []).map(({ id }) => ({ id })),
]);
setSelectedGroups([...selectedGroups, ...(groups || [])]);
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
filterGroups={selectedGroups}
/>
<GroupResourceContext value={adminClient.groups}>
<GroupPickerDialog
type="selectMany"
text={{
title: "addGroupsToGroupPolicy",
ok: "add",
}}
onConfirm={(groups) => {
field.onChange([
...(field.value || []),
...(groups || []).map(({ id }) => ({ id })),
]);
setSelectedGroups([...selectedGroups, ...(groups || [])]);
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
filterGroups={selectedGroups}
/>
</GroupResourceContext>
)}
<Button
data-testid="select-group-button"

View file

@ -11,8 +11,10 @@ import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { GroupPickerDialog } from "../group/GroupPickerDialog";
import { HelpItem } from "@keycloak/keycloak-ui-shared";
import { useAdminClient } from "../../admin-client";
import { GroupResourceContext } from "../../context/group-resource/GroupResourceContext";
import { GroupPickerDialog } from "../group/GroupPickerDialog";
import type { ComponentProps } from "./components";
export const GroupComponent = ({
@ -26,6 +28,7 @@ export const GroupComponent = ({
const [open, setOpen] = useState(false);
const [groups, setGroups] = useState<GroupRepresentation[]>();
const { control } = useFormContext();
const { adminClient } = useAdminClient();
return (
<Controller
@ -35,20 +38,22 @@ export const GroupComponent = ({
render={({ field }) => (
<>
{open && (
<GroupPickerDialog
type="selectOne"
text={{
title: "selectGroup",
ok: "select",
}}
onConfirm={(groups) => {
field.onChange(groups?.[0].path);
setGroups(groups);
setOpen(false);
}}
onClose={() => setOpen(false)}
filterGroups={groups}
/>
<GroupResourceContext value={adminClient.groups}>
<GroupPickerDialog
type="selectOne"
text={{
title: "selectGroup",
ok: "select",
}}
onConfirm={(groups) => {
field.onChange(groups?.[0].path);
setGroups(groups);
setOpen(false);
}}
onClose={() => setOpen(false)}
filterGroups={groups}
/>
</GroupResourceContext>
)}
<FormGroup

View file

@ -27,6 +27,7 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../../admin-client";
import { GroupPath } from "./GroupPath";
import { useGroupResource } from "../../context/group-resource/GroupResourceContext";
import "./group-picker-dialog.css";
@ -56,6 +57,7 @@ export const GroupPickerDialog = ({
onConfirm,
}: GroupPickerDialogProps) => {
const { adminClient } = useAdminClient();
const groupResource = useGroupResource();
const { t } = useTranslation();
const [selectedRows, setSelectedRows] = useState<SelectableGroup[]>([]);
@ -87,10 +89,10 @@ export const GroupPickerDialog = ({
if (filter !== "") {
args.search = filter;
}
groups = await adminClient.groups.find(args);
groups = await groupResource.find(args);
} else {
if (!navigation.map(({ id }) => id).includes(groupId)) {
group = await adminClient.groups.findOne({ id: groupId });
group = await groupResource.findOne({ id: groupId });
if (!group) {
throw new Error(t("notFound"));
}
@ -101,7 +103,7 @@ export const GroupPickerDialog = ({
max,
parentId: groupId,
};
groups = await adminClient.groups.listSubGroups(args);
groups = await groupResource.listSubGroups(args);
}
if (id) {

View file

@ -0,0 +1,27 @@
import {
createNamedContext,
useRequiredContext,
} from "@keycloak/keycloak-ui-shared";
import { Groups } from "@keycloak/keycloak-admin-client";
import { PropsWithChildren } from "react";
export const GroupsResourceContext = createNamedContext<Groups | undefined>(
"GroupsResourceContext",
undefined,
);
export const useGroupResource = () => useRequiredContext(GroupsResourceContext);
type GroupsContextProps = PropsWithChildren & {
value: Groups;
};
export const GroupResourceContext = ({
value,
children,
}: GroupsContextProps) => {
return (
<GroupsResourceContext.Provider value={value}>
{children}
</GroupsResourceContext.Provider>
);
};

View file

@ -9,7 +9,6 @@ import { useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { useAdminClient } from "../admin-client";
import {
AttributeForm,
AttributesForm,
@ -17,9 +16,10 @@ import {
import { arrayToKeyValue } from "../components/key-value-form/key-value-convert";
import { convertFormValuesToObject } from "../util";
import { getLastId } from "./groupIdUtils";
import { useGroupResource } from "../context/group-resource/GroupResourceContext";
export const GroupAttributes = () => {
const { adminClient } = useAdminClient();
const groups = useGroupResource();
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
@ -32,7 +32,7 @@ export const GroupAttributes = () => {
const [currentGroup, setCurrentGroup] = useState<GroupRepresentation>();
useFetch(
() => adminClient.groups.findOne({ id }),
() => groups.findOne({ id }),
(group) => {
form.reset({
attributes: arrayToKeyValue(group?.attributes!),
@ -45,10 +45,7 @@ export const GroupAttributes = () => {
const save = async (attributeForm: AttributeForm) => {
try {
const attributes = convertFormValuesToObject(attributeForm).attributes;
await adminClient.groups.update(
{ id: id! },
{ ...currentGroup, attributes },
);
await groups.update({ id: id! }, { ...currentGroup, attributes });
setCurrentGroup({ ...currentGroup, attributes });
addAlert(t("groupUpdated"), AlertVariant.success);

View file

@ -1,9 +1,9 @@
import type { RoleMappingPayload } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
import { AlertVariant } from "@patternfly/react-core";
import { useTranslation } from "react-i18next";
import { useAdminClient } from "../admin-client";
import { useAlerts } from "@keycloak/keycloak-ui-shared";
import { RoleMapping, Row } from "../components/role-mapping/RoleMapping";
import { useGroupResource } from "../context/group-resource/GroupResourceContext";
type GroupRoleMappingProps = {
id: string;
@ -16,7 +16,7 @@ export const GroupRoleMapping = ({
name,
canManageGroup,
}: GroupRoleMappingProps) => {
const { adminClient } = useAdminClient();
const groups = useGroupResource();
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
@ -27,7 +27,7 @@ export const GroupRoleMapping = ({
.filter((row) => row.client === undefined)
.map((row) => row.role as RoleMappingPayload)
.flat();
await adminClient.groups.addRealmRoleMappings({
await groups.addRealmRoleMappings({
id,
roles: realmRoles,
});
@ -35,7 +35,7 @@ export const GroupRoleMapping = ({
rows
.filter((row) => row.client !== undefined)
.map((row) =>
adminClient.groups.addClientRoleMappings({
groups.addClientRoleMappings({
id,
clientUniqueId: row.client!.id!,
roles: [row.role as RoleMappingPayload],

View file

@ -7,7 +7,6 @@ import { SearchInput, ToolbarItem } from "@patternfly/react-core";
import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useLocation } from "react-router-dom";
import { useAdminClient } from "../admin-client";
import { ListEmptyState } from "@keycloak/keycloak-ui-shared";
import { KeycloakDataTable } from "@keycloak/keycloak-ui-shared";
import { useAccess } from "../context/access/Access";
@ -18,13 +17,14 @@ import { DeleteGroup } from "./components/DeleteGroup";
import { GroupToolbar } from "./components/GroupToolbar";
import { MoveDialog } from "./components/MoveDialog";
import { getLastId } from "./groupIdUtils";
import { useGroupResource } from "../context/group-resource/GroupResourceContext";
type GroupTableProps = {
refresh: () => void;
};
export const GroupTable = ({ refresh: viewRefresh }: GroupTableProps) => {
const { adminClient } = useAdminClient();
const groups = useGroupResource();
const { t } = useTranslation();
const [selectedRows, setSelectedRows] = useState<GroupRepresentation[]>([]);
const [rename, setRename] = useState<GroupRepresentation>();
@ -50,14 +50,14 @@ export const GroupTable = ({ refresh: viewRefresh }: GroupTableProps) => {
max: max,
parentId: id,
};
groupsData = await adminClient.groups.listSubGroups(args);
groupsData = await groups.listSubGroups(args);
} else {
const args: GroupQuery = {
search: search || "",
first: first || undefined,
max: max || undefined,
};
groupsData = await adminClient.groups.find(args);
groupsData = await groups.find(args);
}
return groupsData;

View file

@ -39,6 +39,7 @@ import { DeleteGroup } from "./components/DeleteGroup";
import { GroupTree } from "./components/GroupTree";
import { getId, getLastId } from "./groupIdUtils";
import { toGroups } from "./routes/Groups";
import { GroupResourceContext } from "../context/group-resource/GroupResourceContext";
import "./GroupsSection.css";
@ -133,160 +134,172 @@ export default function GroupsSection() {
/>
)}
<PageSection variant={PageSectionVariants.light} className="pf-v5-u-p-0">
<Drawer isInline isExpanded={open} key={key} position="left">
<DrawerContent
panelContent={
<DrawerPanelContent isResizable>
<DrawerHead>
<GroupTree
refresh={refresh}
canViewDetails={canViewDetails}
<GroupResourceContext value={adminClient.groups}>
<Drawer isInline isExpanded={open} key={key} position="left">
<DrawerContent
panelContent={
<DrawerPanelContent isResizable>
<DrawerHead>
<GroupTree
refresh={refresh}
canViewDetails={canViewDetails}
/>
</DrawerHead>
</DrawerPanelContent>
}
>
<DrawerContentBody>
<Tooltip content={open ? t("hide") : t("show")}>
<Button
aria-label={open ? t("hide") : t("show")}
variant="plain"
icon={open ? <AngleLeftIcon /> : <TreeIcon />}
onClick={toggle}
/>
</DrawerHead>
</DrawerPanelContent>
}
>
<DrawerContentBody>
<Tooltip content={open ? t("hide") : t("show")}>
<Button
aria-label={open ? t("hide") : t("show")}
variant="plain"
icon={open ? <AngleLeftIcon /> : <TreeIcon />}
onClick={toggle}
</Tooltip>
<GroupBreadCrumbs />
<ViewHeader
titleKey={!id ? "groups" : currentGroup()?.name!}
subKey={!id ? "groupsDescription" : ""}
helpUrl={!id ? helpUrls.groupsUrl : ""}
divider={!id}
dropdownItems={
id && canManageGroup
? [
<DropdownItem
data-testid="renameGroupAction"
key="renameGroup"
onClick={() => setRename(currentGroup())}
>
{t("edit")}
</DropdownItem>,
<DropdownItem
data-testid="deleteGroup"
key="deleteGroup"
onClick={toggleDeleteOpen}
>
{t("deleteGroup")}
</DropdownItem>,
]
: undefined
}
/>
</Tooltip>
<GroupBreadCrumbs />
<ViewHeader
titleKey={!id ? "groups" : currentGroup()?.name!}
subKey={!id ? "groupsDescription" : ""}
helpUrl={!id ? helpUrls.groupsUrl : ""}
divider={!id}
dropdownItems={
id && canManageGroup
? [
<DropdownItem
data-testid="renameGroupAction"
key="renameGroup"
onClick={() => setRename(currentGroup())}
>
{t("edit")}
</DropdownItem>,
<DropdownItem
data-testid="deleteGroup"
key="deleteGroup"
onClick={toggleDeleteOpen}
>
{t("deleteGroup")}
</DropdownItem>,
]
: undefined
}
/>
<PageSection className="pf-v5-u-pt-0">
{currentGroup()?.description}
</PageSection>
{subGroups.length > 0 && (
<Tabs
inset={{
default: "insetNone",
md: "insetSm",
xl: "insetLg",
"2xl": "inset2xl",
}}
activeKey={activeTab}
onSelect={(_, key) => setActiveTab(key as number)}
isBox
mountOnEnter
unmountOnExit
>
<Tab
data-testid="groups"
eventKey={0}
title={<TabTitleText>{t("childGroups")}</TabTitleText>}
<PageSection className="pf-v5-u-pt-0">
{currentGroup()?.description}
</PageSection>
{subGroups.length > 0 && (
<Tabs
inset={{
default: "insetNone",
md: "insetSm",
xl: "insetLg",
"2xl": "inset2xl",
}}
activeKey={activeTab}
onSelect={(_, key) => setActiveTab(key as number)}
isBox
mountOnEnter
unmountOnExit
>
<GroupTable refresh={refresh} />
</Tab>
{canViewMembers && (
<Tab
data-testid="members"
eventKey={1}
title={<TabTitleText>{t("members")}</TabTitleText>}
data-testid="groups"
eventKey={0}
title={<TabTitleText>{t("childGroups")}</TabTitleText>}
>
<Members />
<GroupTable refresh={refresh} />
</Tab>
)}
<Tab
data-testid="attributesTab"
eventKey={2}
title={<TabTitleText>{t("attributes")}</TabTitleText>}
>
<GroupAttributes />
</Tab>
{canViewRoles && (
<Tab
eventKey={3}
data-testid="role-mapping-tab"
title={<TabTitleText>{t("roleMapping")}</TabTitleText>}
>
<GroupRoleMapping
id={id!}
name={currentGroup()?.name!}
canManageGroup={canManageGroup}
/>
</Tab>
)}
{canViewPermissions && (
<Tab
eventKey={4}
data-testid="permissionsTab"
title={<TabTitleText>{t("permissions")}</TabTitleText>}
>
<PermissionsTab id={id} type="groups" />
</Tab>
)}
{hasAccess("view-events") && (
<Tab
eventKey={5}
data-testid="admin-events-tab"
title={<TabTitleText>{t("adminEvents")}</TabTitleText>}
>
<Tabs
activeKey={activeEventsTab}
onSelect={(_, key) => setActiveEventsTab(key as string)}
{canViewMembers && (
<Tab
data-testid="members"
eventKey={1}
title={<TabTitleText>{t("members")}</TabTitleText>}
>
<Tab
eventKey="adminEvents"
title={
<TabTitleText>{t("adminEvents")}</TabTitleText>
}
>
<AdminEvents resourcePath={`groups/${id}`} />
</Tab>
<Tab
eventKey="membershipEvents"
title={
<TabTitleText>{t("membershipEvents")}</TabTitleText>
}
>
<AdminEvents resourcePath={`users/*/groups/${id}`} />
</Tab>
<Tab
eventKey="childGroupEvents"
title={
<TabTitleText>{t("childGroupEvents")}</TabTitleText>
}
>
<AdminEvents resourcePath={`groups/${id}/children`} />
</Tab>
</Tabs>
<Members />
</Tab>
)}
<Tab
data-testid="attributesTab"
eventKey={2}
title={<TabTitleText>{t("attributes")}</TabTitleText>}
>
<GroupAttributes />
</Tab>
)}
</Tabs>
)}
{subGroups.length === 0 && <GroupTable refresh={refresh} />}
</DrawerContentBody>
</DrawerContent>
</Drawer>
{canViewRoles && (
<Tab
eventKey={3}
data-testid="role-mapping-tab"
title={<TabTitleText>{t("roleMapping")}</TabTitleText>}
>
<GroupRoleMapping
id={id!}
name={currentGroup()?.name!}
canManageGroup={canManageGroup}
/>
</Tab>
)}
{canViewPermissions && (
<Tab
eventKey={4}
data-testid="permissionsTab"
title={<TabTitleText>{t("permissions")}</TabTitleText>}
>
<PermissionsTab id={id} type="groups" />
</Tab>
)}
{hasAccess("view-events") && (
<Tab
eventKey={5}
data-testid="admin-events-tab"
title={<TabTitleText>{t("adminEvents")}</TabTitleText>}
>
<Tabs
activeKey={activeEventsTab}
onSelect={(_, key) =>
setActiveEventsTab(key as string)
}
>
<Tab
eventKey="adminEvents"
title={
<TabTitleText>{t("adminEvents")}</TabTitleText>
}
>
<AdminEvents resourcePath={`groups/${id}`} />
</Tab>
<Tab
eventKey="membershipEvents"
title={
<TabTitleText>
{t("membershipEvents")}
</TabTitleText>
}
>
<AdminEvents
resourcePath={`users/*/groups/${id}`}
/>
</Tab>
<Tab
eventKey="childGroupEvents"
title={
<TabTitleText>
{t("childGroupEvents")}
</TabTitleText>
}
>
<AdminEvents
resourcePath={`groups/${id}/children`}
/>
</Tab>
</Tabs>
</Tab>
)}
</Tabs>
)}
{subGroups.length === 0 && <GroupTable refresh={refresh} />}
</DrawerContentBody>
</DrawerContent>
</Drawer>
</GroupResourceContext>
</PageSection>
</>
);

View file

@ -34,6 +34,7 @@ import { useSubGroups } from "./SubGroupsContext";
import { getLastId } from "./groupIdUtils";
import { MembershipsModal } from "./MembershipsModal";
import useToggle from "../utils/useToggle";
import { useGroupResource } from "../context/group-resource/GroupResourceContext";
const UserDetailLink = (user: UserRepresentation) => {
const { realm } = useRealm();
@ -52,6 +53,7 @@ const UserDetailLink = (user: UserRepresentation) => {
export const Members = () => {
const { adminClient } = useAdminClient();
const groups = useGroupResource();
const { t } = useTranslation();
const { addAlert, addError } = useAlerts();
const location = useLocation();
@ -66,11 +68,7 @@ export const Members = () => {
const [showMemberships, toggleShowMemberships] = useToggle();
const { hasAccess } = useAccess();
useFetch(
() => adminClient.groups.findOne({ id: group()!.id! }),
setCurrentGroup,
[],
);
useFetch(() => groups.findOne({ id: group()!.id! }), setCurrentGroup, []);
const isManager =
hasAccess("manage-users") || currentGroup?.access!.manageMembership;
@ -90,8 +88,7 @@ export const Members = () => {
first: 0,
max: count,
};
const subGroups: GroupRepresentation[] =
await adminClient.groups.listSubGroups(args);
const subGroups: GroupRepresentation[] = await groups.listSubGroups(args);
nestedGroups = nestedGroups.concat(subGroups);
await Promise.all(
@ -107,7 +104,7 @@ export const Members = () => {
return [];
}
let members = await adminClient.groups.listMembers({
let members = await groups.listMembers({
id: id!,
briefRepresentation: true,
first,
@ -121,7 +118,7 @@ export const Members = () => {
);
await Promise.all(
subGroups.map((g) =>
adminClient.groups.listMembers({
groups.listMembers({
id: g.id!,
briefRepresentation: true,
}),
@ -144,7 +141,7 @@ export const Members = () => {
{addMembers && (
<MemberModal
membersQuery={(first, max) =>
adminClient.groups.listMembers({ id: id!, first, max })
groups.listMembers({ id: id!, first, max })
}
onAdd={async (selectedRows) => {
try {

View file

@ -4,6 +4,7 @@ import { useTranslation } from "react-i18next";
import { useAdminClient } from "../../admin-client";
import { useAlerts } from "@keycloak/keycloak-ui-shared";
import { GroupPickerDialog } from "../../components/group/GroupPickerDialog";
import { GroupResourceContext } from "../../context/group-resource/GroupResourceContext";
type MoveDialogProps = {
source: GroupRepresentation;
@ -44,16 +45,18 @@ export const MoveDialog = ({ source, onClose, refresh }: MoveDialogProps) => {
};
return (
<GroupPickerDialog
type="selectOne"
filterGroups={[source]}
text={{
title: "moveToGroup",
ok: "moveHere",
}}
onClose={onClose}
onConfirm={moveGroup}
isMove
/>
<GroupResourceContext value={adminClient.groups}>
<GroupPickerDialog
type="selectOne"
filterGroups={[source]}
text={{
title: "moveToGroup",
ok: "moveHere",
}}
onClose={onClose}
onConfirm={moveGroup}
isMove
/>
</GroupResourceContext>
);
};

View file

@ -13,6 +13,7 @@ import { useTranslation } from "react-i18next";
import { useAdminClient } from "../../admin-client";
import type { ComponentProps } from "../../components/dynamic/components";
import { GroupPickerDialog } from "../../components/group/GroupPickerDialog";
import { GroupResourceContext } from "../../context/group-resource/GroupResourceContext";
type GroupSelectProps = Omit<ComponentProps, "convertToName"> & {
variant?: "typeahead" | "typeaheadMulti";
@ -78,30 +79,32 @@ export const GroupSelect = ({
render={({ field }) => (
<>
{open && (
<GroupPickerDialog
type={selectOne ? "selectOne" : "selectMany"}
text={{
title: "addGroupsToGroupPolicy",
ok: "add",
}}
onConfirm={(selectGroup) => {
if (selectOne) {
field.onChange(convertGroups(selectGroup || []));
setGroups(selectGroup || []);
} else {
field.onChange([
...(field.value || []),
...convertGroups(selectGroup || []),
]);
setGroups([...groups, ...(selectGroup || [])]);
}
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
filterGroups={groups}
/>
<GroupResourceContext value={adminClient.groups}>
<GroupPickerDialog
type={selectOne ? "selectOne" : "selectMany"}
text={{
title: "addGroupsToGroupPolicy",
ok: "add",
}}
onConfirm={(selectGroup) => {
if (selectOne) {
field.onChange(convertGroups(selectGroup || []));
setGroups(selectGroup || []);
} else {
field.onChange([
...(field.value || []),
...convertGroups(selectGroup || []),
]);
setGroups([...groups, ...(selectGroup || [])]);
}
setOpen(false);
}}
onClose={() => {
setOpen(false);
}}
filterGroups={groups}
/>
</GroupResourceContext>
)}
<Button
data-testid="select-group-button"

View file

@ -2,6 +2,8 @@ import type GroupRepresentation from "@keycloak/keycloak-admin-client/lib/defs/g
import {
Action,
KeycloakDataTable,
KeycloakSpinner,
ListEmptyState,
useAlerts,
useFetch,
useHelp,
@ -26,12 +28,11 @@ import { Link } from "react-router-dom";
import { useAdminClient } from "../admin-client";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
import { KeycloakSpinner } from "@keycloak/keycloak-ui-shared";
import { ListEmptyState } from "@keycloak/keycloak-ui-shared";
import { useAccess } from "../context/access/Access";
import { GroupResourceContext } from "../context/group-resource/GroupResourceContext";
import { useRealm } from "../context/realm-context/RealmContext";
import { toUserFederation } from "../user-federation/routes/UserFederation";
import useToggle from "../utils/useToggle";
import { useAccess } from "../context/access/Access";
export const DefaultsGroupsTab = () => {
const { adminClient } = useAdminClient();
@ -119,7 +120,7 @@ export const DefaultsGroupsTab = () => {
}
return (
<>
<GroupResourceContext value={adminClient.groups}>
<RemoveDialog />
{isGroupPickerOpen && (
<GroupPickerDialog
@ -254,6 +255,6 @@ export const DefaultsGroupsTab = () => {
/>
}
/>
</>
</GroupResourceContext>
);
};

View file

@ -45,6 +45,7 @@ import { FixedButtonsGroup } from "../components/form/FixedButtonGroup";
import { RequiredActionMultiSelect } from "./user-credentials/RequiredActionMultiSelect";
import { useNavigate } from "react-router-dom";
import { CopyToClipboardButton } from "../components/copy-to-clipboard-button/CopyToClipboardButton";
import { GroupResourceContext } from "../context/group-resource/GroupResourceContext";
export type BruteForced = {
isBruteForceProtected?: boolean;
@ -187,25 +188,27 @@ export const UserForm = ({
>
<FormProvider {...form}>
{open && (
<GroupPickerDialog
type="selectMany"
text={{
title: "selectGroups",
ok: "join",
}}
canBrowse={isManager}
onConfirm={async (groups) => {
if (user?.id) {
await addGroups(groups || []);
} else {
await addChips(groups || []);
}
<GroupResourceContext value={adminClient.groups}>
<GroupPickerDialog
type="selectMany"
text={{
title: "selectGroups",
ok: "join",
}}
canBrowse={isManager}
onConfirm={async (groups) => {
if (user?.id) {
await addGroups(groups || []);
} else {
await addChips(groups || []);
}
setOpen(false);
}}
onClose={() => setOpen(false)}
filterGroups={selectedGroups}
/>
setOpen(false);
}}
onClose={() => setOpen(false)}
filterGroups={selectedGroups}
/>
</GroupResourceContext>
)}
{user?.id && (
<>

View file

@ -21,6 +21,7 @@ import { GroupPickerDialog } from "../components/group/GroupPickerDialog";
import { ListEmptyState } from "@keycloak/keycloak-ui-shared";
import { KeycloakDataTable } from "@keycloak/keycloak-ui-shared";
import { useAccess } from "../context/access/Access";
import { GroupResourceContext } from "../context/group-resource/GroupResourceContext";
type UserGroupsProps = {
user: UserRepresentation;
@ -152,20 +153,22 @@ export const UserGroups = ({ user }: UserGroupsProps) => {
<>
<DeleteConfirm />
{open && (
<GroupPickerDialog
id={user.id}
type="selectMany"
text={{
title: t("joinGroupsFor", { username: user.username }),
ok: "join",
}}
canBrowse={isManager}
onClose={() => setOpen(false)}
onConfirm={async (groups = []) => {
await addGroups(groups);
setOpen(false);
}}
/>
<GroupResourceContext value={adminClient.groups}>
<GroupPickerDialog
id={user.id}
type="selectMany"
text={{
title: t("joinGroupsFor", { username: user.username }),
ok: "join",
}}
canBrowse={isManager}
onClose={() => setOpen(false)}
onConfirm={async (groups = []) => {
await addGroups(groups);
setOpen(false);
}}
/>
</GroupResourceContext>
)}
<KeycloakDataTable
key={key}

View file

@ -8,3 +8,5 @@ export type { NetworkErrorOptions } from "./utils/fetchWithError.js";
export type { default as OrganizationInvitationRepresentation } from "./defs/organizationInvitationRepresentation.js";
export { OrganizationInvitationStatus } from "./defs/organizationInvitationRepresentation.js";
export { Groups } from "./resources/groups.js";

View file

@ -284,11 +284,12 @@ export class Groups extends Resource<{ realm?: string }> {
urlParamKeys: ["id"],
});
constructor(client: KeycloakAdminClient) {
constructor(client: KeycloakAdminClient, orgId?: string) {
super(client, {
path: "/admin/realms/{realm}/groups",
path: `/admin/realms/{realm}/${orgId ? "organizations/{orgId}/" : ""}groups`,
getUrlParams: () => ({
realm: client.realmName,
orgId,
}),
getBaseUrl: () => client.baseUrl,
});

View file

@ -4,6 +4,7 @@ import type OrganizationRepresentation from "../defs/organizationRepresentation.
import type OrganizationInvitationRepresentation from "../defs/organizationInvitationRepresentation.js";
import UserRepresentation from "../defs/userRepresentation.js";
import Resource from "./resource.js";
import { Groups } from "./groups.js";
interface PaginatedQuery {
first?: number; // The position of the first result to be processed (pagination offset)
@ -33,6 +34,7 @@ export class Organizations extends Resource<{ realm?: string }> {
/**
* Organizations
*/
#client: KeycloakAdminClient;
constructor(client: KeycloakAdminClient) {
super(client, {
@ -42,6 +44,7 @@ export class Organizations extends Resource<{ realm?: string }> {
}),
getBaseUrl: () => client.baseUrl,
});
this.#client = client;
}
public find = this.makeRequest<
@ -190,4 +193,6 @@ export class Organizations extends Resource<{ realm?: string }> {
path: "/{orgId}/invitations/{invitationId}",
urlParamKeys: ["orgId", "invitationId"],
});
public groups = (orgId: string) => new Groups(this.#client, orgId);
}

View file

@ -57,4 +57,21 @@ describe("Organizations", () => {
expect(allOrganizations).to.be.ok;
expect(allOrganizations).to.be.empty;
});
it("crud a group for organizations", async () => {
const org = await kcAdminClient.organizations.create({
name: "orgG",
enabled: true,
domains: [{ name: "orgg.com" }],
});
const group = await kcAdminClient.organizations.groups(org.id).create({
name: "cool-group",
});
expect(group.id).to.be.ok;
await kcAdminClient.organizations.groups(org.id).del({
id: group.id!,
});
});
});