mirror of
https://github.com/keycloak/keycloak.git
synced 2026-01-09 23:12:06 -03:30
added exact search option to attributes (#34135)
(cherry picked from commit a339e79d3ecbd35c12abd3c67717fc5ea466b415) Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
3f095fe9b5
commit
b82d67e4cd
@ -43,6 +43,11 @@ import { BruteUser, findUsers } from "../role-mapping/resource";
|
||||
import { KeycloakDataTable } from "../table-toolbar/KeycloakDataTable";
|
||||
import { UserDataTableToolbarItems } from "./UserDataTableToolbarItems";
|
||||
|
||||
export type UserFilter = {
|
||||
exact: boolean;
|
||||
userAttribute: UserAttribute[];
|
||||
};
|
||||
|
||||
export type UserAttribute = {
|
||||
name: string;
|
||||
displayName: string;
|
||||
@ -105,7 +110,10 @@ export function UserDataTable() {
|
||||
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
|
||||
const [searchType, setSearchType] = useState<SearchType>("default");
|
||||
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
|
||||
const [activeFilters, setActiveFilters] = useState<UserAttribute[]>([]);
|
||||
const [activeFilters, setActiveFilters] = useState<UserFilter>({
|
||||
exact: false,
|
||||
userAttribute: [],
|
||||
});
|
||||
const [profile, setProfile] = useState<UserProfileConfig>({});
|
||||
const [query, setQuery] = useState("");
|
||||
|
||||
@ -143,10 +151,11 @@ export function UserDataTable() {
|
||||
);
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const params: { [name: string]: string | number } = {
|
||||
const params: { [name: string]: string | number | boolean } = {
|
||||
first: first!,
|
||||
max: max!,
|
||||
q: query!,
|
||||
exact: activeFilters.exact,
|
||||
};
|
||||
|
||||
const searchParam = search || searchUser || "";
|
||||
@ -217,17 +226,16 @@ export function UserDataTable() {
|
||||
const listUsers = !(userStorage.length > 0);
|
||||
|
||||
const clearAllFilters = () => {
|
||||
const filtered = [...activeFilters].filter(
|
||||
(chip) => chip.name !== chip.name,
|
||||
);
|
||||
setActiveFilters(filtered);
|
||||
setActiveFilters({ exact: false, userAttribute: [] });
|
||||
setSearchUser("");
|
||||
setQuery("");
|
||||
refresh();
|
||||
};
|
||||
|
||||
const createQueryString = (filters: UserAttribute[]) => {
|
||||
return filters.map((filter) => `${filter.name}:${filter.value}`).join(" ");
|
||||
const createQueryString = (filters: UserFilter) => {
|
||||
return filters.userAttribute
|
||||
.map((filter) => `${filter.name}:${filter.value}`)
|
||||
.join(" ");
|
||||
};
|
||||
|
||||
const searchUserWithAttributes = () => {
|
||||
@ -239,9 +247,9 @@ export function UserDataTable() {
|
||||
const createAttributeSearchChips = () => {
|
||||
return (
|
||||
<FlexItem>
|
||||
{activeFilters.length > 0 && (
|
||||
{activeFilters.userAttribute.length > 0 && (
|
||||
<>
|
||||
{Object.values(activeFilters).map((entry) => {
|
||||
{Object.values(activeFilters.userAttribute).map((entry) => {
|
||||
return (
|
||||
<ChipGroup
|
||||
className="pf-u-mt-md pf-u-mr-md"
|
||||
@ -253,13 +261,16 @@ export function UserDataTable() {
|
||||
onClick={(event) => {
|
||||
event.stopPropagation();
|
||||
|
||||
const filtered = [...activeFilters].filter(
|
||||
const filtered = [...activeFilters.userAttribute].filter(
|
||||
(chip) => chip.name !== entry.name,
|
||||
);
|
||||
const attributes = createQueryString(filtered);
|
||||
const active = {
|
||||
userAttribute: filtered,
|
||||
exact: activeFilters.exact,
|
||||
};
|
||||
|
||||
setActiveFilters(filtered);
|
||||
setQuery(attributes);
|
||||
setActiveFilters(active);
|
||||
setQuery(createQueryString(active));
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
@ -301,7 +312,7 @@ export function UserDataTable() {
|
||||
};
|
||||
|
||||
const subtoolbar = () => {
|
||||
if (!activeFilters.length) {
|
||||
if (!activeFilters.userAttribute.length) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
@ -326,7 +337,9 @@ export function UserDataTable() {
|
||||
<DeleteConfirm />
|
||||
<UnlockUsersConfirm />
|
||||
<KeycloakDataTable
|
||||
isSearching={searchUser !== "" || activeFilters.length !== 0}
|
||||
isSearching={
|
||||
searchUser !== "" || activeFilters.userAttribute.length !== 0
|
||||
}
|
||||
key={key}
|
||||
loader={loader}
|
||||
isPaginated
|
||||
|
||||
@ -5,33 +5,33 @@ import {
|
||||
AlertVariant,
|
||||
Button,
|
||||
ButtonVariant,
|
||||
Checkbox,
|
||||
InputGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
Text,
|
||||
TextContent,
|
||||
TextVariants,
|
||||
} from "@patternfly/react-core";
|
||||
import { CheckIcon } from "@patternfly/react-icons";
|
||||
import { ReactNode, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { Controller, useForm } from "react-hook-form";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Form } from "react-router-dom";
|
||||
import { label } from "ui-shared";
|
||||
|
||||
import { useAlerts } from "../alert/Alerts";
|
||||
import { label, useAlerts } from "ui-shared";
|
||||
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
|
||||
import { UserAttribute } from "./UserDataTable";
|
||||
import { UserAttribute, UserFilter } from "./UserDataTable";
|
||||
|
||||
type UserDataTableAttributeSearchFormProps = {
|
||||
activeFilters: UserAttribute[];
|
||||
setActiveFilters: (filters: UserAttribute[]) => void;
|
||||
activeFilters: UserFilter;
|
||||
setActiveFilters: (filters: UserFilter) => void;
|
||||
profile: UserProfileConfig;
|
||||
createAttributeSearchChips: () => ReactNode;
|
||||
searchUserWithAttributes: () => void;
|
||||
};
|
||||
|
||||
type UserFilterForm = UserAttribute & { exact: boolean };
|
||||
|
||||
export function UserDataTableAttributeSearchForm({
|
||||
activeFilters,
|
||||
setActiveFilters,
|
||||
@ -57,13 +57,16 @@ export function UserDataTableAttributeSearchForm({
|
||||
setValue,
|
||||
setError,
|
||||
clearErrors,
|
||||
} = useForm<UserAttribute>({
|
||||
control,
|
||||
} = useForm<UserFilterForm>({
|
||||
mode: "onChange",
|
||||
defaultValues,
|
||||
});
|
||||
|
||||
const isAttributeKeyDuplicate = () => {
|
||||
return activeFilters.some((filter) => filter.name === getValues().name);
|
||||
return activeFilters.userAttribute.some(
|
||||
(filter) => filter.name === getValues().name,
|
||||
);
|
||||
};
|
||||
|
||||
const isAttributeNameValid = () => {
|
||||
@ -74,7 +77,9 @@ export function UserDataTableAttributeSearchForm({
|
||||
message: t("searchUserByAttributeMissingKeyError"),
|
||||
});
|
||||
} else if (
|
||||
activeFilters.some((filter) => filter.name === getValues().name)
|
||||
activeFilters.userAttribute.some(
|
||||
(filter) => filter.name === getValues().name,
|
||||
)
|
||||
) {
|
||||
setError("name", {
|
||||
type: "conflict",
|
||||
@ -104,13 +109,11 @@ export function UserDataTableAttributeSearchForm({
|
||||
|
||||
const addToFilter = () => {
|
||||
if (isAttributeValid()) {
|
||||
setActiveFilters([
|
||||
...activeFilters,
|
||||
{
|
||||
...getValues(),
|
||||
},
|
||||
]);
|
||||
reset();
|
||||
setActiveFilters({
|
||||
exact: getValues().exact,
|
||||
userAttribute: [...activeFilters.userAttribute, { ...getValues() }],
|
||||
});
|
||||
reset({ exact: getValues().exact });
|
||||
} else {
|
||||
errors.name?.message &&
|
||||
addAlert(errors.name.message, AlertVariant.danger);
|
||||
@ -120,10 +123,10 @@ export function UserDataTableAttributeSearchForm({
|
||||
};
|
||||
|
||||
const clearActiveFilters = () => {
|
||||
const filtered = [...activeFilters].filter(
|
||||
const filtered = [...activeFilters.userAttribute].filter(
|
||||
(chip) => chip.name !== chip.name,
|
||||
);
|
||||
setActiveFilters(filtered);
|
||||
setActiveFilters({ exact: getValues().exact, userAttribute: filtered });
|
||||
};
|
||||
|
||||
const createAttributeKeyInputField = () => {
|
||||
@ -131,7 +134,7 @@ export function UserDataTableAttributeSearchForm({
|
||||
return (
|
||||
<Select
|
||||
data-testid="search-attribute-name"
|
||||
variant={SelectVariant.typeahead}
|
||||
variant="typeahead"
|
||||
onToggle={(isOpen) => setSelectAttributeKeyOpen(isOpen)}
|
||||
selections={getValues().displayName}
|
||||
onSelect={(_, selectedValue) => {
|
||||
@ -229,12 +232,29 @@ export function UserDataTableAttributeSearchForm({
|
||||
</InputGroup>
|
||||
</div>
|
||||
{createAttributeSearchChips()}
|
||||
|
||||
<div className="pf-v5-u-pt-lg">
|
||||
<Controller
|
||||
name="exact"
|
||||
defaultValue={false}
|
||||
control={control}
|
||||
render={({ field }) => (
|
||||
<Checkbox
|
||||
id="exact"
|
||||
data-testid="exact"
|
||||
label={t("exactSearch")}
|
||||
isChecked={field.value}
|
||||
onChange={field.onChange}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<ActionGroup className="user-attribute-search-form-action-group">
|
||||
<Button
|
||||
data-testid="search-user-attribute-btn"
|
||||
variant="primary"
|
||||
type="submit"
|
||||
isDisabled={!activeFilters.length}
|
||||
isDisabled={!activeFilters.userAttribute.length}
|
||||
onClick={searchUserWithAttributes}
|
||||
>
|
||||
{t("search")}
|
||||
|
||||
@ -16,9 +16,9 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { useAccess } from "../../context/access/Access";
|
||||
import { SearchDropdown, SearchType } from "../../user/details/SearchFilter";
|
||||
import { UserAttribute } from "./UserDataTable";
|
||||
import { UserDataTableAttributeSearchForm } from "./UserDataTableAttributeSearchForm";
|
||||
import DropdownPanel from "../dropdown-panel/DropdownPanel";
|
||||
import { UserFilter } from "./UserDataTable";
|
||||
import { UserDataTableAttributeSearchForm } from "./UserDataTableAttributeSearchForm";
|
||||
|
||||
type UserDataTableToolbarItemsProps = {
|
||||
searchDropdownOpen: boolean;
|
||||
@ -32,8 +32,8 @@ type UserDataTableToolbarItemsProps = {
|
||||
setSearchType: (searchType: SearchType) => void;
|
||||
searchUser: string;
|
||||
setSearchUser: (searchUser: string) => void;
|
||||
activeFilters: UserAttribute[];
|
||||
setActiveFilters: (activeFilters: UserAttribute[]) => void;
|
||||
activeFilters: UserFilter;
|
||||
setActiveFilters: (activeFilters: UserFilter) => void;
|
||||
refresh: () => void;
|
||||
profile: UserProfileConfig;
|
||||
clearAllFilters: () => void;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user