Skip to content

Commit a339e79

Browse files
edewitpedroigor
authored andcommitted
added exact search option to attributes
Signed-off-by: Erik Jan de Wit <[email protected]>
1 parent b7eaa9b commit a339e79

File tree

3 files changed

+81
-45
lines changed

3 files changed

+81
-45
lines changed

js/apps/admin-ui/src/components/users/UserDataTable.tsx

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ import { useState } from "react";
3333
import { useTranslation } from "react-i18next";
3434
import { Link, useNavigate } from "react-router-dom";
3535
import { useAdminClient } from "../../admin-client";
36+
import { fetchRealmInfo } from "../../context/auth/admin-ui-endpoint";
37+
import { UiRealmInfo } from "../../context/auth/uiRealmInfo";
3638
import { useRealm } from "../../context/realm-context/RealmContext";
3739
import { SearchType } from "../../user/details/SearchFilter";
3840
import { toAddUser } from "../../user/routes/AddUser";
@@ -41,8 +43,11 @@ import { emptyFormatter } from "../../util";
4143
import { useConfirmDialog } from "../confirm-dialog/ConfirmDialog";
4244
import { BruteUser, findUsers } from "../role-mapping/resource";
4345
import { UserDataTableToolbarItems } from "./UserDataTableToolbarItems";
44-
import { UiRealmInfo } from "../../context/auth/uiRealmInfo";
45-
import { fetchRealmInfo } from "../../context/auth/admin-ui-endpoint";
46+
47+
export type UserFilter = {
48+
exact: boolean;
49+
userAttribute: UserAttribute[];
50+
};
4651

