diff --git a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/client_details/tabs/authorization_subtabs/PoliciesTab.ts b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/client_details/tabs/authorization_subtabs/PoliciesTab.ts index 29c44d1aef..6a5b7d65ff 100644 --- a/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/client_details/tabs/authorization_subtabs/PoliciesTab.ts +++ b/js/apps/admin-ui/cypress/support/pages/admin-ui/manage/clients/client_details/tabs/authorization_subtabs/PoliciesTab.ts @@ -29,6 +29,7 @@ export default class PoliciesTab extends CommonPage { inputClient(clientName: string) { cy.get("#clients").click(); cy.get("ul li").contains(clientName).click(); + cy.get("#clients").click(); return this; } } diff --git a/js/apps/admin-ui/src/clients/authorization/SearchDropdown.tsx b/js/apps/admin-ui/src/clients/authorization/SearchDropdown.tsx index c2d83ce970..c73e50a0f3 100644 --- a/js/apps/admin-ui/src/clients/authorization/SearchDropdown.tsx +++ b/js/apps/admin-ui/src/clients/authorization/SearchDropdown.tsx @@ -5,16 +5,11 @@ import { Dropdown, DropdownToggle, Form, - FormGroup, - Select, - SelectOption, - SelectVariant, } from "@patternfly/react-core"; import { useEffect } from "react"; -import { Controller, useForm } from "react-hook-form"; +import { FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; - -import { KeycloakTextInput } from "../../components/keycloak-text-input/KeycloakTextInput"; +import { SelectControl, TextControl } from "ui-shared"; import useToggle from "../../utils/useToggle"; import "./search-dropdown.css"; @@ -42,16 +37,14 @@ export const SearchDropdown = ({ type, }: SearchDropdownProps) => { const { t } = useTranslation(); + const form = useForm({ mode: "onChange" }); const { - register, - control, reset, formState: { isDirty }, handleSubmit, - } = useForm({ mode: "onChange" }); + } = form; const [open, toggle] = useToggle(); - const [typeOpen, toggleType] = useToggle(); const submit = (form: SearchForm) => { toggle(); @@ -60,21 +53,6 @@ export const SearchDropdown = ({ useEffect(() => reset(search), [search]); - const typeOptions = (value?: string) => [ - - {t("allTypes")} - , - ...(types || []).map((type) => ( - - {type.name} - - )), - ]; - return ( -
- - - - {type === "resource" && ( - <> - - - - - - - - - - - )} - {type !== "resource" && type !== "policy" && ( - - - - )} - {type !== "policy" && ( - - - - )} - {type !== "resource" && ( - - + + + {type === "resource" && ( + <> + + + + + )} + {type !== "resource" && type !== "policy" && ( + + )} + {type !== "policy" && } + {type !== "resource" && ( + ( - - )} + label={t("type")} + controller={{ + defaultValue: "", + }} + options={[ + { key: "", value: t("allTypes") }, + ...(types || []).map(({ type, name }) => ({ + key: type!, + value: name!, + })), + ]} /> - - )} - - - - -
+ )} + + + + + +
); }; diff --git a/js/apps/admin-ui/src/clients/authorization/Settings.tsx b/js/apps/admin-ui/src/clients/authorization/Settings.tsx index 411c3b8116..38bee61b3a 100644 --- a/js/apps/admin-ui/src/clients/authorization/Settings.tsx +++ b/js/apps/admin-ui/src/clients/authorization/Settings.tsx @@ -1,3 +1,4 @@ +import type ResourceServerRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceServerRepresentation"; import { AlertVariant, Button, @@ -5,24 +6,22 @@ import { FormGroup, PageSection, Radio, - Switch, } from "@patternfly/react-core"; import { useState } from "react"; import { Controller, FormProvider, useForm } from "react-hook-form"; import { useTranslation } from "react-i18next"; import { HelpItem } from "ui-shared"; - 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 { FixedButtonsGroup } from "../../components/form/FixedButtonGroup"; import { FormAccess } from "../../components/form/FormAccess"; import { KeycloakSpinner } from "../../components/keycloak-spinner/KeycloakSpinner"; +import { useAccess } from "../../context/access/Access"; +import { useFetch } from "../../utils/useFetch"; import useToggle from "../../utils/useToggle"; import { DecisionStrategySelect } from "./DecisionStrategySelect"; import { ImportDialog } from "./ImportDialog"; -import { useFetch } from "../../utils/useFetch"; -import { useAccess } from "../../context/access/Access"; const POLICY_ENFORCEMENT_MODES = [ "ENFORCING", @@ -145,35 +144,12 @@ export const AuthorizationSettings = ({ clientId }: { clientId: string }) => { - - - } - > - ( - - )} + label={t("allowRemoteResourceManagement")} + labelIcon={t("allowRemoteResourceManagementHelp")} /> - + reset(resource)} diff --git a/js/apps/admin-ui/src/clients/authorization/policy/Client.tsx b/js/apps/admin-ui/src/clients/authorization/policy/Client.tsx index 71b8c47117..fb6bcbdd1b 100644 --- a/js/apps/admin-ui/src/clients/authorization/policy/Client.tsx +++ b/js/apps/admin-ui/src/clients/authorization/policy/Client.tsx @@ -1,114 +1,18 @@ -import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; -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 { SelectVariant } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; -import { HelpItem } from "ui-shared"; - -import { adminClient } from "../../../admin-client"; -import { useFetch } from "../../../utils/useFetch"; +import { ClientSelect } from "../../../components/client/ClientSelect"; export const Client = () => { const { t } = useTranslation(); - const { - control, - getValues, - formState: { errors }, - } = useFormContext(); - const values: string[] | undefined = getValues("clients"); - - const [open, setOpen] = useState(false); - const [clients, setClients] = useState([]); - 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) => ( - - {option.clientId} - - )); return ( - - } - fieldId="clients" - helperTextInvalid={t("requiredClient")} - validated={errors.clients ? "error" : "default"} - isRequired - > - value.length > 0, - }} - render={({ field }) => ( - - )} - /> - + helpText={t("policyClientHelp")} + required + defaultValue={[]} + variant={SelectVariant.typeaheadMulti} + /> ); }; diff --git a/js/apps/admin-ui/src/components/client/ClientSelect.tsx b/js/apps/admin-ui/src/components/client/ClientSelect.tsx index 56ced6bd63..14a2cb9573 100644 --- a/js/apps/admin-ui/src/components/client/ClientSelect.tsx +++ b/js/apps/admin-ui/src/components/client/ClientSelect.tsx @@ -1,6 +1,6 @@ import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation"; 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 { useTranslation } from "react-i18next"; import { SelectControl } from "ui-shared"; @@ -9,7 +9,7 @@ import { adminClient } from "../../admin-client"; import { useFetch } from "../../utils/useFetch"; import type { ComponentProps } from "../dynamic/components"; -type ClientSelectProps = ComponentProps & {}; +type ClientSelectProps = ComponentProps & Pick; export const ClientSelect = ({ name, @@ -18,6 +18,7 @@ export const ClientSelect = ({ defaultValue, isDisabled = false, required = false, + variant = SelectVariant.typeahead, }: ClientSelectProps) => { const { t } = useTranslation(); @@ -54,12 +55,12 @@ export const ClientSelect = ({ }, }} onFilter={(value) => setSearch(value)} - variant={SelectVariant.typeahead} + variant={variant} isDisabled={isDisabled} - options={[ - { key: "", value: t("none") }, - ...clients.map(({ id, clientId }) => ({ key: id!, value: clientId! })), - ]} + options={clients.map(({ id, clientId }) => ({ + key: id!, + value: clientId!, + }))} /> ); }; diff --git a/js/libs/ui-shared/src/controls/SelectControl.tsx b/js/libs/ui-shared/src/controls/SelectControl.tsx index b1af7c568e..4014e68427 100644 --- a/js/libs/ui-shared/src/controls/SelectControl.tsx +++ b/js/libs/ui-shared/src/controls/SelectControl.tsx @@ -1,12 +1,3 @@ -import { useState } from "react"; -import { - Controller, - ControllerProps, - FieldValues, - FieldPath, - useFormContext, - UseControllerProps, -} from "react-hook-form"; import { Select, SelectOption, @@ -14,6 +5,15 @@ import { SelectVariant, ValidatedOptions, } from "@patternfly/react-core"; +import { useState } from "react"; +import { + Controller, + ControllerProps, + FieldPath, + FieldValues, + UseControllerProps, + useFormContext, +} from "react-hook-form"; import { FormLabel } from "./FormLabel"; export type SelectControlOption = { @@ -21,6 +21,8 @@ export type SelectControlOption = { value: string; }; +type OptionType = string[] | SelectControlOption[]; + export type SelectControlProps< T extends FieldValues, P extends FieldPath = FieldPath, @@ -37,12 +39,17 @@ export type SelectControlProps< UseControllerProps & { name: string; label?: string; - options: string[] | SelectControlOption[]; + options: OptionType; labelIcon?: string; controller: Omit; 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 = < T extends FieldValues, P extends FieldPath = FieldPath, @@ -68,19 +75,19 @@ export const SelectControl = < option.toString().toLowerCase().startsWith(lowercasePrefix), ) .map((option) => ( - - {typeof option === "string" ? option : option.value} + + {isString(option) ? option : option.value} )); }; + const isSelectBasedOptions = ( + options: OptionType, + ): options is SelectControlOption[] => typeof options[0] !== "string"; return ( @@ -94,8 +101,8 @@ export const SelectControl = < toggleId={name.slice(name.lastIndexOf(".") + 1)} onToggle={(isOpen) => setOpen(isOpen)} selections={ - typeof options[0] !== "string" - ? (options as SelectControlOption[]) + isSelectBasedOptions(options) + ? options .filter((o) => Array.isArray(value) ? value.includes(o.key) @@ -104,11 +111,15 @@ export const SelectControl = < .map((o) => o.value) : value } - onSelect={(_, v) => { - if (variant === "typeaheadmulti") { + onSelect={(event, v) => { + event.stopPropagation(); + if (Array.isArray(value)) { const option = v.toString(); - if (value.includes(option)) { - onChange(value.filter((item: string) => item !== option)); + const key = isSelectBasedOptions(options) + ? options.find((o) => o.value === option)?.key + : option; + if (value.includes(key)) { + onChange(value.filter((item: string) => item !== key)); } else { onChange([...value, option]); }