diff --git a/src/clients/authorization/AuthorizationEvaluate.tsx b/src/clients/authorization/AuthorizationEvaluate.tsx index 42d74200b9..52b37ad2e5 100644 --- a/src/clients/authorization/AuthorizationEvaluate.tsx +++ b/src/clients/authorization/AuthorizationEvaluate.tsx @@ -29,7 +29,7 @@ import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/ro import { useAdminClient, useFetch } from "../../context/auth/AdminClient"; import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation"; import { useRealm } from "../../context/realm-context/RealmContext"; -import { AttributeInput } from "../../components/attribute-input/AttributeInput"; +import { KeyBasedAttributeInput } from "./KeyBasedAttributeInput"; import { defaultContextAttributes } from "../utils"; import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation"; import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation"; @@ -518,13 +518,12 @@ export const AuthorizationEvaluate = ({ client }: Props) => { helperTextInvalid={t("common:required")} fieldId="resourcesAndAuthScopes" > - ((item) => ({ name: item.name!, key: item._id!, }))} resources={resources} - isKeySelectable name="resources" /> @@ -613,9 +612,8 @@ export const AuthorizationEvaluate = ({ client }: Props) => { helperTextInvalid={t("common:required")} fieldId="contextualAttributes" > - diff --git a/src/clients/authorization/KeyBasedAttributeInput.tsx b/src/clients/authorization/KeyBasedAttributeInput.tsx new file mode 100644 index 0000000000..3046afacdb --- /dev/null +++ b/src/clients/authorization/KeyBasedAttributeInput.tsx @@ -0,0 +1,271 @@ +import React, { useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Controller, useFieldArray, useFormContext } from "react-hook-form"; +import { + Button, + Select, + SelectOption, + SelectVariant, + TextInput, +} from "@patternfly/react-core"; +import { + TableComposable, + Tbody, + Td, + Th, + Thead, + Tr, +} from "@patternfly/react-table"; +import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons"; + +import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation"; +import { defaultContextAttributes } from "../utils"; +import { camelCase } from "lodash-es"; + +import "../../components/attribute-form/attribute-form.css"; + +export type AttributeType = { + key?: string; + name: string; + custom?: boolean; + values?: { + [key: string]: string; + }[]; +}; + +type AttributeInputProps = { + name: string; + selectableValues?: AttributeType[]; + resources?: ResourceRepresentation[]; +}; + +type ValueInputProps = { + name: string; + rowIndex: number; + attribute: any; + selectableValues?: AttributeType[]; + resources?: ResourceRepresentation[]; +}; + +const ValueInput = ({ + name, + rowIndex, + attribute, + selectableValues, + resources, +}: ValueInputProps) => { + const { t } = useTranslation("common"); + const { control, register, getValues } = useFormContext(); + + const [isValueOpenArray, setIsValueOpenArray] = useState([false]); + + const toggleValueSelect = (rowIndex: number, open: boolean) => { + const arr = [...isValueOpenArray]; + arr[rowIndex] = open; + setIsValueOpenArray(arr); + }; + + let attributeValues: { key: string; name: string }[] | undefined = []; + + const scopeValues = resources?.find( + (resource) => resource.name === getValues().resources[rowIndex]?.key + )?.scopes; + + if (selectableValues) { + attributeValues = defaultContextAttributes.find( + (attr) => attr.key === getValues().context[rowIndex]?.key + )?.values; + } + + const renderSelectOptionType = () => { + if (attributeValues?.length && !resources) { + return attributeValues.map((attr) => ( + + {attr.name} + + )); + } else if (scopeValues?.length) { + return scopeValues.map((scope) => ( + + {scope.name} + + )); + } + }; + + const getMessageBundleKey = (attributeName: string) => + camelCase(attributeName).replace(/\W/g, ""); + + return ( + + {resources || attributeValues?.length ? ( + ( + + )} + /> + ) : ( + + )} + + ); +}; + +export const KeyBasedAttributeInput = ({ + name, + selectableValues, + resources, +}: AttributeInputProps) => { + const { t } = useTranslation("common"); + const { control, watch } = useFormContext(); + const { fields, append, remove } = useFieldArray({ + control: control, + name, + }); + + const [isKeyOpenArray, setIsKeyOpenArray] = useState([false]); + const toggleKeySelect = (rowIndex: number, open: boolean) => { + const arr = [...isKeyOpenArray]; + arr[rowIndex] = open; + setIsKeyOpenArray(arr); + }; + + useEffect(() => { + if (!fields.length) { + append({ key: "", value: "" }); + } + }, [fields]); + + const watchLastValue = watch(`${name}[${fields.length - 1}].value`, ""); + + return ( + + + + + {t("key")} + + + {t("value")} + + + + + {fields.map((attribute, rowIndex) => ( + + + ( + + )} + /> + + + + + + + ))} + + + + + + + + ); +}; diff --git a/src/components/attribute-form/AttributeForm.tsx b/src/components/attribute-form/AttributeForm.tsx index e3da98a9a5..8af0030d97 100644 --- a/src/components/attribute-form/AttributeForm.tsx +++ b/src/components/attribute-form/AttributeForm.tsx @@ -5,10 +5,7 @@ import { ActionGroup, Button } from "@patternfly/react-core"; import type { RoleRepresentation } from "../../model/role-model"; import type { KeyValueType } from "./attribute-convert"; -import { - AttributeInput, - AttributeType, -} from "../attribute-input/AttributeInput"; +import { AttributeInput } from "../attribute-input/AttributeInput"; import { FormAccess } from "../form-access/FormAccess"; export type AttributeForm = Omit & { @@ -17,19 +14,11 @@ export type AttributeForm = Omit & { export type AttributesFormProps = { form: UseFormMethods; - isKeySelectable?: boolean; - selectableValues?: AttributeType[]; save?: (model: AttributeForm) => void; reset?: () => void; }; -export const AttributesForm = ({ - form, - reset, - save, - isKeySelectable, - selectableValues, -}: AttributesFormProps) => { +export const AttributesForm = ({ form, reset, save }: AttributesFormProps) => { const { t } = useTranslation("roles"); const noSaveCancelButtons = !save && !reset; const { @@ -43,11 +32,7 @@ export const AttributesForm = ({ onSubmit={save ? handleSubmit(save) : undefined} > - + {!noSaveCancelButtons && ( diff --git a/src/components/attribute-input/AttributeInput.tsx b/src/components/attribute-input/AttributeInput.tsx index 5d5e848d97..52ee74838d 100644 --- a/src/components/attribute-input/AttributeInput.tsx +++ b/src/components/attribute-input/AttributeInput.tsx @@ -1,13 +1,7 @@ -import React, { useEffect, useState } from "react"; +import React, { useEffect } from "react"; import { useTranslation } from "react-i18next"; -import { Controller, useFieldArray, useFormContext } from "react-hook-form"; -import { - Button, - Select, - SelectOption, - SelectVariant, - TextInput, -} from "@patternfly/react-core"; +import { useFieldArray, useFormContext } from "react-hook-form"; +import { Button, TextInput } from "@patternfly/react-core"; import { TableComposable, Tbody, @@ -19,142 +13,27 @@ import { import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons"; import "../attribute-form/attribute-form.css"; -import { defaultContextAttributes } from "../../clients/utils"; -import { camelCase } from "lodash-es"; -import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation"; - -export type AttributeType = { - key?: string; - name: string; - custom?: boolean; - values?: { - [key: string]: string; - }[]; -}; type AttributeInputProps = { name: string; - selectableValues?: AttributeType[]; - isKeySelectable?: boolean; - resources?: ResourceRepresentation[]; }; -export const AttributeInput = ({ - name, - isKeySelectable, - selectableValues, - resources, -}: AttributeInputProps) => { +export const AttributeInput = ({ name }: AttributeInputProps) => { const { t } = useTranslation("common"); - const { control, register, watch, getValues } = useFormContext(); + const { control, register, watch } = useFormContext(); const { fields, append, remove } = useFieldArray({ control: control, name, }); + const watchLast = watch(`${name}[${fields.length - 1}].key`, ""); + useEffect(() => { if (!fields.length) { append({ key: "", value: "" }); } }, [fields]); - const [isKeyOpenArray, setIsKeyOpenArray] = useState([false]); - const watchLastKey = watch(`${name}[${fields.length - 1}].key`, ""); - const watchLastValue = watch(`${name}[${fields.length - 1}].value`, ""); - - const [isValueOpenArray, setIsValueOpenArray] = useState([false]); - const toggleKeySelect = (rowIndex: number, open: boolean) => { - const arr = [...isKeyOpenArray]; - arr[rowIndex] = open; - setIsKeyOpenArray(arr); - }; - - const toggleValueSelect = (rowIndex: number, open: boolean) => { - const arr = [...isValueOpenArray]; - arr[rowIndex] = open; - setIsValueOpenArray(arr); - }; - - const renderValueInput = (rowIndex: number, attribute: any) => { - let attributeValues: { key: string; name: string }[] | undefined = []; - - const scopeValues = resources?.find( - (resource) => resource.name === getValues().resources[rowIndex]?.key - )?.scopes; - - if (selectableValues) { - attributeValues = defaultContextAttributes.find( - (attr) => attr.key === getValues().context[rowIndex]?.key - )?.values; - } - - const renderSelectOptionType = () => { - if (attributeValues?.length && !resources) { - return attributeValues.map((attr) => ( - - {attr.name} - - )); - } else if (scopeValues?.length) { - return scopeValues.map((scope) => ( - - {scope.name} - - )); - } - }; - - const getMessageBundleKey = (attributeName: string) => - camelCase(attributeName).replace(/\W/g, ""); - - return ( - - {resources || attributeValues?.length ? ( - ( - - )} - /> - ) : ( - - )} - - ); - }; - return ( ( - {isKeySelectable ? ( - ( - - )} - /> - ) : ( - - )} + + + + - {renderValueInput(rowIndex, attribute)}