added exact search option to attributes
Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
parent
b7eaa9b0cb
commit
a339e79d3e
3 changed files with 81 additions and 45 deletions
|
@ -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
|
||||
|
|
|
@ -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")}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue