mirror of
https://github.com/keycloak/keycloak.git
synced 2025-12-21 14:30:05 -06:00
add description to groups
fixes #39172 Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com> Signed-off-by: Alexander Schwartz <aschwart@redhat.com> Co-authored-by: Alexander Schwartz <aschwart@redhat.com>
This commit is contained in:
@@ -34,6 +34,7 @@ public class GroupRepresentation {
|
||||
// to identify a group and operate on it in a basic way
|
||||
protected String id;
|
||||
protected String name;
|
||||
protected String description;
|
||||
protected String path;
|
||||
protected String parentId;
|
||||
protected Long subGroupCount;
|
||||
@@ -62,6 +63,14 @@ public class GroupRepresentation {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
@@ -526,7 +526,6 @@ showRemaining=Show ${remaining}
|
||||
searchProfile=Search profile
|
||||
eventTypes.UPDATE_EMAIL_ERROR.name=Update email error
|
||||
removeConfirm_other=Are you sure you want to remove these groups?
|
||||
renameGroup=Rename group
|
||||
configure=Configure
|
||||
searchScopeHelp=For one level, the search applies only for users in the DNs specified by User DNs. For subtree, the search applies to the whole subtree. See LDAP documentation for more details.
|
||||
jumpToSection=Jump to section
|
||||
@@ -2101,7 +2100,7 @@ tableView=Table view
|
||||
addClientProfile=Add client profile
|
||||
maxFailureWaitSeconds=Max wait
|
||||
userEventsRegistered=User events registered
|
||||
renameAGroup=Rename group
|
||||
editGroup=Edit group
|
||||
eventConfigError=Could not save event configuration\: {{error}}
|
||||
confirmAccessTokenTitle=Regenerate registration access token?
|
||||
target=Target
|
||||
|
||||
@@ -159,7 +159,7 @@ export const GroupTable = ({ refresh: viewRefresh }: GroupTableProps) => {
|
||||
? []
|
||||
: [
|
||||
{
|
||||
title: t("rename"),
|
||||
title: t("edit"),
|
||||
onRowClick: async (group) => {
|
||||
setRename(group);
|
||||
return false;
|
||||
|
||||
@@ -54,7 +54,10 @@ export const GroupsModal = ({
|
||||
useState<GroupRepresentation | null>(null);
|
||||
|
||||
const form = useForm({
|
||||
defaultValues: { name: "" },
|
||||
defaultValues: {
|
||||
name: rename?.name || "",
|
||||
description: rename?.description || "",
|
||||
},
|
||||
});
|
||||
const { handleSubmit, formState } = form;
|
||||
|
||||
@@ -235,13 +238,13 @@ export const GroupsModal = ({
|
||||
} else if (rename) {
|
||||
await adminClient.groups.update(
|
||||
{ id },
|
||||
{ ...rename, name: group.name },
|
||||
{ ...rename, name: group.name, description: group.description },
|
||||
);
|
||||
} else {
|
||||
await adminClient.groups.updateChildGroup({ id }, group);
|
||||
}
|
||||
|
||||
refresh(rename ? { ...rename, name: group.name } : undefined);
|
||||
refresh(rename ? { ...rename, ...group } : undefined);
|
||||
handleModalToggle();
|
||||
addAlert(
|
||||
t(
|
||||
@@ -263,12 +266,12 @@ export const GroupsModal = ({
|
||||
variant={ModalVariant.small}
|
||||
title={
|
||||
rename
|
||||
? t("renameAGroup")
|
||||
? t("editGroup")
|
||||
: duplicateId
|
||||
? t("duplicateAGroup")
|
||||
: t("createAGroup")
|
||||
}
|
||||
isOpen={true}
|
||||
isOpen
|
||||
onClose={handleModalToggle}
|
||||
actions={[
|
||||
<FormSubmitButton
|
||||
@@ -279,7 +282,7 @@ export const GroupsModal = ({
|
||||
allowInvalid
|
||||
allowNonDirty
|
||||
>
|
||||
{t(rename ? "rename" : duplicateId ? "duplicate" : "create")}
|
||||
{t(rename ? "edit" : duplicateId ? "duplicate" : "create")}
|
||||
</FormSubmitButton>,
|
||||
<Button
|
||||
id="modal-cancel"
|
||||
@@ -308,6 +311,7 @@ export const GroupsModal = ({
|
||||
rules={{ required: t("required") }}
|
||||
autoFocus
|
||||
/>
|
||||
<TextControl name="description" label={t("description")} />
|
||||
</Form>
|
||||
</FormProvider>
|
||||
</Modal>
|
||||
|
||||
@@ -169,7 +169,7 @@ export default function GroupsSection() {
|
||||
key="renameGroup"
|
||||
onClick={() => setRename(currentGroup())}
|
||||
>
|
||||
{t("renameGroup")}
|
||||
{t("edit")}
|
||||
</DropdownItem>,
|
||||
<DropdownItem
|
||||
data-testid="deleteGroup"
|
||||
@@ -182,6 +182,9 @@ export default function GroupsSection() {
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<PageSection className="pf-v5-u-pt-0">
|
||||
{currentGroup()?.description}
|
||||
</PageSection>
|
||||
{subGroups.length > 0 && (
|
||||
<Tabs
|
||||
inset={{
|
||||
|
||||
@@ -125,7 +125,7 @@ const GroupTreeContextMenu = ({
|
||||
>
|
||||
<DropdownList>
|
||||
<DropdownItem key="rename" onClick={toggleRenameOpen}>
|
||||
{t("rename")}
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
<DropdownItem key="move" onClick={toggleMoveOpen}>
|
||||
{t("moveTo")}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { test } from "@playwright/test";
|
||||
import { expect, test } from "@playwright/test";
|
||||
import { v4 as uuid } from "uuid";
|
||||
import adminClient from "../utils/AdminClient";
|
||||
import { login } from "../utils/login";
|
||||
@@ -14,10 +14,11 @@ import {
|
||||
assertRowExists,
|
||||
clickRowKebabItem,
|
||||
clickSelectRow,
|
||||
clickTableRowItem,
|
||||
clickTableToolbarItem,
|
||||
searchItem,
|
||||
} from "../utils/table";
|
||||
import { createGroup, renameGroup, searchGroup } from "./list";
|
||||
import { createGroup, editGroup, searchGroup } from "./list";
|
||||
import { goToGroupDetails } from "./util";
|
||||
|
||||
test.describe("Group test", () => {
|
||||
@@ -48,15 +49,18 @@ test.describe("Group test", () => {
|
||||
});
|
||||
|
||||
test("Create group test", async ({ page }) => {
|
||||
await createGroup(page, groupName, true);
|
||||
await createGroup(page, groupName, "", true);
|
||||
await assertNotificationMessage(page, "Group created");
|
||||
await searchGroup(page, groupName);
|
||||
await assertRowExists(page, groupName, true);
|
||||
|
||||
// create group from search bar
|
||||
const secondGroupName = `group-second-${uuid()}`;
|
||||
await createGroup(page, secondGroupName, false);
|
||||
await createGroup(page, secondGroupName, "some sort of description", false);
|
||||
await assertNotificationMessage(page, "Group created");
|
||||
await clickTableRowItem(page, secondGroupName);
|
||||
await expect(page.getByText("some sort of description")).toBeVisible();
|
||||
await page.goBack();
|
||||
|
||||
await searchGroup(page, secondGroupName);
|
||||
await assertRowExists(page, secondGroupName, true);
|
||||
@@ -64,7 +68,7 @@ test.describe("Group test", () => {
|
||||
});
|
||||
|
||||
test("Fail to create group with empty name", async ({ page }) => {
|
||||
await createGroup(page, " ", true);
|
||||
await createGroup(page, " ", "", true);
|
||||
await assertNotificationMessage(
|
||||
page,
|
||||
"Could not create group Group name is missing",
|
||||
@@ -72,8 +76,8 @@ test.describe("Group test", () => {
|
||||
});
|
||||
|
||||
test("Fail to create group with duplicated name", async ({ page }) => {
|
||||
await createGroup(page, groupName, true);
|
||||
await createGroup(page, groupName, false);
|
||||
await createGroup(page, groupName, "", true);
|
||||
await createGroup(page, groupName, "", false);
|
||||
await assertNotificationMessage(
|
||||
page,
|
||||
`Could not create group Top level group named '${groupName}' already exists.`,
|
||||
@@ -130,10 +134,11 @@ test.describe("Search group under current group", () => {
|
||||
await assertRowExists(page, predefinedGroups[2], false);
|
||||
});
|
||||
|
||||
test("Rename group", async ({ page }) => {
|
||||
test("Edit group", async ({ page }) => {
|
||||
const newGroupName = "new_group_name";
|
||||
await clickRowKebabItem(page, predefinedGroups[3], "Rename");
|
||||
await renameGroup(page, newGroupName);
|
||||
const description = "new description";
|
||||
await clickRowKebabItem(page, predefinedGroups[3], "Edit");
|
||||
await editGroup(page, newGroupName, description);
|
||||
await assertNotificationMessage(page, "Group updated");
|
||||
await assertRowExists(page, newGroupName);
|
||||
await assertRowExists(page, predefinedGroups[3], false);
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Page } from "@playwright/test";
|
||||
export async function createGroup(
|
||||
page: Page,
|
||||
name: string,
|
||||
description: string,
|
||||
fromEmptyState = false,
|
||||
) {
|
||||
if (fromEmptyState) {
|
||||
@@ -11,6 +12,7 @@ export async function createGroup(
|
||||
await page.getByTestId("openCreateGroupModal").click();
|
||||
}
|
||||
await page.getByTestId("name").fill(name);
|
||||
await page.getByTestId("description").fill(description);
|
||||
await page.getByTestId("createGroup").click();
|
||||
}
|
||||
|
||||
@@ -22,7 +24,8 @@ export async function searchGroup(page: Page, name: string) {
|
||||
.click();
|
||||
}
|
||||
|
||||
export async function renameGroup(page: Page, name: string) {
|
||||
export async function editGroup(page: Page, name: string, description: string) {
|
||||
await page.getByTestId("name").fill(name);
|
||||
await page.getByTestId("description").fill(description);
|
||||
await page.getByTestId("renameGroup").click();
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
export default interface GroupRepresentation {
|
||||
id?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
path?: string;
|
||||
subGroupCount?: number;
|
||||
subGroups?: GroupRepresentation[];
|
||||
|
||||
@@ -70,7 +70,7 @@ describe("Groups", () => {
|
||||
const groupId = currentGroup.id;
|
||||
await kcAdminClient.groups.update(
|
||||
{ id: groupId! },
|
||||
{ name: "another-group-name" },
|
||||
{ name: "another-group-name", description: "another-group-description" },
|
||||
);
|
||||
|
||||
const group = await kcAdminClient.groups.findOne({
|
||||
|
||||
@@ -112,6 +112,18 @@ public class GroupAdapter implements GroupModel {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
if (isUpdated()) return updated.getDescription();
|
||||
return cached.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDescription(String description) {
|
||||
getDelegateForUpdate();
|
||||
updated.setDescription(description);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSingleAttribute(String name, String value) {
|
||||
getDelegateForUpdate();
|
||||
|
||||
@@ -39,6 +39,7 @@ public class CachedGroup extends AbstractRevisioned implements InRealm {
|
||||
|
||||
private final String realm;
|
||||
private final String name;
|
||||
private final String description;
|
||||
private final String parentId;
|
||||
private final LazyLoader<GroupModel, MultivaluedHashMap<String, String>> attributes;
|
||||
private final LazyLoader<GroupModel, Set<String>> roleMappings;
|
||||
@@ -49,6 +50,7 @@ public class CachedGroup extends AbstractRevisioned implements InRealm {
|
||||
super(revision, group.getId());
|
||||
this.realm = realm.getId();
|
||||
this.name = group.getName();
|
||||
this.description = group.getDescription();
|
||||
this.parentId = group.getParentId();
|
||||
this.attributes = new DefaultLazyLoader<>(source -> new MultivaluedHashMap<>(source.getAttributes()), MultivaluedHashMap::new);
|
||||
this.roleMappings = new DefaultLazyLoader<>(source -> source.getRoleMappingsStream().map(RoleModel::getId).collect(Collectors.toSet()), Collections::emptySet);
|
||||
@@ -76,6 +78,10 @@ public class CachedGroup extends AbstractRevisioned implements InRealm {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public String getParentId() {
|
||||
return parentId;
|
||||
}
|
||||
|
||||
@@ -89,6 +89,17 @@ public class GroupAdapter implements GroupModel , JpaModel<GroupEntity> {
|
||||
fireGroupUpdatedEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return group.getDescription();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDescription(String description) {
|
||||
group.setDescription(description);
|
||||
fireGroupUpdatedEvent();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GroupModel getParent() {
|
||||
String parentId = this.getParentId();
|
||||
|
||||
@@ -51,6 +51,10 @@ public class GroupEntity {
|
||||
@Column(name = "NAME")
|
||||
protected String name;
|
||||
|
||||
@Nationalized
|
||||
@Column(name = "DESCRIPTION")
|
||||
protected String description;
|
||||
|
||||
@Column(name = "PARENT_GROUP")
|
||||
private String parentId;
|
||||
|
||||
@@ -92,6 +96,14 @@ public class GroupEntity {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public String getRealm() {
|
||||
return realm;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<!--
|
||||
~ * Copyright 2024 Red Hat, Inc. and/or its affiliates
|
||||
~ * and other contributors as indicated by the @author tags.
|
||||
~ *
|
||||
~ * Licensed under the Apache License, Version 2.0 (the "License");
|
||||
~ * you may not use this file except in compliance with the License.
|
||||
~ * You may obtain a copy of the License at
|
||||
~ *
|
||||
~ * http://www.apache.org/licenses/LICENSE-2.0
|
||||
~ *
|
||||
~ * Unless required by applicable law or agreed to in writing, software
|
||||
~ * distributed under the License is distributed on an "AS IS" BASIS,
|
||||
~ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
~ * See the License for the specific language governing permissions and
|
||||
~ * limitations under the License.
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.1.xsd">
|
||||
|
||||
<changeSet author="keycloak" id="26.3.0-groups-description">
|
||||
<addColumn tableName="KEYCLOAK_GROUP">
|
||||
<column name="DESCRIPTION" type="NVARCHAR(255)" />
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -86,5 +86,6 @@
|
||||
<include file="META-INF/jpa-changelog-26.0.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-26.1.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-26.2.0.xml"/>
|
||||
<include file="META-INF/jpa-changelog-26.3.0.xml"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
|
||||
@@ -150,6 +150,7 @@ public class ModelToRepresentation {
|
||||
GroupRepresentation rep = new GroupRepresentation();
|
||||
rep.setId(group.getId());
|
||||
rep.setName(group.getName());
|
||||
rep.setDescription(group.getDescription());
|
||||
rep.setPath(buildGroupPath(group));
|
||||
rep.setParentId(group.getParentId());
|
||||
if (!full) return rep;
|
||||
|
||||
@@ -143,6 +143,7 @@ public class KeycloakModelUtilsTest {
|
||||
static boolean escapeSlashes = false;
|
||||
|
||||
private String name;
|
||||
private String description;
|
||||
private GroupModel parent;
|
||||
|
||||
public GroupAdapterTest(String name, GroupModel parent) {
|
||||
@@ -165,6 +166,16 @@ public class KeycloakModelUtilsTest {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setDescription(String description) {
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setSingleAttribute(String name, String value) {
|
||||
}
|
||||
|
||||
@@ -193,6 +193,10 @@ public interface GroupModel extends RoleMapperModel {
|
||||
|
||||
void setName(String name);
|
||||
|
||||
String getDescription();
|
||||
|
||||
void setDescription(String description);
|
||||
|
||||
/**
|
||||
* Set single value of specified attribute. Remove all other existing values
|
||||
*
|
||||
|
||||
@@ -269,6 +269,8 @@ public class GroupResource {
|
||||
model.removeAttribute(attr);
|
||||
}
|
||||
}
|
||||
|
||||
model.setDescription(rep.getDescription());
|
||||
}
|
||||
|
||||
@Path("role-mappings")
|
||||
|
||||
@@ -114,6 +114,15 @@ public class HardcodedGroupStorageProvider implements GroupStorageProvider {
|
||||
return groupName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public void setDescription(String description) {
|
||||
throw new ReadOnlyException("group is read only");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Stream<RoleModel> getRealmRoleMappingsStream() {
|
||||
return Stream.empty();
|
||||
|
||||
Reference in New Issue
Block a user