4752
export type UserAttribute = {
4853
name: string;
@@ -119,7 +124,10 @@ export function UserDataTable() {
119124
const [selectedRows, setSelectedRows] = useState<UserRepresentation[]>([]);
120125
const [searchType, setSearchType] = useState<SearchType>("default");
121126
const [searchDropdownOpen, setSearchDropdownOpen] = useState(false);
122-
const [activeFilters, setActiveFilters] = useState<UserAttribute[]>([]);
127+
const [activeFilters, setActiveFilters] = useState<UserFilter>({
128+
exact: false,
129+
userAttribute: [],
130+
});
123131
const [profile, setProfile] = useState<UserProfileConfig>({});
124132
const [query, setQuery] = useState("");
125133

@@ -145,10 +153,11 @@ export function UserDataTable() {
145153
);
146154

147155
const loader = async (first?: number, max?: number, search?: string) => {
148-
const params: { [name: string]: string | number } = {
156+
const params: { [name: string]: string | number | boolean } = {
149157
first: first!,
150158
max: max!,
151159
q: query!,
160+
exact: activeFilters.exact,
152161
};
153162

154163
const searchParam = search || searchUser || "";
@@ -219,17 +228,16 @@ export function UserDataTable() {
219228
const listUsers = !uiRealmInfo.userProfileProvidersEnabled;
220229

221230
const clearAllFilters = () => {
222-
const filtered = [...activeFilters].filter(
223-
(chip) => chip.name !== chip.name,
224-
);
225-
setActiveFilters(filtered);
231+
setActiveFilters({ exact: false, userAttribute: [] });
226232
setSearchUser("");
227233
setQuery("");
228234
refresh();
229235
};
230236

231-
const createQueryString = (filters: UserAttribute[]) => {
232-
return filters.map((filter) => `${filter.name}:${filter.value}`).join(" ");
237+
const createQueryString = (filters: UserFilter) => {
238+
return filters.userAttribute
239+
.map((filter) => `${filter.name}:${filter.value}`)
240+
.join(" ");
233241
};
234242

235243
const searchUserWithAttributes = () => {
@@ -241,9 +249,9 @@ export function UserDataTable() {
241249
const createAttributeSearchChips = () => {
242250
return (
243251
<FlexItem>
244-
{activeFilters.length > 0 && (
252+
{activeFilters.userAttribute.length > 0 && (
245253
<>
246-
{Object.values(activeFilters).map((entry) => {
254+
{Object.values(activeFilters.userAttribute).map((entry) => {
247255
return (
248256
<ChipGroup
249257
className="pf-v5-u-mt-md pf-v5-u-mr-md"
@@ -256,13 +264,16 @@ export function UserDataTable() {
256264
onClick={(event) => {
257265
event.stopPropagation();
258266

259-
const filtered = [...activeFilters].filter(
267+
const filtered = [...activeFilters.userAttribute].filter(
260268
(chip) => chip.name !== entry.name,
261269
);
262-
const attributes = createQueryString(filtered);
270+
const active = {
271+
userAttribute: filtered,
272+
exact: activeFilters.exact,
273+
};
263274

264-
setActiveFilters(filtered);
265-
setQuery(attributes);
275+
setActiveFilters(active);
276+
setQuery(createQueryString(active));
266277
refresh();
267278
}}
268279
>
@@ -304,7 +315,7 @@ export function UserDataTable() {
304315
};
305316

306317
const subtoolbar = () => {
307-
if (!activeFilters.length) {
318+
if (!activeFilters.userAttribute.length) {
308319
return;
309320
}
310321
return (
@@ -329,7 +340,9 @@ export function UserDataTable() {
329340
<DeleteConfirm />
330341
<UnlockUsersConfirm />
331342
<KeycloakDataTable
332-
isSearching={searchUser !== "" || activeFilters.length !== 0}
343+
isSearching={
344+
searchUser !== "" || activeFilters.userAttribute.length !== 0
345+
}
333346
key={key}
334347
loader={loader}
335348
isPaginated

js/apps/admin-ui/src/components/users/UserDataTableAttributeSearchForm.tsx

Lines changed: 41 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import {
33
KeycloakSelect,
44
SelectVariant,
55
label,
6+
useAlerts,
67
} from "@keycloak/keycloak-ui-shared";
78
import {
89
ActionGroup,
910
Alert,
1011
AlertVariant,
1112
Button,
1213
ButtonVariant,
14+
Checkbox,
1315
InputGroup,
1416
InputGroupItem,
1517
SelectOption,
@@ -20,20 +22,21 @@ import {
2022
} from "@patternfly/react-core";
2123
import { CheckIcon } from "@patternfly/react-icons";
2224
import { ReactNode, useState } from "react";
23-
import { useForm } from "react-hook-form";
25+
import { Controller, useForm } from "react-hook-form";
2426
import { useTranslation } from "react-i18next";
2527
import { Form } from "react-router-dom";
26-
import { useAlerts } from "@keycloak/keycloak-ui-shared";
27-
import { UserAttribute } from "./UserDataTable";
28+
import { UserAttribute, UserFilter } from "./UserDataTable";
2829

2930
type UserDataTableAttributeSearchFormProps = {
30-
activeFilters: UserAttribute[];
31-
setActiveFilters: (filters: UserAttribute[]) => void;
31+
activeFilters: UserFilter;
32+
setActiveFilters: (filters: UserFilter) => void;
3233
profile: UserProfileConfig;
3334
createAttributeSearchChips: () => ReactNode;
3435
searchUserWithAttributes: () => void;
3536
};
3637

38+
type UserFilterForm = UserAttribute & { exact: boolean };
39+
3740
export function UserDataTableAttributeSearchForm({
3841
activeFilters,
3942
setActiveFilters,
@@ -59,13 +62,16 @@ export function UserDataTableAttributeSearchForm({
5962
setValue,
6063
setError,
6164
clearErrors,
62-
} = useForm<UserAttribute>({
65+
control,
66+
} = useForm<UserFilterForm>({
6367
mode: "onChange",
6468
defaultValues,
6569
});
6670

6771
const isAttributeKeyDuplicate = () => {
68-
return activeFilters.some((filter) => filter.name === getValues().name);
72+
return activeFilters.userAttribute.some(
73+
(filter) => filter.name === getValues().name,
74+
);
6975
};
7076

7177
const isAttributeNameValid = () => {
@@ -76,7 +82,9 @@ export function UserDataTableAttributeSearchForm({
7682
message: t("searchUserByAttributeMissingKeyError"),
7783
});
7884
} else if (
79-
activeFilters.some((filter) => filter.name === getValues().name)
85+
activeFilters.userAttribute.some(
86+
(filter) => filter.name === getValues().name,
87+
)
8088
) {
8189
setError("name", {
8290
type: "conflict",
@@ -106,13 +114,11 @@ export function UserDataTableAttributeSearchForm({
106114

107115
const addToFilter = () => {
108116
if (isAttributeValid()) {
109-
setActiveFilters([
110-
...activeFilters,
111-
{
112-
...getValues(),
113-
},
114-
]);
115-
reset();
117+
setActiveFilters({
118+
exact: getValues().exact,
119+
userAttribute: [...activeFilters.userAttribute, { ...getValues() }],
120+
});
121+
reset({ exact: getValues().exact });
116122
} else {
117123
if (errors.name?.message) {
118124
addAlert(errors.name.message, AlertVariant.danger);
@@ -125,10 +131,10 @@ export function UserDataTableAttributeSearchForm({
125131
};
126132

127133
const clearActiveFilters = () => {
128-
const filtered = [...activeFilters].filter(
134+
const filtered = [...activeFilters.userAttribute].filter(
129135
(chip) => chip.name !== chip.name,
130136
);
131-
setActiveFilters(filtered);
137+
setActiveFilters({ exact: getValues().exact, userAttribute: filtered });
132138
};
133139

134140
const createAttributeKeyInputField = () => {
@@ -244,12 +250,29 @@ export function UserDataTableAttributeSearchForm({
244250
</InputGroup>
245251
</div>
246252
{createAttributeSearchChips()}
253+
254+
<div className="pf-v5-u-pt-lg">
255+
<Controller
256+
name="exact"
257+
defaultValue={false}
258+
control={control}
259+
render={({ field }) => (
260+
<Checkbox
261+
id="exact"
262+
data-testid="exact"
263+
label={t("exactSearch")}
264+
isChecked={field.value}
265+
onChange={field.onChange}
266+
/>
267+
)}
268+
/>
269+
</div>
247270
<ActionGroup className="user-attribute-search-form-action-group">
248271
<Button
249272
data-testid="search-user-attribute-btn"
250273
variant="primary"
251274
type="submit"
252-
isDisabled={!activeFilters.length}
275+
isDisabled={!activeFilters.userAttribute.length}
253276
onClick={searchUserWithAttributes}
254277
>
255278
{t("search")}

js/apps/admin-ui/src/components/users/UserDataTableToolbarItems.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,24 +3,24 @@ import type { UserProfileConfig } from "@keycloak/keycloak-admin-client/lib/defs
33
import {
44
Button,
55
ButtonVariant,
6+
Dropdown,
7+
DropdownItem,
8+
DropdownList,
69
InputGroup,
7-
SearchInput,
8-
ToolbarItem,
910
InputGroupItem,
10-
Dropdown,
1111
MenuToggle,
12-
DropdownList,
13-
DropdownItem,
12+
SearchInput,
13+
ToolbarItem,
1414
} from "@patternfly/react-core";
1515
import { ArrowRightIcon, EllipsisVIcon } from "@patternfly/react-icons";
1616
import { ReactNode, useState } from "react";
1717
import { useTranslation } from "react-i18next";
1818

1919
import { useAccess } from "../../context/access/Access";
2020
import { SearchDropdown, SearchType } from "../../user/details/SearchFilter";
21-
import { UserAttribute } from "./UserDataTable";
22-
import { UserDataTableAttributeSearchForm } from "./UserDataTableAttributeSearchForm";
2321
import DropdownPanel from "../dropdown-panel/DropdownPanel";
22+
import { UserFilter } from "./UserDataTable";
23+
import { UserDataTableAttributeSearchForm } from "./UserDataTableAttributeSearchForm";
2424

2525
type UserDataTableToolbarItemsProps = {
2626
searchDropdownOpen: boolean;
@@ -34,8 +34,8 @@ type UserDataTableToolbarItemsProps = {
3434
setSearchType: (searchType: SearchType) => void;
3535
searchUser: string;
3636
setSearchUser: (searchUser: string) => void;
37-
activeFilters: UserAttribute[];
38-
setActiveFilters: (activeFilters: UserAttribute[]) => void;
37+
activeFilters: UserFilter;
38+
setActiveFilters: (activeFilters: UserFilter) => void;
3939
refresh: () => void;
4040
profile: UserProfileConfig;
4141
clearAllFilters: () => void;

0 commit comments

Comments
 (0)