changed to use ui-shared (#27933)

Signed-off-by: Erik Jan de Wit <erikjan.dewit@gmail.com>
This commit is contained in:
Erik Jan de Wit 2024-03-22 11:27:09 +01:00 committed by GitHub
parent 5a99c558dc
commit 53d52ecf15
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 114 additions and 289 deletions

View file

@ -29,6 +29,7 @@ export default class PoliciesTab extends CommonPage {
inputClient(clientName: string) { inputClient(clientName: string) {
cy.get("#clients").click(); cy.get("#clients").click();
cy.get("ul li").contains(clientName).click(); cy.get("ul li").contains(clientName).click();
cy.get("#clients").click();
return this; return this;
} }
} }

View file

@ -5,16 +5,11 @@ import {
Dropdown, Dropdown,
DropdownToggle, DropdownToggle,
Form, Form,
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useEffect } from "react"; import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form"; import { FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SelectControl, TextControl } from "ui-shared";
import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput";
import useToggle from "../../utils/useToggle"; import useToggle from "../../utils/useToggle";
import "./search-dropdown.css"; import "./search-dropdown.css";
@ -42,16 +37,14 @@ export const SearchDropdown = ({
type, type,
}: SearchDropdownProps) => { }: SearchDropdownProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const form = useForm<SearchForm>({ mode: "onChange" });
const { const {
register,
control,
reset, reset,
formState: { isDirty }, formState: { isDirty },
handleSubmit, handleSubmit,
} = useForm<SearchForm>({ mode: "onChange" }); } = form;
const [open, toggle] = useToggle(); const [open, toggle] = useToggle();
const [typeOpen, toggleType] = useToggle();
const submit = (form: SearchForm) => { const submit = (form: SearchForm) => {
toggle(); toggle();
@ -60,21 +53,6 @@ export const SearchDropdown = ({
useEffect(() => reset(search), [search]); useEffect(() => reset(search), [search]);
const typeOptions = (value?: string) => [
<SelectOption key="empty" value="">
{t("allTypes")}
</SelectOption>,
...(types || []).map((type) => (
<SelectOption
selected={type.type === value}
key={type.type}
value={type.type}
>
{type.name}
</SelectOption>
)),
];
return ( return (
<Dropdown <Dropdown
data-testid="searchdropdown_dorpdown" data-testid="searchdropdown_dorpdown"
@ -91,86 +69,39 @@ export const SearchDropdown = ({
} }
isOpen={open} isOpen={open}
> >
<FormProvider {...form}>
<Form <Form
isHorizontal isHorizontal
className="keycloak__client_authentication__searchdropdown_form" className="keycloak__client_authentication__searchdropdown_form"
onSubmit={handleSubmit(submit)} onSubmit={handleSubmit(submit)}
> >
<FormGroup label={t("name")} fieldId="name"> <TextControl name="name" label={t("name")} />
<KeycloakTextInput
id="name"
data-testid="searchdropdown_name"
{...register("name")}
/>
</FormGroup>
{type === "resource" && ( {type === "resource" && (
<> <>
<FormGroup label={t("type")} fieldId="type"> <TextControl name="type" label={t("type")} />
<KeycloakTextInput <TextControl name="uris" label={t("uris")} />
id="type" <TextControl name="owner" label={t("owner")} />
data-testid="searchdropdown_type"
{...register("type")}
/>
</FormGroup>
<FormGroup label={t("uris")} fieldId="uri">
<KeycloakTextInput
id="uri"
data-testid="searchdropdown_uri"
{...register("uri")}
/>
</FormGroup>
<FormGroup label={t("owner")} fieldId="owner">
<KeycloakTextInput
id="owner"
data-testid="searchdropdown_owner"
{...register("owner")}
/>
</FormGroup>
</> </>
)} )}
{type !== "resource" && type !== "policy" && ( {type !== "resource" && type !== "policy" && (
<FormGroup label={t("resource")} fieldId="resource"> <TextControl name="resource" label={t("resource")} />
<KeycloakTextInput
id="resource"
data-testid="searchdropdown_resource"
{...register("resource")}
/>
</FormGroup>
)}
{type !== "policy" && (
<FormGroup label={t("scope")} fieldId="scope">
<KeycloakTextInput
id="scope"
data-testid="searchdropdown_scope"
{...register("scope")}
/>
</FormGroup>
)} )}
{type !== "policy" && <TextControl name="scope" label={t("scope")} />}
{type !== "resource" && ( {type !== "resource" && (
<FormGroup label={t("type")} fieldId="type"> <SelectControl
<Controller
name="type" name="type"
defaultValue="" label={t("type")}
control={control} controller={{
render={({ field }) => ( defaultValue: "",
<Select
toggleId="type"
onToggle={toggleType}
onSelect={(event, value) => {
event.stopPropagation();
field.onChange(value);
toggleType();
}} }}
selections={field.value || t("allTypes")} options={[
variant={SelectVariant.single} { key: "", value: t("allTypes") },
aria-label={t("type")} ...(types || []).map(({ type, name }) => ({
isOpen={typeOpen} key: type!,
> value: name!,
{typeOptions(field.value)} })),
</Select> ]}
)}
/> />
</FormGroup>
)} )}
<ActionGroup> <ActionGroup>
<Button <Button
@ -190,6 +121,7 @@ export const SearchDropdown = ({
</Button> </Button>
</ActionGroup> </ActionGroup>
</Form> </Form>
</FormProvider>
</Dropdown> </Dropdown>
); );
}; };

View file

@ -1,3 +1,4 @@
import type ResourceServerRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceServerRepresentation";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -5,24 +6,22 @@ import {
FormGroup, FormGroup,
PageSection, PageSection,
Radio, Radio,
Switch,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useState } from "react"; import { useState } from "react";
import { Controller, FormProvider, useForm } from "react-hook-form"; import { Controller, FormProvider, useForm } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared"; import { HelpItem } from "ui-shared";
import { adminClient } from "../../admin-client"; import { adminClient } from "../../admin-client";
import type ResourceServerRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceServerRepresentation"; import { DefaultSwitchControl } from "../../components/SwitchControl";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { FixedButtonsGroup } from "../../components/form/FixedButtonGroup"; import { FixedButtonsGroup } from "../../components/form/FixedButtonGroup";
import { FormAccess } from "../../components/form/FormAccess"; import { FormAccess } from "../../components/form/FormAccess";
import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner";
import { useAccess } from "../../context/access/Access";
import { useFetch } from "../../utils/useFetch";
import useToggle from "../../utils/useToggle"; import useToggle from "../../utils/useToggle";
import { DecisionStrategySelect } from "./DecisionStrategySelect"; import { DecisionStrategySelect } from "./DecisionStrategySelect";
import { ImportDialog } from "./ImportDialog"; import { ImportDialog } from "./ImportDialog";
import { useFetch } from "../../utils/useFetch";
import { useAccess } from "../../context/access/Access";
const POLICY_ENFORCEMENT_MODES = [ const POLICY_ENFORCEMENT_MODES = [
"ENFORCING", "ENFORCING",
@ -145,35 +144,12 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => {
</FormGroup> </FormGroup>
<FormProvider {...form}> <FormProvider {...form}>
<DecisionStrategySelect isLimited /> <DecisionStrategySelect isLimited />
</FormProvider> <DefaultSwitchControl
<FormGroup
hasNoPaddingTop
label={t("allowRemoteResourceManagement")}
fieldId="allowRemoteResourceManagement"
labelIcon={
<HelpItem
helpText={t("allowRemoteResourceManagementHelp")}
fieldLabelId="allowRemoteResourceManagement"
/>
}
>
<Controller
name="allowRemoteResourceManagement" name="allowRemoteResourceManagement"
data-testid="allowRemoteResourceManagement" label={t("allowRemoteResourceManagement")}
defaultValue={false} labelIcon={t("allowRemoteResourceManagementHelp")}
control={control}
render={({ field }) => (
<Switch
id="allowRemoteResourceManagement"
label={t("on")}
labelOff={t("off")}
isChecked={field.value}
onChange={field.onChange}
aria-label={t("allowRemoteResourceManagement")}
/> />
)} </FormProvider>
/>
</FormGroup>
<FixedButtonsGroup <FixedButtonsGroup
name="authenticationSettings" name="authenticationSettings"
reset={() => reset(resource)} reset={() => reset(resource)}

View file

@ -1,114 +1,18 @@
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import { SelectVariant } from "@patternfly/react-core";
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
import {
FormGroup,
Select,
SelectOption,
SelectVariant,
} from "@patternfly/react-core";
import { useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { HelpItem } from "ui-shared"; import { ClientSelect } from "../../../components/client/ClientSelect";
import { adminClient } from "../../../admin-client";
import { useFetch } from "../../../utils/useFetch";
export const Client = () => { export const Client = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const {
control,
getValues,
formState: { errors },
} = useFormContext();
const values: string[] | undefined = getValues("clients");
const [open, setOpen] = useState(false);
const [clients, setClients] = useState<ClientRepresentation[]>([]);
const [search, setSearch] = useState("");
useFetch(
async () => {
const params: ClientQuery = {
max: 20,
};
if (search) {
params.clientId = search;
params.search = true;
}
if (values?.length && !search) {
return await Promise.all(
values.map(
(id: string) =>
adminClient.clients.findOne({ id }) as ClientRepresentation,
),
);
}
return await adminClient.clients.find(params);
},
setClients,
[search],
);
const convert = (clients: ClientRepresentation[]) =>
clients.map((option) => (
<SelectOption
key={option.id!}
value={option.id}
selected={values?.includes(option.id!)}
>
{option.clientId}
</SelectOption>
));
return ( return (
<FormGroup <ClientSelect
label={t("clients")}
labelIcon={
<HelpItem helpText={t("policyClientHelp")} fieldLabelId="client" />
}
fieldId="clients"
helperTextInvalid={t("requiredClient")}
validated={errors.clients ? "error" : "default"}
isRequired
>
<Controller
name="clients" name="clients"
label={t("clients")}
helpText={t("policyClientHelp")}
required
defaultValue={[]} defaultValue={[]}
control={control}
rules={{
validate: (value) => value.length > 0,
}}
render={({ field }) => (
<Select
toggleId="clients"
variant={SelectVariant.typeaheadMulti} variant={SelectVariant.typeaheadMulti}
typeAheadAriaLabel={t("clients")}
onToggle={(open) => setOpen(open)}
isOpen={open}
selections={field.value}
aria-label={t("selectClients")}
onFilter={(_, value) => {
setSearch(value);
return convert(clients);
}}
onSelect={(_, v) => {
const option = v.toString();
if (field.value.includes(option)) {
field.onChange(
field.value.filter((item: string) => item !== option),
);
} else {
field.onChange([...field.value, option]);
}
setOpen(false);
}}
>
{convert(clients)}
</Select>
)}
/> />
</FormGroup>
); );
}; };

View file

@ -1,6 +1,6 @@
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients"; import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
import { SelectVariant } from "@patternfly/react-core"; import { SelectProps, SelectVariant } from "@patternfly/react-core";
import { useState } from "react"; import { useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { SelectControl } from "ui-shared"; import { SelectControl } from "ui-shared";
@ -9,7 +9,7 @@ import { adminClient } from "../../admin-client";
import { useFetch } from "../../utils/useFetch"; import { useFetch } from "../../utils/useFetch";
import type { ComponentProps } from "../dynamic/components"; import type { ComponentProps } from "../dynamic/components";
type ClientSelectProps = ComponentProps & {}; type ClientSelectProps = ComponentProps & Pick<SelectProps, "variant">;
export const ClientSelect = ({ export const ClientSelect = ({
name, name,
@ -18,6 +18,7 @@ export const ClientSelect = ({
defaultValue, defaultValue,
isDisabled = false, isDisabled = false,
required = false, required = false,
variant = SelectVariant.typeahead,
}: ClientSelectProps) => { }: ClientSelectProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
@ -54,12 +55,12 @@ export const ClientSelect = ({
}, },
}} }}
onFilter={(value) => setSearch(value)} onFilter={(value) => setSearch(value)}
variant={SelectVariant.typeahead} variant={variant}
isDisabled={isDisabled} isDisabled={isDisabled}
options={[ options={clients.map(({ id, clientId }) => ({
{ key: "", value: t("none") }, key: id!,
...clients.map(({ id, clientId }) => ({ key: id!, value: clientId! })), value: clientId!,
]} }))}
/> />
); );
}; };

View file

@ -1,12 +1,3 @@
import { useState } from "react";
import {
Controller,
ControllerProps,
FieldValues,
FieldPath,
useFormContext,
UseControllerProps,
} from "react-hook-form";
import { import {
Select, Select,
SelectOption, SelectOption,
@ -14,6 +5,15 @@ import {
SelectVariant, SelectVariant,
ValidatedOptions, ValidatedOptions,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { useState } from "react";
import {
Controller,
ControllerProps,
FieldPath,
FieldValues,
UseControllerProps,
useFormContext,
} from "react-hook-form";
import { FormLabel } from "./FormLabel"; import { FormLabel } from "./FormLabel";
export type SelectControlOption = { export type SelectControlOption = {
@ -21,6 +21,8 @@ export type SelectControlOption = {
value: string; value: string;
}; };
type OptionType = string[] | SelectControlOption[];
export type SelectControlProps< export type SelectControlProps<
T extends FieldValues, T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>, P extends FieldPath<T> = FieldPath<T>,
@ -37,12 +39,17 @@ export type SelectControlProps<
UseControllerProps<T, P> & { UseControllerProps<T, P> & {
name: string; name: string;
label?: string; label?: string;
options: string[] | SelectControlOption[]; options: OptionType;
labelIcon?: string; labelIcon?: string;
controller: Omit<ControllerProps, "name" | "render">; controller: Omit<ControllerProps, "name" | "render">;
onFilter?: (value: string) => void; onFilter?: (value: string) => void;
}; };
const isString = (option: SelectControlOption | string): option is string =>
typeof option === "string";
const key = (option: SelectControlOption | string) =>
isString(option) ? option : option.key;
export const SelectControl = < export const SelectControl = <
T extends FieldValues, T extends FieldValues,
P extends FieldPath<T> = FieldPath<T>, P extends FieldPath<T> = FieldPath<T>,
@ -68,19 +75,19 @@ export const SelectControl = <
option.toString().toLowerCase().startsWith(lowercasePrefix), option.toString().toLowerCase().startsWith(lowercasePrefix),
) )
.map((option) => ( .map((option) => (
<SelectOption <SelectOption key={key(option)} value={key(option)}>
key={typeof option === "string" ? option : option.key} {isString(option) ? option : option.value}
value={typeof option === "string" ? option : option.key}
>
{typeof option === "string" ? option : option.value}
</SelectOption> </SelectOption>
)); ));
}; };
const isSelectBasedOptions = (
options: OptionType,
): options is SelectControlOption[] => typeof options[0] !== "string";
return ( return (
<FormLabel <FormLabel
name={name} name={name}
label={label} label={label}
isRequired={controller.rules?.required === true} isRequired={!!controller.rules?.required}
error={errors[name]} error={errors[name]}
labelIcon={labelIcon} labelIcon={labelIcon}
> >
@ -94,8 +101,8 @@ export const SelectControl = <
toggleId={name.slice(name.lastIndexOf(".") + 1)} toggleId={name.slice(name.lastIndexOf(".") + 1)}
onToggle={(isOpen) => setOpen(isOpen)} onToggle={(isOpen) => setOpen(isOpen)}
selections={ selections={
typeof options[0] !== "string" isSelectBasedOptions(options)
? (options as SelectControlOption[]) ? options
.filter((o) => .filter((o) =>
Array.isArray(value) Array.isArray(value)
? value.includes(o.key) ? value.includes(o.key)
@ -104,11 +111,15 @@ export const SelectControl = <
.map((o) => o.value) .map((o) => o.value)
: value : value
} }
onSelect={(_, v) => { onSelect={(event, v) => {
if (variant === "typeaheadmulti") { event.stopPropagation();
if (Array.isArray(value)) {
const option = v.toString(); const option = v.toString();
if (value.includes(option)) { const key = isSelectBasedOptions(options)
onChange(value.filter((item: string) => item !== option)); ? options.find((o) => o.value === option)?.key
: option;
if (value.includes(key)) {
onChange(value.filter((item: string) => item !== key));
} else { } else {
onChange([...value, option]); onChange([...value, option]);
} }