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 { 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
|
||||||
|
|
|
@ -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")}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
Loading…
Reference in a new issue