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 { useTranslation } from "react-i18next";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import { useAdminClient } from "../../admin-client"; 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 { useRealm } from "../../context/realm-context/RealmContext";
import { SearchType } from "../../user/details/SearchFilter"; import { SearchType } from "../../user/details/SearchFilter";
import { toAddUser } from "../../user/routes/AddUser"; import { toAddUser } from "../../user/routes/AddUser";
@ -41,8 +43,11 @@ import { emptyFormatter } from "../../util";
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
import { BruteUser, findUsers } from "../role-mapping/resource"; import { BruteUser, findUsers } from "../role-mapping/resource";
import { UserDataTableToolbarItems } from "./UserDataTableToolbarItems"; 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 = { export type UserAttribute = {
name: string; name: string;
@ -119,7 +124,10 @@ export function UserDataTable() {
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]); const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
const [searchType, setSearchType] = useState<SearchType>("default"); const [searchType, setSearchType] = useState<SearchType>("default");
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false); const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
const [activeFilters, setActiveFilters] = useState<UserAttribute[]>([]); const [activeFilters, setActiveFilters] = useState<UserFilter>({
exact: false,
userAttribute: [],
});
const [profile, setProfile] = useState<UserProfileConfig>({}); const [profile, setProfile] = useState<UserProfileConfig>({});
const [query, setQuery] = useState(""); const [query, setQuery] = useState("");
@ -145,10 +153,11 @@ export function UserDataTable() {
); );
const loader = async (first?: number, max?: number, search?: string) => { const loader = async (first?: number, max?: number, search?: string) => {
const params: { [name: string]: string | number } = { const params: { [name: string]: string | number | boolean } = {
first: first!, first: first!,
max: max!, max: max!,
q: query!, q: query!,
exact: activeFilters.exact,
}; };
const searchParam = search || searchUser || ""; const searchParam = search || searchUser || "";
@ -219,17 +228,16 @@ export function UserDataTable() {
const listUsers = !uiRealmInfo.userProfileProvidersEnabled; const listUsers = !uiRealmInfo.userProfileProvidersEnabled;
const clearAllFilters = () => { const clearAllFilters = () => {
const filtered = [...activeFilters].filter( setActiveFilters({ exact: false, userAttribute: [] });
(chip) => chip.name !== chip.name,
);
setActiveFilters(filtered);
setSearchUser(""); setSearchUser("");
setQuery(""); setQuery("");
refresh(); refresh();
}; };
const createQueryString = (filters: UserAttribute[]) => { const createQueryString = (filters: UserFilter) => {
return filters.map((filter) => `${filter.name}:${filter.value}`).join(" "); return filters.userAttribute
.map((filter) => `${filter.name}:${filter.value}`)
.join(" ");
}; };
const searchUserWithAttributes = () => { const searchUserWithAttributes = () => {
@ -241,9 +249,9 @@ export function UserDataTable() {
const createAttributeSearchChips = () => { const createAttributeSearchChips = () => {
return ( return (
<FlexItem> <FlexItem>
{activeFilters.length > 0 && ( {activeFilters.userAttribute.length > 0 && (
<> <>
{Object.values(activeFilters).map((entry) => { {Object.values(activeFilters.userAttribute).map((entry) => {
return ( return (
<ChipGroup <ChipGroup
className="pf-v5-u-mt-md pf-v5-u-mr-md" className="pf-v5-u-mt-md pf-v5-u-mr-md"
@ -256,13 +264,16 @@ export function UserDataTable() {
onClick={(event) => { onClick={(event) => {
event.stopPropagation(); event.stopPropagation();
const filtered = [...activeFilters].filter( const filtered = [...activeFilters.userAttribute].filter(
(chip) => chip.name !== entry.name, (chip) => chip.name !== entry.name,
); );
const attributes = createQueryString(filtered); const active = {
userAttribute: filtered,
exact: activeFilters.exact,
};
setActiveFilters(filtered); setActiveFilters(active);
setQuery(attributes); setQuery(createQueryString(active));
refresh(); refresh();
}} }}
> >
@ -304,7 +315,7 @@ export function UserDataTable() {
}; };
const subtoolbar = () => { const subtoolbar = () => {
if (!activeFilters.length) { if (!activeFilters.userAttribute.length) {
return; return;
} }
return ( return (
@ -329,7 +340,9 @@ export function UserDataTable() {
<DeleteConfirm /> <DeleteConfirm />
<UnlockUsersConfirm /> <UnlockUsersConfirm />
<KeycloakDataTable <KeycloakDataTable
isSearching={searchUser !== "" || activeFilters.length !== 0} isSearching={
searchUser !== "" || activeFilters.userAttribute.length !== 0
}
key={key} key={key}
loader={loader} loader={loader}
isPaginated isPaginated

View file

@ -3,6 +3,7 @@ import {
KeycloakSelect, KeycloakSelect,
SelectVariant, SelectVariant,
label, label,
useAlerts,
} from "@keycloak/keycloak-ui-shared"; } from "@keycloak/keycloak-ui-shared";
import { import {
ActionGroup, ActionGroup,
@ -10,6 +11,7 @@ import {
AlertVariant, AlertVariant,
Button, Button,
ButtonVariant, ButtonVariant,
Checkbox,
InputGroup, InputGroup,
InputGroupItem, InputGroupItem,
SelectOption, SelectOption,
@ -20,20 +22,21 @@ import {
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { CheckIcon } from "@patternfly/react-icons"; import { CheckIcon } from "@patternfly/react-icons";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
import { useForm } from "react-hook-form"; import { Controller, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Form } from "react-router-dom"; import { Form } from "react-router-dom";
import { useAlerts } from "@keycloak/keycloak-ui-shared"; import { UserAttribute, UserFilter } from "./UserDataTable";
import { UserAttribute } from "./UserDataTable";
type UserDataTableAttributeSearchFormProps = { type UserDataTableAttributeSearchFormProps = {
activeFilters: UserAttribute[]; activeFilters: UserFilter;
setActiveFilters: (filters: UserAttribute[]) => void; setActiveFilters: (filters: UserFilter) => void;
profile: UserProfileConfig; profile: UserProfileConfig;
createAttributeSearchChips: () => ReactNode; createAttributeSearchChips: () => ReactNode;
searchUserWithAttributes: () => void; searchUserWithAttributes: () => void;
}; };
type UserFilterForm = UserAttribute & { exact: boolean };
export function UserDataTableAttributeSearchForm({ export function UserDataTableAttributeSearchForm({
activeFilters, activeFilters,
setActiveFilters, setActiveFilters,
@ -59,13 +62,16 @@ export function UserDataTableAttributeSearchForm({
setValue, setValue,
setError, setError,
clearErrors, clearErrors,
} = useForm<UserAttribute>({ control,
} = useForm<UserFilterForm>({
mode: "onChange", mode: "onChange",
defaultValues, defaultValues,
}); });
const isAttributeKeyDuplicate = () => { const isAttributeKeyDuplicate = () => {
return activeFilters.some((filter) => filter.name === getValues().name); return activeFilters.userAttribute.some(
(filter) => filter.name === getValues().name,
);
}; };
const isAttributeNameValid = () => { const isAttributeNameValid = () => {
@ -76,7 +82,9 @@ export function UserDataTableAttributeSearchForm({
message: t("searchUserByAttributeMissingKeyError"), message: t("searchUserByAttributeMissingKeyError"),
}); });
} else if ( } else if (
activeFilters.some((filter) => filter.name === getValues().name) activeFilters.userAttribute.some(
(filter) => filter.name === getValues().name,
)
) { ) {
setError("name", { setError("name", {
type: "conflict", type: "conflict",
@ -106,13 +114,11 @@ export function UserDataTableAttributeSearchForm({
const addToFilter = () => { const addToFilter = () => {
if (isAttributeValid()) { if (isAttributeValid()) {
setActiveFilters([ setActiveFilters({
...activeFilters, exact: getValues().exact,
{ userAttribute: [...activeFilters.userAttribute, { ...getValues() }],
...getValues(), });
}, reset({ exact: getValues().exact });
]);
reset();
} else { } else {
if (errors.name?.message) { if (errors.name?.message) {
addAlert(errors.name.message, AlertVariant.danger); addAlert(errors.name.message, AlertVariant.danger);
@ -125,10 +131,10 @@ export function UserDataTableAttributeSearchForm({
}; };
const clearActiveFilters = () => { const clearActiveFilters = () => {
const filtered = [...activeFilters].filter( const filtered = [...activeFilters.userAttribute].filter(
(chip) => chip.name !== chip.name, (chip) => chip.name !== chip.name,
); );
setActiveFilters(filtered); setActiveFilters({ exact: getValues().exact, userAttribute: filtered });
}; };
const createAttributeKeyInputField = () => { const createAttributeKeyInputField = () => {
@ -244,12 +250,29 @@ export function UserDataTableAttributeSearchForm({
</InputGroup> </InputGroup>
</div> </div>
{createAttributeSearchChips()} {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"> <ActionGroup className="user-attribute-search-form-action-group">
<Button <Button
data-testid="search-user-attribute-btn" data-testid="search-user-attribute-btn"
variant="primary" variant="primary"
type="submit" type="submit"
isDisabled={!activeFilters.length} isDisabled={!activeFilters.userAttribute.length}
onClick={searchUserWithAttributes} onClick={searchUserWithAttributes}
> >
{t("search")} {t("search")}

View file

@ -3,14 +3,14 @@ import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs
import { import {
Button, Button,
ButtonVariant, ButtonVariant,
Dropdown,
DropdownItem,
DropdownList,
InputGroup, InputGroup,
InputGroupItem,
MenuToggle,
SearchInput, SearchInput,
ToolbarItem, ToolbarItem,
InputGroupItem,
Dropdown,
MenuToggle,
DropdownList,
DropdownItem,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { ArrowRightIcon, EllipsisVIcon } from "@patternfly/react-icons"; import { ArrowRightIcon, EllipsisVIcon } from "@patternfly/react-icons";
import { ReactNode, useState } from "react"; import { ReactNode, useState } from "react";
@ -18,9 +18,9 @@ import { useTranslation } from "react-i18next";
import { useAccess } from "../../context/access/Access"; import { useAccess } from "../../context/access/Access";
import { SearchDropdown, SearchType } from "../../user/details/SearchFilter"; import { SearchDropdown, SearchType } from "../../user/details/SearchFilter";
import { UserAttribute } from "./UserDataTable";
import { UserDataTableAttributeSearchForm } from "./UserDataTableAttributeSearchForm";
import DropdownPanel from "../dropdown-panel/DropdownPanel"; import DropdownPanel from "../dropdown-panel/DropdownPanel";
import { UserFilter } from "./UserDataTable";
import { UserDataTableAttributeSearchForm } from "./UserDataTableAttributeSearchForm";
type UserDataTableToolbarItemsProps = { type UserDataTableToolbarItemsProps = {
searchDropdownOpen: boolean; searchDropdownOpen: boolean;
@ -34,8 +34,8 @@ type UserDataTableToolbarItemsProps = {
setSearchType: (searchType: SearchType) => void; setSearchType: (searchType: SearchType) => void;
searchUser: string; searchUser: string;
setSearchUser: (searchUser: string) => void; setSearchUser: (searchUser: string) => void;
activeFilters: UserAttribute[]; activeFilters: UserFilter;
setActiveFilters: (activeFilters: UserAttribute[]) => void; setActiveFilters: (activeFilters: UserFilter) => void;
refresh: () => void; refresh: () => void;
profile: UserProfileConfig; profile: UserProfileConfig;
clearAllFilters: () => void; clearAllFilters: () => void;