Users: Add groups tab and list groups (#450)
* usergroups call wip * user groups * add user groups tab and list group data * clean up log stmts * add cypress test * clean up userGroups * remove comment * fix types * cypress test * fix lint and cypress test * lint * address PR feedback from Mark * clean up * remove component from viewheader * rebase and format * remove duplicate identifier * wrap groups in section * fix ts errors * add search functionality * remove comment * list groups initially * remove log stmt
This commit is contained in:
parent
6c4aa0b100
commit
236e89dc63
10 changed files with 250 additions and 8 deletions
|
@ -62,6 +62,12 @@ describe("Users test", () => {
|
|||
|
||||
masthead.checkNotificationMessage("The user has been saved");
|
||||
|
||||
cy.wait(1000);
|
||||
|
||||
// Go to user details
|
||||
|
||||
cy.getId("user-groups-tab").click();
|
||||
|
||||
sidebarPage.goToUsers();
|
||||
listingPage.searchItem(itemId).itemExist(itemId);
|
||||
|
||||
|
@ -75,5 +81,6 @@ describe("Users test", () => {
|
|||
|
||||
listingPage.itemExist(itemId, false);
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
|
|
@ -43,6 +43,7 @@ export const RealmRoleTabs = () => {
|
|||
const [role, setRole] = useState<RoleFormType>();
|
||||
|
||||
const { id, clientId } = useParams<{ id: string; clientId: string }>();
|
||||
|
||||
const { url } = useRouteMatch();
|
||||
|
||||
const { realm } = useRealm();
|
||||
|
|
|
@ -35,7 +35,7 @@ export const RealmRolesSection = () => {
|
|||
params.search = searchParam;
|
||||
}
|
||||
|
||||
if (listRoles) {
|
||||
if (!listRoles && !searchParam) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
@ -22,6 +22,7 @@ import { UsersSection } from "./user/UsersSection";
|
|||
import { MappingDetails } from "./client-scopes/details/MappingDetails";
|
||||
import { ClientDetails } from "./clients/ClientDetails";
|
||||
import { UsersTabs } from "./user/UsersTabs";
|
||||
import { UserGroups } from "./user/UserGroups";
|
||||
import { UserFederationKerberosSettings } from "./user-federation/UserFederationKerberosSettings";
|
||||
import { UserFederationLdapSettings } from "./user-federation/UserFederationLdapSettings";
|
||||
import { RoleMappingForm } from "./client-scopes/add/RoleMappingForm";
|
||||
|
@ -145,6 +146,12 @@ export const routes: RoutesFn = (t: TFunction) => [
|
|||
breadcrumb: t("users:createUser"),
|
||||
access: "manage-users",
|
||||
},
|
||||
{
|
||||
path: "/:realm/users/:id",
|
||||
component: UserGroups,
|
||||
breadcrumb: t("users:userDetails"),
|
||||
access: "manage-users",
|
||||
},
|
||||
{
|
||||
path: "/:realm/users/:id/:tab",
|
||||
component: UsersTabs,
|
||||
|
|
|
@ -49,9 +49,9 @@ export const UserForm = ({
|
|||
useEffect(() => {
|
||||
if (editMode) {
|
||||
return asyncStateFetch(
|
||||
() => adminClient.users.find({ username: id }),
|
||||
() => adminClient.users.findOne({ id: id }),
|
||||
(user) => {
|
||||
setupForm(user[0]);
|
||||
setupForm(user);
|
||||
},
|
||||
handleError
|
||||
);
|
||||
|
|
193
src/user/UserGroups.tsx
Normal file
193
src/user/UserGroups.tsx
Normal file
|
@ -0,0 +1,193 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Checkbox,
|
||||
PageSection,
|
||||
} from "@patternfly/react-core";
|
||||
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
|
||||
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
|
||||
import { useAlerts } from "../components/alert/Alerts";
|
||||
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
|
||||
import { emptyFormatter } from "../util";
|
||||
import { asyncStateFetch, useAdminClient } from "../context/auth/AdminClient";
|
||||
import GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation";
|
||||
import { cellWidth } from "@patternfly/react-table";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
|
||||
export const UserGroups = () => {
|
||||
const { t } = useTranslation("roles");
|
||||
const { addAlert } = useAlerts();
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
const handleError = useErrorHandler();
|
||||
|
||||
const [selectedGroup, setSelectedGroup] = useState<GroupRepresentation>();
|
||||
const [listGroups, setListGroups] = useState(true);
|
||||
const [search, setSearch] = useState("");
|
||||
const [username, setUsername] = useState("");
|
||||
|
||||
const [isDirectMembership, setDirectMembership] = useState(false);
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const params: { [name: string]: string | number } = {
|
||||
first: first!,
|
||||
max: max!,
|
||||
};
|
||||
|
||||
const user = await adminClient.users.findOne({ id });
|
||||
setUsername(user.username!);
|
||||
|
||||
const searchParam = search || "";
|
||||
if (searchParam) {
|
||||
params.search = searchParam;
|
||||
setSearch(searchParam);
|
||||
}
|
||||
|
||||
if (!searchParam && !listGroups) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return await adminClient.users.listGroups({ ...params, id });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
return asyncStateFetch(
|
||||
() => {
|
||||
return Promise.resolve(adminClient.users.listGroups({ id }));
|
||||
},
|
||||
(response) => {
|
||||
setListGroups(!!(response && response.length > 0));
|
||||
},
|
||||
handleError
|
||||
);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
refresh();
|
||||
}, [isDirectMembership]);
|
||||
|
||||
const AliasRenderer = (group: GroupRepresentation) => {
|
||||
return <>{group.name}</>;
|
||||
};
|
||||
|
||||
const LeaveButtonRenderer = (group: GroupRepresentation) => {
|
||||
return (
|
||||
<>
|
||||
<Button onClick={() => leave(group)} variant="link">
|
||||
{t("users:Leave")}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const toggleModal = () => setOpen(!open);
|
||||
|
||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||
titleKey: t("users:leaveGroup", {
|
||||
name: selectedGroup?.name,
|
||||
}),
|
||||
messageKey: t("users:leaveGroupConfirmDialog", {
|
||||
groupname: selectedGroup?.name,
|
||||
username: username,
|
||||
}),
|
||||
continueButtonLabel: "users:leave",
|
||||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await adminClient.users.delFromGroup({
|
||||
id,
|
||||
groupId: selectedGroup!.id!,
|
||||
});
|
||||
refresh();
|
||||
addAlert(t("users:removedGroupMembership"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("users:removedGroupMembershipError", { error }),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const leave = (group: GroupRepresentation) => {
|
||||
setSelectedGroup(group);
|
||||
toggleDeleteDialog();
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<PageSection variant="light">
|
||||
<DeleteConfirm />
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
ariaLabelKey="roles:roleList"
|
||||
searchPlaceholderKey="groups:searchGroup"
|
||||
canSelectAll
|
||||
onSelect={() => {}}
|
||||
toolbarItem={
|
||||
<>
|
||||
<Button
|
||||
className="kc-join-group-button"
|
||||
key="join-group-button"
|
||||
onClick={() => toggleModal()}
|
||||
data-testid="add-group-button"
|
||||
>
|
||||
{t("users:joinGroup")}
|
||||
</Button>
|
||||
<Checkbox
|
||||
label={t("users:directMembership")}
|
||||
key="direct-membership-check"
|
||||
id="kc-direct-membership-checkbox"
|
||||
onChange={() => setDirectMembership(!isDirectMembership)}
|
||||
isChecked={isDirectMembership}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "groupMembership",
|
||||
displayKey: "users:groupMembership",
|
||||
cellRenderer: AliasRenderer,
|
||||
cellFormatters: [emptyFormatter()],
|
||||
transforms: [cellWidth(40)],
|
||||
},
|
||||
{
|
||||
name: "path",
|
||||
displayKey: "users:Path",
|
||||
cellFormatters: [emptyFormatter()],
|
||||
transforms: [cellWidth(45)],
|
||||
},
|
||||
{
|
||||
name: "",
|
||||
cellRenderer: LeaveButtonRenderer,
|
||||
cellFormatters: [emptyFormatter()],
|
||||
transforms: [cellWidth(20)],
|
||||
},
|
||||
]}
|
||||
emptyState={
|
||||
!search ? (
|
||||
<ListEmptyState
|
||||
hasIcon={true}
|
||||
message={t("users:noGroups")}
|
||||
instructions={t("users:noGroupsText")}
|
||||
primaryActionText={t("users:joinGroup")}
|
||||
onPrimaryAction={() => {}}
|
||||
/>
|
||||
) : (
|
||||
""
|
||||
)
|
||||
}
|
||||
/>
|
||||
</PageSection>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -74,7 +74,7 @@ export const UsersSection = () => {
|
|||
|
||||
const UserDetailLink = (user: UserRepresentation) => (
|
||||
<>
|
||||
<Link key={user.username} to={`${url}/${user.username}/details`}>
|
||||
<Link key={user.username} to={`${url}/${user.id}/details`}>
|
||||
{user.username}
|
||||
</Link>
|
||||
</>
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import React from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import {
|
||||
AlertVariant,
|
||||
PageSection,
|
||||
|
@ -15,6 +15,7 @@ import { useAlerts } from "../components/alert/Alerts";
|
|||
import { useAdminClient } from "../context/auth/AdminClient";
|
||||
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
|
||||
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
|
||||
import { UserGroups } from "./UserGroups";
|
||||
|
||||
export const UsersTabs = () => {
|
||||
const { t } = useTranslation("roles");
|
||||
|
@ -25,6 +26,17 @@ export const UsersTabs = () => {
|
|||
const adminClient = useAdminClient();
|
||||
const form = useForm<UserRepresentation>({ mode: "onChange" });
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [user, setUser] = useState("");
|
||||
|
||||
useEffect(() => {
|
||||
const update = async () => {
|
||||
if (id) {
|
||||
const fetchedUser = await adminClient.users.findOne({ id });
|
||||
setUser(fetchedUser.username!);
|
||||
}
|
||||
};
|
||||
setTimeout(update, 100);
|
||||
}, []);
|
||||
|
||||
const save = async (user: UserRepresentation) => {
|
||||
try {
|
||||
|
@ -48,7 +60,7 @@ export const UsersTabs = () => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<ViewHeader titleKey={id! || t("users:createUser")} subKey="" />
|
||||
<ViewHeader titleKey={user! || t("users:createUser")} subKey="" />
|
||||
<PageSection variant="light">
|
||||
{id && (
|
||||
<KeycloakTabs isBox>
|
||||
|
@ -59,6 +71,13 @@ export const UsersTabs = () => {
|
|||
>
|
||||
<UserForm form={form} save={save} editMode={true} />
|
||||
</Tab>
|
||||
<Tab
|
||||
eventKey="groups"
|
||||
data-testid="user-groups-tab"
|
||||
title={<TabTitleText>{t("groups")}</TabTitleText>}
|
||||
>
|
||||
<UserGroups />
|
||||
</Tab>
|
||||
</KeycloakTabs>
|
||||
)}
|
||||
{!id && <UserForm form={form} save={save} editMode={false} />}
|
||||
|
|
|
@ -7,6 +7,17 @@
|
|||
"createNewUser": "Create new user",
|
||||
"noUsersFound": "No users found",
|
||||
"noUsersFoundError": "No users found due to {{error}}",
|
||||
"noGroups": "No groups",
|
||||
"noGroupsText": "You haven't added this user to any groups. Join a group to get started.",
|
||||
"joinGroup": "Join Group",
|
||||
"leave": "Leave",
|
||||
"leaveGroup": "Leave group {{name}}?",
|
||||
"leaveGroupConfirmDialog": "Are you sure you want to remove {{username}} from the group {{groupname}}?",
|
||||
"directMembership": "Direct membership",
|
||||
"groupMembership": "Group membership",
|
||||
"removedGroupMembership": "Removed group membership",
|
||||
"removedGroupMembershipError": "Error removing group membership",
|
||||
"path": "Path",
|
||||
"emptyInstructions": "Change your search criteria or add a user",
|
||||
"id": "ID",
|
||||
"createdAt": "Created at",
|
||||
|
|
|
@ -1,4 +1,8 @@
|
|||
|
||||
.keycloak__user-section__email-verified {
|
||||
color: var(--pf-global--danger-color--100);
|
||||
}
|
||||
}
|
||||
|
||||
button.pf-c-button.pf-m-primary.kc-join-group-button {
|
||||
margin-left: var(--pf-global--spacer--md);
|
||||
margin-right: var(--pf-global--spacer--xl);
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue