keycloak-scim/js/apps/admin-ui/src/components/users/UserDataTableAttributeSearchForm.tsx
Andreas Blättlinger 35736b86ae
User Attribute Search (#19574)
Co-authored-by: Andreas Blaettlinger <bln1imb@bosch.com>
2023-06-12 14:17:54 +02:00

255 lines
7.1 KiB
TypeScript

import type UserProfileConfig from "@keycloak/keycloak-admin-client/lib/defs/userProfileConfig";
import {
ActionGroup,
Alert,
AlertVariant,
Button,
ButtonVariant,
InputGroup,
Select,
SelectOption,
SelectVariant,
Text,
TextContent,
TextVariants,
} from "@patternfly/react-core";
import { Form } from "react-router-dom";
import { KeycloakTextInput } from "../keycloak-text-input/KeycloakTextInput";
import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form";
import { isBundleKey, unWrap } from "../../user/utils";
import { CheckIcon } from "@patternfly/react-icons";
import { useAlerts } from "../alert/Alerts";
import { ReactNode, useState } from "react";
import { UserAttribute } from "./UserDataTable";
type UserDataTableAttributeSearchFormProps = {
activeFilters: UserAttribute[];
setActiveFilters: (filters: UserAttribute[]) => void;
profile: UserProfileConfig;
createAttributeSearchChips: () => ReactNode;
searchUserWithAttributes: () => void;
};
export function UserDataTableAttributeSearchForm({
activeFilters,
setActiveFilters,
profile,
createAttributeSearchChips,
searchUserWithAttributes,
}: UserDataTableAttributeSearchFormProps) {
const { t } = useTranslation("users");
const { addAlert } = useAlerts();
const [selectAttributeKeyOpen, setSelectAttributeKeyOpen] = useState(false);
const defaultValues: UserAttribute = {
name: "",
displayName: "",
value: "",
};
const {
getValues,
register,
reset,
formState: { errors },
setValue,
setError,
clearErrors,
} = useForm<UserAttribute>({
mode: "onChange",
defaultValues,
});
const isAttributeKeyDuplicate = () => {
return activeFilters.some((filter) => filter.name === getValues().name);
};
const isAttributeNameValid = () => {
let valid = false;
if (!getValues().name.length) {
setError("name", {
type: "empty",
message: t("searchUserByAttributeMissingKeyError"),
});
} else if (
activeFilters.some((filter) => filter.name === getValues().name)
) {
setError("name", {
type: "conflict",
message: t("searchUserByAttributeKeyAlreadyInUseError"),
});
} else {
valid = true;
}
return valid;
};
const isAttributeValueValid = () => {
let valid = false;
if (!getValues().value.length) {
setError("value", {
type: "empty",
message: t("searchUserByAttributeMissingValueError"),
});
} else {
valid = true;
}
return valid;
};
const isAttributeValid = () =>
isAttributeNameValid() && isAttributeValueValid();
const addToFilter = () => {
if (isAttributeValid()) {
setActiveFilters([
...activeFilters,
{
...getValues(),
},
]);
reset();
} else {
errors.name?.message &&
addAlert(errors.name.message, AlertVariant.danger);
errors.value?.message &&
addAlert(errors.value.message, AlertVariant.danger);
}
};
const clearActiveFilters = () => {
const filtered = [...activeFilters].filter(
(chip) => chip.name !== chip.name
);
setActiveFilters(filtered);
};
const createAttributeKeyInputField = () => {
if (profile) {
return (
<Select
data-testid="search-attribute-name"
variant={SelectVariant.typeahead}
onToggle={(isOpen) => setSelectAttributeKeyOpen(isOpen)}
selections={getValues().displayName}
onSelect={(_, selectedValue) => {
setValue("displayName", selectedValue.toString());
if (isAttributeKeyDuplicate()) {
setError("name", { type: "conflict" });
} else {
clearErrors("name");
}
}}
isOpen={selectAttributeKeyOpen}
placeholderText={t("selectAttribute")}
validated={errors.name && "error"}
maxHeight={300}
{...register("displayName", {
required: true,
validate: isAttributeNameValid,
})}
>
{profile.attributes?.map((option) => (
<SelectOption
key={option.name}
value={
(isBundleKey(option.displayName)
? t(unWrap(option.displayName!))
: option.displayName) || option.name
}
onClick={(e) => {
e.stopPropagation();
setSelectAttributeKeyOpen(false);
setValue("name", option.name!);
}}
/>
))}
</Select>
);
} else {
return (
<KeycloakTextInput
id="name"
placeholder={t("common:keyPlaceholder")}
validated={errors.name && "error"}
onKeyDown={(e) => e.key === "Enter" && addToFilter()}
{...register("name", {
required: true,
validate: isAttributeNameValid,
})}
/>
);
}
};
return (
<Form className="user-attribute-search-form">
<TextContent className="user-attribute-search-form-headline">
<Text component={TextVariants.h6}>{t("selectAttributes")}</Text>
</TextContent>
<Alert
isInline
className="user-attribute-search-form-alert"
variant="info"
title={t("searchUserByAttributeDescription")}
/>
<TextContent className="user-attribute-search-form-key-value">
<div className="user-attribute-search-form-left">
<Text component={TextVariants.h6}>{t("common:key")}</Text>
</div>
<div className="user-attribute-search-form-right">
<Text component={TextVariants.h6}>{t("common:value")}</Text>
</div>
</TextContent>
<div className="user-attribute-search-form-left">
{createAttributeKeyInputField()}
</div>
<div className="user-attribute-search-form-right">
<InputGroup>
<KeycloakTextInput
id="value"
placeholder={t("common:valuePlaceholder")}
validated={errors.value && "error"}
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
addToFilter();
}
}}
{...register("value", {
required: true,
validate: isAttributeValueValid,
})}
/>
<Button
variant="control"
icon={<CheckIcon />}
onClick={addToFilter}
/>
</InputGroup>
</div>
{createAttributeSearchChips()}
<ActionGroup className="user-attribute-search-form-action-group">
<Button
data-testid="search-user-attribute-btn"
variant="primary"
type="submit"
isDisabled={!activeFilters.length}
onClick={searchUserWithAttributes}
>
{t("common:search")}
</Button>
<Button
variant={ButtonVariant.link}
onClick={() => {
reset();
clearActiveFilters();
}}
>
{t("common:reset")}
</Button>
</ActionGroup>
</Form>
);
}