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}
/>
) : (
""