Split out the key based into a sperate component (#2177)
This commit is contained in:
parent
d45e53f350
commit
8d5b2f903a
5 changed files with 303 additions and 204 deletions
|
@ -29,7 +29,7 @@ import type RoleRepresentation from "@keycloak/keycloak-admin-client/lib/defs/ro
|
||||||
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation";
|
import type ResourceEvaluation from "@keycloak/keycloak-admin-client/lib/defs/resourceEvaluation";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
import { AttributeInput } from "../../components/attribute-input/AttributeInput";
|
import { KeyBasedAttributeInput } from "./KeyBasedAttributeInput";
|
||||||
import { defaultContextAttributes } from "../utils";
|
import { defaultContextAttributes } from "../utils";
|
||||||
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
|
import type EvaluationResultRepresentation from "@keycloak/keycloak-admin-client/lib/defs/evaluationResultRepresentation";
|
||||||
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
import type ResourceRepresentation from "@keycloak/keycloak-admin-client/lib/defs/resourceRepresentation";
|
||||||
|
@ -518,13 +518,12 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
||||||
helperTextInvalid={t("common:required")}
|
helperTextInvalid={t("common:required")}
|
||||||
fieldId="resourcesAndAuthScopes"
|
fieldId="resourcesAndAuthScopes"
|
||||||
>
|
>
|
||||||
<AttributeInput
|
<KeyBasedAttributeInput
|
||||||
selectableValues={resources.map<AttributeType>((item) => ({
|
selectableValues={resources.map<AttributeType>((item) => ({
|
||||||
name: item.name!,
|
name: item.name!,
|
||||||
key: item._id!,
|
key: item._id!,
|
||||||
}))}
|
}))}
|
||||||
resources={resources}
|
resources={resources}
|
||||||
isKeySelectable
|
|
||||||
name="resources"
|
name="resources"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
@ -613,9 +612,8 @@ export const AuthorizationEvaluate = ({ client }: Props) => {
|
||||||
helperTextInvalid={t("common:required")}
|
helperTextInvalid={t("common:required")}
|
||||||
fieldId="contextualAttributes"
|
fieldId="contextualAttributes"
|
||||||
>
|
>
|
||||||
<AttributeInput
|
<KeyBasedAttributeInput
|
||||||
selectableValues={defaultContextAttributes}
|
selectableValues={defaultContextAttributes}
|
||||||
isKeySelectable
|
|
||||||
name="context.attributes"
|
name="context.attributes"
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
|
|
271
src/clients/authorization/KeyBasedAttributeInput.tsx
Normal file
271
src/clients/authorization/KeyBasedAttributeInput.tsx
Normal file
|
@ -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) => (
|
||||||
|
<SelectOption key={attr.key} value={attr.key}>
|
||||||
|
{attr.name}
|
||||||
|
</SelectOption>
|
||||||
|
));
|
||||||
|
} else if (scopeValues?.length) {
|
||||||
|
return scopeValues.map((scope) => (
|
||||||
|
<SelectOption key={scope.name} value={scope.name}>
|
||||||
|
{scope.name}
|
||||||
|
</SelectOption>
|
||||||
|
));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getMessageBundleKey = (attributeName: string) =>
|
||||||
|
camelCase(attributeName).replace(/\W/g, "");
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Td>
|
||||||
|
{resources || attributeValues?.length ? (
|
||||||
|
<Controller
|
||||||
|
name={`${name}[${rowIndex}].value`}
|
||||||
|
defaultValue={[]}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
id={`${attribute.id}-value`}
|
||||||
|
className="kc-attribute-value-selectable"
|
||||||
|
name={`${name}[${rowIndex}].value`}
|
||||||
|
chipGroupProps={{
|
||||||
|
numChips: 1,
|
||||||
|
expandedText: t("common:hide"),
|
||||||
|
collapsedText: t("common:showRemaining"),
|
||||||
|
}}
|
||||||
|
toggleId={`group-${name}`}
|
||||||
|
onToggle={(open) => toggleValueSelect(rowIndex, open)}
|
||||||
|
isOpen={isValueOpenArray[rowIndex]}
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||||
|
placeholderText={t("clients:selectOrTypeAKey")}
|
||||||
|
selections={value}
|
||||||
|
onSelect={(_, v) => {
|
||||||
|
onChange(v);
|
||||||
|
|
||||||
|
toggleValueSelect(rowIndex, false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{renderSelectOptionType()}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<TextInput
|
||||||
|
id={`${getMessageBundleKey(attribute.key)}-value`}
|
||||||
|
className="value-input"
|
||||||
|
name={`${name}[${rowIndex}].value`}
|
||||||
|
ref={register()}
|
||||||
|
defaultValue={attribute.value}
|
||||||
|
data-testid="attribute-value-input"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Td>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<TableComposable
|
||||||
|
className="kc-attributes__table"
|
||||||
|
aria-label="Role attribute keys and values"
|
||||||
|
variant="compact"
|
||||||
|
borders={false}
|
||||||
|
>
|
||||||
|
<Thead>
|
||||||
|
<Tr>
|
||||||
|
<Th id="key" width={40}>
|
||||||
|
{t("key")}
|
||||||
|
</Th>
|
||||||
|
<Th id="value" width={40}>
|
||||||
|
{t("value")}
|
||||||
|
</Th>
|
||||||
|
</Tr>
|
||||||
|
</Thead>
|
||||||
|
<Tbody>
|
||||||
|
{fields.map((attribute, rowIndex) => (
|
||||||
|
<Tr key={attribute.id} data-testid="attribute-row">
|
||||||
|
<Td>
|
||||||
|
<Controller
|
||||||
|
name={`${name}[${rowIndex}].key`}
|
||||||
|
defaultValue={attribute.key}
|
||||||
|
control={control}
|
||||||
|
render={({ onChange, value }) => (
|
||||||
|
<Select
|
||||||
|
id={`${name}[${rowIndex}].key`}
|
||||||
|
className="kc-attribute-key-selectable"
|
||||||
|
name={`${name}[${rowIndex}].key`}
|
||||||
|
toggleId={`group-${name}`}
|
||||||
|
onToggle={(open) => toggleKeySelect(rowIndex, open)}
|
||||||
|
isOpen={isKeyOpenArray[rowIndex]}
|
||||||
|
variant={SelectVariant.typeahead}
|
||||||
|
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||||
|
placeholderText={t("clients:selectOrTypeAKey")}
|
||||||
|
selections={value}
|
||||||
|
onSelect={(_, v) => {
|
||||||
|
onChange(v.toString());
|
||||||
|
|
||||||
|
toggleKeySelect(rowIndex, false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{selectableValues?.map((attribute) => (
|
||||||
|
<SelectOption
|
||||||
|
selected={attribute.name === value}
|
||||||
|
key={attribute.key}
|
||||||
|
value={resources ? attribute.name : attribute.key}
|
||||||
|
>
|
||||||
|
{attribute.name}
|
||||||
|
</SelectOption>
|
||||||
|
))}
|
||||||
|
</Select>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</Td>
|
||||||
|
<ValueInput
|
||||||
|
name={name}
|
||||||
|
attribute={attribute}
|
||||||
|
rowIndex={rowIndex}
|
||||||
|
selectableValues={selectableValues}
|
||||||
|
resources={resources}
|
||||||
|
/>
|
||||||
|
<Td key="minus-button" id={`kc-minus-button-${rowIndex}`}>
|
||||||
|
<Button
|
||||||
|
id={`minus-button-${rowIndex}`}
|
||||||
|
variant="link"
|
||||||
|
className="kc-attributes__minus-icon"
|
||||||
|
onClick={() => remove(rowIndex)}
|
||||||
|
>
|
||||||
|
<MinusCircleIcon />
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
))}
|
||||||
|
<Tr>
|
||||||
|
<Td>
|
||||||
|
<Button
|
||||||
|
aria-label={t("roles:addAttributeText")}
|
||||||
|
id="plus-icon"
|
||||||
|
variant="link"
|
||||||
|
className="kc-attributes__plus-icon"
|
||||||
|
onClick={() => {
|
||||||
|
append({ key: "", value: "" });
|
||||||
|
setIsKeyOpenArray([...isKeyOpenArray, false]);
|
||||||
|
}}
|
||||||
|
icon={<PlusCircleIcon />}
|
||||||
|
isDisabled={!watchLastValue}
|
||||||
|
data-testid="attribute-add-row"
|
||||||
|
>
|
||||||
|
{t("roles:addAttributeText")}
|
||||||
|
</Button>
|
||||||
|
</Td>
|
||||||
|
</Tr>
|
||||||
|
</Tbody>
|
||||||
|
</TableComposable>
|
||||||
|
);
|
||||||
|
};
|
|
@ -5,10 +5,7 @@ import { ActionGroup, Button } from "@patternfly/react-core";
|
||||||
|
|
||||||
import type { RoleRepresentation } from "../../model/role-model";
|
import type { RoleRepresentation } from "../../model/role-model";
|
||||||
import type { KeyValueType } from "./attribute-convert";
|
import type { KeyValueType } from "./attribute-convert";
|
||||||
import {
|
import { AttributeInput } from "../attribute-input/AttributeInput";
|
||||||
AttributeInput,
|
|
||||||
AttributeType,
|
|
||||||
} from "../attribute-input/AttributeInput";
|
|
||||||
import { FormAccess } from "../form-access/FormAccess";
|
import { FormAccess } from "../form-access/FormAccess";
|
||||||
|
|
||||||
export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
||||||
|
@ -17,19 +14,11 @@ export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
||||||
|
|
||||||
export type AttributesFormProps = {
|
export type AttributesFormProps = {
|
||||||
form: UseFormMethods<AttributeForm>;
|
form: UseFormMethods<AttributeForm>;
|
||||||
isKeySelectable?: boolean;
|
|
||||||
selectableValues?: AttributeType[];
|
|
||||||
save?: (model: AttributeForm) => void;
|
save?: (model: AttributeForm) => void;
|
||||||
reset?: () => void;
|
reset?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AttributesForm = ({
|
export const AttributesForm = ({ form, reset, save }: AttributesFormProps) => {
|
||||||
form,
|
|
||||||
reset,
|
|
||||||
save,
|
|
||||||
isKeySelectable,
|
|
||||||
selectableValues,
|
|
||||||
}: AttributesFormProps) => {
|
|
||||||
const { t } = useTranslation("roles");
|
const { t } = useTranslation("roles");
|
||||||
const noSaveCancelButtons = !save && !reset;
|
const noSaveCancelButtons = !save && !reset;
|
||||||
const {
|
const {
|
||||||
|
@ -43,11 +32,7 @@ export const AttributesForm = ({
|
||||||
onSubmit={save ? handleSubmit(save) : undefined}
|
onSubmit={save ? handleSubmit(save) : undefined}
|
||||||
>
|
>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
<AttributeInput
|
<AttributeInput name="attributes" />
|
||||||
isKeySelectable={isKeySelectable}
|
|
||||||
selectableValues={selectableValues}
|
|
||||||
name="attributes"
|
|
||||||
/>
|
|
||||||
</FormProvider>
|
</FormProvider>
|
||||||
{!noSaveCancelButtons && (
|
{!noSaveCancelButtons && (
|
||||||
<ActionGroup className="kc-attributes__action-group">
|
<ActionGroup className="kc-attributes__action-group">
|
||||||
|
|
|
@ -1,13 +1,7 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||||
import {
|
import { Button, TextInput } from "@patternfly/react-core";
|
||||||
Button,
|
|
||||||
Select,
|
|
||||||
SelectOption,
|
|
||||||
SelectVariant,
|
|
||||||
TextInput,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import {
|
import {
|
||||||
TableComposable,
|
TableComposable,
|
||||||
Tbody,
|
Tbody,
|
||||||
|
@ -19,142 +13,27 @@ import {
|
||||||
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
||||||
|
|
||||||
import "../attribute-form/attribute-form.css";
|
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 = {
|
type AttributeInputProps = {
|
||||||
name: string;
|
name: string;
|
||||||
selectableValues?: AttributeType[];
|
|
||||||
isKeySelectable?: boolean;
|
|
||||||
resources?: ResourceRepresentation[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AttributeInput = ({
|
export const AttributeInput = ({ name }: AttributeInputProps) => {
|
||||||
name,
|
|
||||||
isKeySelectable,
|
|
||||||
selectableValues,
|
|
||||||
resources,
|
|
||||||
}: AttributeInputProps) => {
|
|
||||||
const { t } = useTranslation("common");
|
const { t } = useTranslation("common");
|
||||||
const { control, register, watch, getValues } = useFormContext();
|
const { control, register, watch } = useFormContext();
|
||||||
const { fields, append, remove } = useFieldArray({
|
const { fields, append, remove } = useFieldArray({
|
||||||
control: control,
|
control: control,
|
||||||
name,
|
name,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const watchLast = watch(`${name}[${fields.length - 1}].key`, "");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!fields.length) {
|
if (!fields.length) {
|
||||||
append({ key: "", value: "" });
|
append({ key: "", value: "" });
|
||||||
}
|
}
|
||||||
}, [fields]);
|
}, [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) => (
|
|
||||||
<SelectOption key={attr.key} value={attr.key}>
|
|
||||||
{attr.name}
|
|
||||||
</SelectOption>
|
|
||||||
));
|
|
||||||
} else if (scopeValues?.length) {
|
|
||||||
return scopeValues.map((scope) => (
|
|
||||||
<SelectOption key={scope.name} value={scope.name}>
|
|
||||||
{scope.name}
|
|
||||||
</SelectOption>
|
|
||||||
));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getMessageBundleKey = (attributeName: string) =>
|
|
||||||
camelCase(attributeName).replace(/\W/g, "");
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Td>
|
|
||||||
{resources || attributeValues?.length ? (
|
|
||||||
<Controller
|
|
||||||
name={`${name}[${rowIndex}].value`}
|
|
||||||
defaultValue={[]}
|
|
||||||
control={control}
|
|
||||||
render={({ onChange, value }) => (
|
|
||||||
<Select
|
|
||||||
id={`${attribute.id}-value`}
|
|
||||||
className="kc-attribute-value-selectable"
|
|
||||||
name={`${name}[${rowIndex}].value`}
|
|
||||||
chipGroupProps={{
|
|
||||||
numChips: 1,
|
|
||||||
expandedText: t("common:hide"),
|
|
||||||
collapsedText: t("common:showRemaining"),
|
|
||||||
}}
|
|
||||||
toggleId={`group-${name}`}
|
|
||||||
onToggle={(open) => toggleValueSelect(rowIndex, open)}
|
|
||||||
isOpen={isValueOpenArray[rowIndex]}
|
|
||||||
variant={SelectVariant.typeahead}
|
|
||||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
|
||||||
placeholderText={t("clients:selectOrTypeAKey")}
|
|
||||||
selections={value}
|
|
||||||
onSelect={(_, v) => {
|
|
||||||
onChange(v);
|
|
||||||
|
|
||||||
toggleValueSelect(rowIndex, false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{renderSelectOptionType()}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextInput
|
|
||||||
id={`${getMessageBundleKey(attribute.key)}-value`}
|
|
||||||
className="value-input"
|
|
||||||
name={`${name}[${rowIndex}].value`}
|
|
||||||
ref={register()}
|
|
||||||
defaultValue={attribute.value}
|
|
||||||
data-testid="attribute-value-input"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Td>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TableComposable
|
<TableComposable
|
||||||
className="kc-attributes__table"
|
className="kc-attributes__table"
|
||||||
|
@ -176,52 +55,23 @@ export const AttributeInput = ({
|
||||||
{fields.map((attribute, rowIndex) => (
|
{fields.map((attribute, rowIndex) => (
|
||||||
<Tr key={attribute.id} data-testid="attribute-row">
|
<Tr key={attribute.id} data-testid="attribute-row">
|
||||||
<Td>
|
<Td>
|
||||||
{isKeySelectable ? (
|
<TextInput
|
||||||
<Controller
|
id={`${attribute.id}-key`}
|
||||||
name={`${name}[${rowIndex}].key`}
|
name={`${name}[${rowIndex}].key`}
|
||||||
defaultValue={attribute.key}
|
ref={register()}
|
||||||
control={control}
|
defaultValue={attribute.key}
|
||||||
render={({ onChange, value }) => (
|
data-testid="attribute-key-input"
|
||||||
<Select
|
/>
|
||||||
id={`${name}[${rowIndex}].key`}
|
</Td>
|
||||||
className="kc-attribute-key-selectable"
|
<Td>
|
||||||
name={`${name}[${rowIndex}].key`}
|
<TextInput
|
||||||
toggleId={`group-${name}`}
|
id={`${attribute.id}-value`}
|
||||||
onToggle={(open) => toggleKeySelect(rowIndex, open)}
|
name={`${name}[${rowIndex}].value`}
|
||||||
isOpen={isKeyOpenArray[rowIndex]}
|
ref={register()}
|
||||||
variant={SelectVariant.typeahead}
|
defaultValue={attribute.value}
|
||||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
data-testid="attribute-value-input"
|
||||||
placeholderText={t("clients:selectOrTypeAKey")}
|
/>
|
||||||
selections={value}
|
|
||||||
onSelect={(_, v) => {
|
|
||||||
onChange(v.toString());
|
|
||||||
|
|
||||||
toggleKeySelect(rowIndex, false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{selectableValues?.map((attribute) => (
|
|
||||||
<SelectOption
|
|
||||||
selected={attribute.name === value}
|
|
||||||
key={attribute.key}
|
|
||||||
value={resources ? attribute.name : attribute.key}
|
|
||||||
>
|
|
||||||
{attribute.name}
|
|
||||||
</SelectOption>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<TextInput
|
|
||||||
id={`${attribute.id}-key`}
|
|
||||||
name={`${name}[${rowIndex}].key`}
|
|
||||||
ref={register()}
|
|
||||||
defaultValue={attribute.key}
|
|
||||||
data-testid="attribute-key-input"
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Td>
|
</Td>
|
||||||
{renderValueInput(rowIndex, attribute)}
|
|
||||||
<Td key="minus-button" id={`kc-minus-button-${rowIndex}`}>
|
<Td key="minus-button" id={`kc-minus-button-${rowIndex}`}>
|
||||||
<Button
|
<Button
|
||||||
id={`minus-button-${rowIndex}`}
|
id={`minus-button-${rowIndex}`}
|
||||||
|
@ -241,14 +91,9 @@ export const AttributeInput = ({
|
||||||
id="plus-icon"
|
id="plus-icon"
|
||||||
variant="link"
|
variant="link"
|
||||||
className="kc-attributes__plus-icon"
|
className="kc-attributes__plus-icon"
|
||||||
onClick={() => {
|
onClick={() => append({ key: "", value: "" })}
|
||||||
append({ key: "", value: "" });
|
|
||||||
if (isKeySelectable) {
|
|
||||||
setIsKeyOpenArray([...isKeyOpenArray, false]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
icon={<PlusCircleIcon />}
|
icon={<PlusCircleIcon />}
|
||||||
isDisabled={isKeySelectable ? !watchLastValue : !watchLastKey}
|
isDisabled={!watchLast}
|
||||||
data-testid="attribute-add-row"
|
data-testid="attribute-add-row"
|
||||||
>
|
>
|
||||||
{t("roles:addAttributeText")}
|
{t("roles:addAttributeText")}
|
||||||
|
|
|
@ -182,7 +182,7 @@ export default function RealmRoleTabs() {
|
||||||
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
|
||||||
titleKey: "roles:roleDeleteConfirm",
|
titleKey: "roles:roleDeleteConfirm",
|
||||||
messageKey: t("roles:roleDeleteConfirmDialog", {
|
messageKey: t("roles:roleDeleteConfirmDialog", {
|
||||||
selectedRoleName: role?.name || t("createRole"),
|
name: role?.name || t("createRole"),
|
||||||
}),
|
}),
|
||||||
continueButtonLabel: "common:delete",
|
continueButtonLabel: "common:delete",
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
|
|
Loading…
Reference in a new issue