mirror of
https://github.com/keycloak/keycloak.git
synced 2026-02-03 20:39:33 -05:00
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:
parent
3e568fc81b
commit
52119f839e
19 changed files with 368 additions and 282 deletions
4
js/apps/.prettierrc
Normal file
4
js/apps/.prettierrc
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"tabWidth": 2,
|
||||
"useTabs": false
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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 && (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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!,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in a new issue