Skip to content

Commit

Permalink
Backport to expose membership type
Browse files Browse the repository at this point in the history
Signed-off-by: Agnieszka Gancarczyk <[email protected]>
  • Loading branch information
agagancarczyk authored and pedroigor committed Nov 27, 2024
1 parent 3400602 commit 61cdd65
Show file tree
Hide file tree
Showing 20 changed files with 610 additions and 59 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,6 @@ public enum MembershipType {
* Indicates that member cannot exist without group/organization.
*/
MANAGED;

public static final String NAME = "membershipType";
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
import jakarta.ws.rs.core.MediaType;
import jakarta.ws.rs.core.Response;
import org.keycloak.representations.idm.MemberRepresentation;
import org.keycloak.representations.idm.MembershipType;
import org.keycloak.representations.idm.OrganizationRepresentation;

public interface OrganizationMembersResource {
Expand Down Expand Up @@ -83,6 +84,28 @@ List<MemberRepresentation> search(
@QueryParam("max") Integer max
);

/**
* Return all organization members that match the specified filters.
*
* @param search a {@code String} representing either a member's username, e-mail, first name, or last name.
* @param exact if {@code true}, the members will be searched using exact match for the {@code search} param - i.e.
* at least one of the username main attributes must match exactly the {@code search} param. If false,
* the method returns all members with at least one main attribute partially matching the {@code search} param.
* @param membershipType The {@link org.keycloak.representations.idm.MembershipType}.
* @param first index of the first element (pagination offset).
* @param max the maximum number of results.
* @return a list containing the matched organization members.
*/
@GET
@Produces(MediaType.APPLICATION_JSON)
List<MemberRepresentation> search(
@QueryParam("search") String search,
@QueryParam("exact") Boolean exact,
@QueryParam("membershipType") MembershipType membershipType,
@QueryParam("first") Integer first,
@QueryParam("max") Integer max
);

@Path("{id}")
OrganizationMemberResource member(@PathParam("id") String id);

Expand Down Expand Up @@ -111,4 +134,4 @@ Response inviteUser(@FormParam("email") String email,
@GET
@Produces(MediaType.APPLICATION_JSON)
List<OrganizationRepresentation> getOrganizations(@PathParam("id") String id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3268,4 +3268,21 @@ groupDuplicated=Group duplicated
duplicateAGroup=Duplicate group
couldNotFetchClientRoleMappings=Could not fetch client role mappings\: {{error}}
duplicateGroupWarning=Duplication of groups with a large number of subgroups is not supported. Please ensure that the group you are duplicating does not have a large number of subgroups.
errorSavingTranslations=Error saving translations\: '{{error}}'
errorSavingTranslations=Error saving translations\: '{{error}}'
clearCachesTitle=Clear Caches
realmCache=Realm Cache
userCache=User Cache
keysCache=Keys Cache
clearButtonTitle=Clear
clearRealmCacheHelp=This will clear entries for all realms.
clearUserCacheHelp=This will clear entries for all realms.
clearKeysCacheHelp=Clears all entries from the cache of external public keys. These are keys of external clients or identity providers. This will clear all entries for all realms.
clearCacheSuccess=Cache cleared successfully
clearCacheError=Could not clear cache\: {{error}}
expandRow=Expand row
membershipType=Membership type
managedMembership=Managed membership
filterByMembershipType=Filter by Membership Type
organizationsMembersListError=Could not fetch organization members\: {{error}}
MANAGED=Managed
UNMANAGED=Unmanaged
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import {
Badge,
MenuToggle,
Select,
SelectList,
SelectOption,
} from "@patternfly/react-core";

type CheckboxFilterOptions = {
value: string;
label: string;
};

type CheckboxFilterComponentProps = {
filterPlaceholderText: string;
isOpen: boolean;
options: CheckboxFilterOptions[];
onOpenChange: (isOpen: boolean) => void;
onToggleClick: () => void;
onSelect: (
event: React.MouseEvent<HTMLButtonElement>,
selection: string,
) => void;
selectedItems: string[];
width?: string;
};

export const CheckboxFilterComponent = ({
filterPlaceholderText,
isOpen,
options,
onOpenChange,
onToggleClick,
onSelect,
selectedItems,
width,
}: CheckboxFilterComponentProps) => {
const toggle = (toggleRef: React.RefObject<HTMLButtonElement>) => (
<MenuToggle
ref={toggleRef}
onClick={onToggleClick}
isExpanded={isOpen}
style={{
width,
}}
>
{filterPlaceholderText}
{selectedItems.length > 0 && (
<Badge isRead className="pf-v5-u-m-xs">
{selectedItems.length}
</Badge>
)}
</MenuToggle>
);

return (
<Select
role="menu"
id="checkbox-select"
isOpen={isOpen}
selected={selectedItems}
onSelect={(event, value) => {
onSelect(event as React.MouseEvent<HTMLButtonElement>, value as string);
}}
onOpenChange={onOpenChange}
toggle={toggle}
data-testid="checkbox-filter-select"
>
<SelectList>
{options.map((option) => (
<SelectOption
key={option.value}
hasCheckbox
value={option.value}
isSelected={selectedItems.includes(option.value)}
data-testid={`checkbox-filter-option-${option.value}`}
>
{option.label}
</SelectOption>
))}
</SelectList>
</Select>
);
};
64 changes: 64 additions & 0 deletions js/apps/admin-ui/src/components/dynamic/SearchInputComponent.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import {
Button,
TextInputGroup,
TextInputGroupMain,
TextInputGroupUtilities,
} from "@patternfly/react-core";
import { ArrowRightIcon, SearchIcon, TimesIcon } from "@patternfly/react-icons";
import { useTranslation } from "react-i18next";

type SearchInputComponentProps = {
value: string;
onChange: (value: string) => void;
onSearch: (value: string) => void;
onClear: () => void;
placeholder?: string;
"aria-label"?: string;
};

export const SearchInputComponent = ({
value,
onChange,
onSearch,
onClear,
placeholder,
"aria-label": ariaLabel,
}: SearchInputComponentProps) => {
const { t } = useTranslation();

return (
<>
<TextInputGroup>
<TextInputGroupMain
icon={<SearchIcon />}
value={value}
onChange={(event: React.FormEvent<HTMLInputElement>) =>
onChange(event.currentTarget.value)
}
placeholder={placeholder}
aria-label={ariaLabel}
data-testid="search-input"
/>
<TextInputGroupUtilities style={{ marginInline: "0px" }}>
{value && (
<Button
variant="plain"
onClick={onClear}
aria-label={t("clear")}
data-testid="clear-search"
icon={<TimesIcon />}
/>
)}
</TextInputGroupUtilities>
</TextInputGroup>
<Button
icon={<ArrowRightIcon />}
variant="control"
style={{ marginLeft: "0.1rem" }}
onClick={() => onSearch(value)}
aria-label={t("search")}
data-testid="search"
/>
</>
);
};
Loading

0 comments on commit 61cdd65

Please sign in to comment.