added exact search option to attributes

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-10-03 09:03:14 +02:00 committed by Pedro Igor
parent b7eaa9b0cb
commit a339e79d3e
3 changed files with 81 additions and 45 deletions

View file

@ -33,6 +33,8 @@ import { useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom";
import { useAdminClient } from "../../admin-client";
import { fetchRealmInfo } from "../../context/auth/admin-ui-endpoint";
import { UiRealmInfo } from "../../context/auth/uiRealmInfo";
import { useRealm } from "../../context/realm-context/RealmContext";
import { SearchType } from "../../user/details/SearchFilter";
import { toAddUser } from "../../user/routes/AddUser";
@ -41,8 +43,11 @@ import { emptyFormatter } from "../../util";
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
import { BruteUser, findUsers } from "../role-mapping/resource";
import { UserDataTableToolbarItems } from "./UserDataTableToolbarItems";
import { UiRealmInfo } from "../../context/auth/uiRealmInfo";
import { fetchRealmInfo } from "../../context/auth/admin-ui-endpoint";
export type UserFilter = {
exact: boolean;
userAttribute: UserAttribute[];
};
export type UserAttribute = {
name: string;
@ -119,7 +124,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("");
@ -145,10 +153,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 || "";
@ -219,17 +228,16 @@ export function UserDataTable() {
const listUsers = !uiRealmInfo.userProfileProvidersEnabled;
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 = () => {
@ -241,9 +249,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-v5-u-mt-md pf-v5-u-mr-md"
@ -256,13 +264,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();
}}
>
@ -304,7 +315,7 @@ export function UserDataTable() {
};
const subtoolbar = () => {
if (!activeFilters.length) {
if (!activeFilters.userAttribute.length) {
return;
}
return (
@ -329,7 +340,9 @@ export function UserDataTable() {
<DeleteConfirm />
<UnlockUsersConfirm />
<KeycloakDataTable
isSearching={searchUser !== "" || activeFilters.length !== 0}
isSearching={
searchUser !== "" || activeFilters.userAttribute.length !== 0
}
key={key}
loader={loader}
isPaginated

View file

@ -3,6 +3,7 @@ import {
KeycloakSelect,
SelectVariant,
label,
useAlerts,
} from "@keycloak/keycloak-ui-shared";
import {
ActionGroup,
@ -10,6 +11,7 @@ import {
AlertVariant,
Button,
ButtonVariant,
Checkbox,
InputGroup,
InputGroupItem,
SelectOption,
@ -20,20 +22,21 @@ import {
} 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 { useAlerts } from "@keycloak/keycloak-ui-shared";
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,
@ -59,13 +62,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 = () => {
@ -76,7 +82,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",
@ -106,13 +114,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 {
if (errors.name?.message) {
addAlert(errors.name.message, AlertVariant.danger);
@ -125,10 +131,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 = () => {
@ -244,12 +250,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")}

View file

@ -3,14 +3,14 @@ import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs
import {
Button,
ButtonVariant,
Dropdown,
DropdownItem,
DropdownList,
InputGroup,
InputGroupItem,
MenuToggle,
SearchInput,
ToolbarItem,
InputGroupItem,
Dropdown,
MenuToggle,
DropdownList,
DropdownItem,
} from "@patternfly/react-core";
import { ArrowRightIcon, EllipsisVIcon } from "@patternfly/react-icons";
import { ReactNode, useState } from "react";
@ -18,9 +18,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;
@ -34,8 +34,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;