diff --git a/src/components/group/GroupPath.tsx b/src/components/group/GroupPath.tsx new file mode 100644 index 0000000000..d40e65fbf8 --- /dev/null +++ b/src/components/group/GroupPath.tsx @@ -0,0 +1,48 @@ +import React from "react"; +import { Tooltip } from "@patternfly/react-core"; +import type { TableTextProps } from "@patternfly/react-table"; + +import type GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentation"; + +type GroupPathProps = TableTextProps & { + group: GroupRepresentation; +}; + +const MAX_LENGTH = 20; +const PART = 10; + +const truncatePath = (path?: string) => { + if (path && path.length >= MAX_LENGTH) { + return ( + path.substr(0, PART) + + "..." + + path.substr(path.length - PART, path.length) + ); + } + return path; +}; + +export const GroupPath = ({ + group: { path }, + onMouseEnter: onMouseEnterProp = () => {}, + ...props +}: GroupPathProps) => { + const [tooltip, setTooltip] = React.useState(""); + const onMouseEnter = (event: any) => { + setTooltip(path!); + onMouseEnterProp(event); + }; + const text = ( + + {truncatePath(path)} + + ); + + return tooltip !== "" ? ( + + {text} + + ) : ( + text + ); +}; diff --git a/src/groups/Members.tsx b/src/groups/Members.tsx index a864230fd1..8adb53a0aa 100644 --- a/src/groups/Members.tsx +++ b/src/groups/Members.tsx @@ -1,5 +1,5 @@ import React, { useState } from "react"; -import { useLocation } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import { useTranslation } from "react-i18next"; import _ from "lodash"; import { @@ -16,6 +16,7 @@ import type GroupRepresentation from "keycloak-admin/lib/defs/groupRepresentatio import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { useAdminClient } from "../context/auth/AdminClient"; +import { useRealm } from "../context/realm-context/RealmContext"; import { useAlerts } from "../components/alert/Alerts"; import { emptyFormatter } from "../util"; @@ -23,6 +24,7 @@ import { getLastId } from "./groupIdUtils"; import { useSubGroups } from "./SubGroupsContext"; import { MemberModal } from "./MembersModal"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; +import { GroupPath } from "../components/group/GroupPath"; type MembersOf = UserRepresentation & { membership: GroupRepresentation[]; @@ -31,6 +33,7 @@ type MembersOf = UserRepresentation & { export const Members = () => { const { t } = useTranslation("groups"); const adminClient = useAdminClient(); + const { realm } = useRealm(); const { addAlert } = useAlerts(); const location = useLocation(); const id = getLastId(location.pathname); @@ -84,13 +87,23 @@ export const Members = () => { const MemberOfRenderer = (member: MembersOf) => { return ( <> - {member.membership.map((group) => ( - <>{group.path} + {member.membership.map((group, index) => ( + <> + + {member.membership[index + 1] ? ", " : ""} + ))} ); }; + const UserDetailLink = (user: MembersOf) => ( + <> + + {user.username} + + + ); return ( <> {addMembers && ( @@ -132,7 +145,10 @@ export const Members = () => { setIsKebabOpen(!isKebabOpen)} /> + setIsKebabOpen(!isKebabOpen)} + isDisabled={selectedRows.length === 0} + /> } isOpen={isKebabOpen} isPlain @@ -169,10 +185,29 @@ export const Members = () => { } + actions={[ + { + title: t("leave"), + onRowClick: async (user) => { + try { + await adminClient.users.delFromGroup({ + id: user.id!, + groupId: id!, + }); + addAlert(t("usersLeft", { count: 1 }), AlertVariant.success); + } catch (error) { + addAlert(t("usersLeftError"), AlertVariant.danger); + } + + return true; + }, + }, + ]} columns={[ { name: "username", displayKey: "common:name", + cellRenderer: UserDetailLink, }, { name: "email", diff --git a/src/groups/SearchGroups.tsx b/src/groups/SearchGroups.tsx index 28406de808..0778aed2d9 100644 --- a/src/groups/SearchGroups.tsx +++ b/src/groups/SearchGroups.tsx @@ -21,6 +21,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable import { useAdminClient } from "../context/auth/AdminClient"; import { useRealm } from "../context/realm-context/RealmContext"; import { ListEmptyState } from "../components/list-empty-state/ListEmptyState"; +import { GroupPath } from "../components/group/GroupPath"; import { useSubGroups } from "./SubGroupsContext"; type SearchGroup = GroupRepresentation & { @@ -101,6 +102,8 @@ export const SearchGroups = () => { return result; }; + const Path = (group: GroupRepresentation) => ; + return ( <> @@ -155,6 +158,7 @@ export const SearchGroups = () => { { name: "path", displayKey: "groups:path", + cellRenderer: Path, }, ]} emptyState={ diff --git a/src/user/UserGroups.tsx b/src/user/UserGroups.tsx index fa911393b7..f21f57375f 100644 --- a/src/user/UserGroups.tsx +++ b/src/user/UserGroups.tsx @@ -22,6 +22,7 @@ import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation" import { GroupPickerDialog } from "../components/group/GroupPickerDialog"; import { HelpContext } from "../components/help-enabler/HelpHeader"; import { QuestionCircleIcon } from "@patternfly/react-icons"; +import { GroupPath } from "../components/group/GroupPath"; type GroupTableData = GroupRepresentation & { membersLength?: number; @@ -271,6 +272,8 @@ export const UserGroups = () => { }); }; + const Path = (group: GroupRepresentation) => ; + return ( <> @@ -345,7 +348,7 @@ export const UserGroups = () => { { name: "path", displayKey: "users:path", - cellFormatters: [emptyFormatter()], + cellRenderer: Path, transforms: [cellWidth(45)], }, @@ -362,6 +365,8 @@ export const UserGroups = () => { hasIcon={true} message={t("noGroups")} instructions={t("noGroupsText")} + primaryActionText={t("joinGroup")} + onPrimaryAction={toggleModal} /> ) : ( ""