Adds selectable attribute functionality (#1840)
* index on authEvaluateTab: f0a2494d
Add nexus profile for releases (#1704)
auth evaluate wip
wip auth evaluate tab
add identity information and permissions
help text and wip evaluate
* wip contextual attributes
* wip contextual attributes
* add conditional dropdown for auth method
* key and value can be saved now
* wip selectable save
* selectable attribute input save success
* changes to AttributeInput only
* revert files to versions from master
* add back defaultContextAttributes
* set id
* delete autheval file
* remove isKeySelectable
* lift props
* localize names and separate dropdown from the other attribute inputs
* remove log stmt
* add placeholder, return implicitly
* fix select/save
* reorder hook and conditional
* translate, fields/save working
This commit is contained in:
parent
dcfb84e7d1
commit
105d0dffb0
7 changed files with 254 additions and 26 deletions
|
@ -37,6 +37,31 @@ export default {
|
|||
searchByName: "Search by name",
|
||||
setup: "Setup",
|
||||
evaluate: "Evaluate",
|
||||
selectOrTypeAKey: "Select or type a key",
|
||||
custom: "Custom Attribute...",
|
||||
kc: {
|
||||
identity: {
|
||||
authc: {
|
||||
method: "Authentication Method",
|
||||
},
|
||||
},
|
||||
realm: {
|
||||
name: "Realm",
|
||||
},
|
||||
time: {
|
||||
date_time: "Date/Time (MM/dd/yyyy hh:mm:ss)",
|
||||
},
|
||||
client: {
|
||||
network: {
|
||||
ip_address: "Client IPv4 Address",
|
||||
host: "Client Host",
|
||||
},
|
||||
user_agent: "Client/User Agent",
|
||||
},
|
||||
},
|
||||
password: "Password",
|
||||
oneTimePassword: "One-Time Password",
|
||||
kerberos: "Kerberos",
|
||||
assignRole: "Assign role",
|
||||
unAssignRole: "Unassign",
|
||||
removeMappingTitle: "Remove mapping?",
|
||||
|
|
|
@ -19,3 +19,49 @@ export const getProtocolName = (t: TFunction<"clients">, protocol: string) => {
|
|||
|
||||
return protocol;
|
||||
};
|
||||
|
||||
export const defaultContextAttributes = [
|
||||
{
|
||||
key: "custom",
|
||||
name: "Custom Attribute...",
|
||||
custom: true,
|
||||
},
|
||||
{
|
||||
key: "kc.identity.authc.method",
|
||||
name: "Authentication Method",
|
||||
values: [
|
||||
{
|
||||
key: "pwd",
|
||||
name: "Password",
|
||||
},
|
||||
{
|
||||
key: "otp",
|
||||
name: "One-Time Password",
|
||||
},
|
||||
{
|
||||
key: "kbr",
|
||||
name: "Kerberos",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "kc.realm.name",
|
||||
name: "Realm",
|
||||
},
|
||||
{
|
||||
key: "kc.time.date_time",
|
||||
name: "Date/Time (MM/dd/yyyy hh:mm:ss)",
|
||||
},
|
||||
{
|
||||
key: "kc.client.network.ip_address",
|
||||
name: "Client IPv4 Address",
|
||||
},
|
||||
{
|
||||
key: "kc.client.network.host",
|
||||
name: "Client Host",
|
||||
},
|
||||
{
|
||||
key: "kc.client.user_agent",
|
||||
name: "Client/User Agent",
|
||||
},
|
||||
];
|
||||
|
|
|
@ -14,11 +14,19 @@ export type AttributeForm = Omit<RoleRepresentation, "attributes"> & {
|
|||
|
||||
export type AttributesFormProps = {
|
||||
form: UseFormMethods<AttributeForm>;
|
||||
isKeySelectable?: boolean;
|
||||
selectableValues?: string[];
|
||||
save?: (model: AttributeForm) => void;
|
||||
reset?: () => void;
|
||||
};
|
||||
|
||||
export const AttributesForm = ({ form, reset, save }: AttributesFormProps) => {
|
||||
export const AttributesForm = ({
|
||||
form,
|
||||
reset,
|
||||
save,
|
||||
isKeySelectable,
|
||||
selectableValues,
|
||||
}: AttributesFormProps) => {
|
||||
const { t } = useTranslation("roles");
|
||||
const noSaveCancelButtons = !save && !reset;
|
||||
const {
|
||||
|
@ -32,7 +40,11 @@ export const AttributesForm = ({ form, reset, save }: AttributesFormProps) => {
|
|||
onSubmit={save ? handleSubmit(save) : undefined}
|
||||
>
|
||||
<FormProvider {...form}>
|
||||
<AttributeInput name="attributes" />
|
||||
<AttributeInput
|
||||
isKeySelectable={isKeySelectable}
|
||||
selectableValues={selectableValues}
|
||||
name="attributes"
|
||||
/>
|
||||
</FormProvider>
|
||||
{!noSaveCancelButtons && (
|
||||
<ActionGroup className="kc-attributes__action-group">
|
||||
|
|
|
@ -20,4 +20,12 @@
|
|||
--pf-c-form__group--m-action--MarginTop: calc(
|
||||
var(--pf-global--spacer--2xl) - var(--pf-global--spacer--sm)
|
||||
);
|
||||
}
|
||||
|
||||
.pf-c-select.kc-attribute-key-selectable {
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.pf-c-select.kc-attribute-value-selectable {
|
||||
width: 350px;
|
||||
}
|
|
@ -1,7 +1,14 @@
|
|||
import React, { useEffect } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useFieldArray, useFormContext } from "react-hook-form";
|
||||
import { Button, TextInput } from "@patternfly/react-core";
|
||||
// import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
Button,
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
TextInput,
|
||||
} from "@patternfly/react-core";
|
||||
import {
|
||||
TableComposable,
|
||||
Tbody,
|
||||
|
@ -11,17 +18,36 @@ import {
|
|||
Tr,
|
||||
} from "@patternfly/react-table";
|
||||
import { MinusCircleIcon, PlusCircleIcon } from "@patternfly/react-icons";
|
||||
import { Controller, useFieldArray, useFormContext } from "react-hook-form";
|
||||
|
||||
import "../attribute-form/attribute-form.css";
|
||||
import { defaultContextAttributes } from "../../clients/utils";
|
||||
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?: string[];
|
||||
isKeySelectable?: boolean;
|
||||
resources?: ResourceRepresentation[];
|
||||
};
|
||||
|
||||
export const AttributeInput = ({ name }: AttributeInputProps) => {
|
||||
export const AttributeInput = ({
|
||||
name,
|
||||
isKeySelectable,
|
||||
selectableValues,
|
||||
}: AttributeInputProps) => {
|
||||
const { t } = useTranslation("common");
|
||||
const { control, register, watch } = useFormContext();
|
||||
const { fields, append, remove } = useFieldArray({
|
||||
const { fields, append, remove, insert } = useFieldArray({
|
||||
control: control,
|
||||
name,
|
||||
});
|
||||
|
@ -32,7 +58,73 @@ export const AttributeInput = ({ name }: AttributeInputProps) => {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const watchLast = watch(`${name}[${fields.length - 1}].key`, "");
|
||||
const [isOpenArray, setIsOpenArray] = useState<boolean[]>([false]);
|
||||
const watchLastKey = watch(`${name}[${fields.length - 1}].key`, "");
|
||||
const watchLastValue = watch(`${name}[${fields.length - 1}].value`, "");
|
||||
|
||||
const [valueOpen, setValueOpen] = useState(false);
|
||||
const toggleSelect = (rowIndex: number, open: boolean) => {
|
||||
const arr = [...isOpenArray];
|
||||
arr[rowIndex] = open;
|
||||
setIsOpenArray(arr);
|
||||
};
|
||||
|
||||
const renderValueInput = (rowIndex: number, attribute: any) => {
|
||||
const attributeValues = defaultContextAttributes.find(
|
||||
(attr) => attr.key === attribute.key
|
||||
)?.values;
|
||||
|
||||
return (
|
||||
<Td>
|
||||
{attributeValues?.length ? (
|
||||
<Controller
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
defaultValue={attribute.value}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
id={`${attribute.id}-value`}
|
||||
className="kc-attribute-value-selectable"
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
toggleId={`group-${name}`}
|
||||
onToggle={(open) => setValueOpen(open)}
|
||||
isOpen={valueOpen}
|
||||
variant={SelectVariant.typeahead}
|
||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||
placeholderText={t("clients:selectOrTypeAKey")}
|
||||
selections={value}
|
||||
onSelect={(_, selectedValue) => {
|
||||
remove(rowIndex);
|
||||
insert(rowIndex, {
|
||||
key: attribute.key,
|
||||
value: selectedValue,
|
||||
});
|
||||
onChange(selectedValue);
|
||||
|
||||
setValueOpen(false);
|
||||
}}
|
||||
>
|
||||
{attributeValues.map((attribute) => (
|
||||
<SelectOption key={attribute.key} value={attribute.key}>
|
||||
{t(`${attribute.name}`)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<TextInput
|
||||
id={`$clients:${attribute.key}-value`}
|
||||
className="value-input"
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
ref={register()}
|
||||
defaultValue={attribute.value}
|
||||
data-testid="attribute-value-input"
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<TableComposable
|
||||
|
@ -55,23 +147,56 @@ export const AttributeInput = ({ name }: AttributeInputProps) => {
|
|||
{fields.map((attribute, rowIndex) => (
|
||||
<Tr key={attribute.id} data-testid="attribute-row">
|
||||
<Td>
|
||||
<TextInput
|
||||
id={`${attribute.id}-key`}
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
ref={register()}
|
||||
defaultValue={attribute.key}
|
||||
data-testid="attribute-key-input"
|
||||
/>
|
||||
</Td>
|
||||
<Td>
|
||||
<TextInput
|
||||
id={`${attribute.id}-value`}
|
||||
name={`${name}[${rowIndex}].value`}
|
||||
ref={register()}
|
||||
defaultValue={attribute.value}
|
||||
data-testid="attribute-value-input"
|
||||
/>
|
||||
{isKeySelectable ? (
|
||||
<FormGroup fieldId="test">
|
||||
<Controller
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
defaultValue={attribute.key}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId="id"
|
||||
id={`${attribute.id}-key`}
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
className="kc-attribute-key-selectable"
|
||||
variant={SelectVariant.typeahead}
|
||||
typeAheadAriaLabel={t("clients:selectOrTypeAKey")}
|
||||
placeholderText={t("clients:selectOrTypeAKey")}
|
||||
onToggle={(open) => toggleSelect(rowIndex, open)}
|
||||
onSelect={(_, selectedValue) => {
|
||||
remove(rowIndex);
|
||||
insert(rowIndex, {
|
||||
key: selectedValue,
|
||||
value: attribute.value,
|
||||
});
|
||||
onChange(selectedValue);
|
||||
|
||||
toggleSelect(rowIndex, false);
|
||||
}}
|
||||
selections={value}
|
||||
aria-label="some label"
|
||||
isOpen={isOpenArray[rowIndex]}
|
||||
>
|
||||
{selectableValues?.map((attribute) => (
|
||||
<SelectOption key={attribute} value={attribute}>
|
||||
{t(`clients:${attribute}`)}
|
||||
</SelectOption>
|
||||
))}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : (
|
||||
<TextInput
|
||||
id={`${attribute.id}-key`}
|
||||
name={`${name}[${rowIndex}].key`}
|
||||
ref={register()}
|
||||
defaultValue={attribute.key}
|
||||
data-testid="attribute-key-input"
|
||||
/>
|
||||
)}
|
||||
</Td>
|
||||
{renderValueInput(rowIndex, attribute)}
|
||||
<Td key="minus-button" id={`kc-minus-button-${rowIndex}`}>
|
||||
<Button
|
||||
id={`minus-button-${rowIndex}`}
|
||||
|
@ -91,9 +216,14 @@ export const AttributeInput = ({ name }: AttributeInputProps) => {
|
|||
id="plus-icon"
|
||||
variant="link"
|
||||
className="kc-attributes__plus-icon"
|
||||
onClick={() => append({ key: "", value: "" })}
|
||||
onClick={() => {
|
||||
append({ key: "", value: "" });
|
||||
if (isKeySelectable) {
|
||||
setIsOpenArray([...isOpenArray, false]);
|
||||
}
|
||||
}}
|
||||
icon={<PlusCircleIcon />}
|
||||
isDisabled={!watchLast}
|
||||
isDisabled={isKeySelectable ? !watchLastValue : !watchLastKey}
|
||||
data-testid="attribute-add-row"
|
||||
>
|
||||
{t("roles:addAttributeText")}
|
||||
|
|
|
@ -39,6 +39,7 @@ import {
|
|||
ClientRoleRoute,
|
||||
toClientRole,
|
||||
} from "./routes/ClientRole";
|
||||
import { defaultContextAttributes } from "../clients/utils";
|
||||
|
||||
export default function RealmRoleTabs() {
|
||||
const { t } = useTranslation("roles");
|
||||
|
@ -377,6 +378,10 @@ export default function RealmRoleTabs() {
|
|||
title={<TabTitleText>{t("common:attributes")}</TabTitleText>}
|
||||
>
|
||||
<AttributesForm
|
||||
isKeySelectable
|
||||
selectableValues={defaultContextAttributes.map(
|
||||
(item) => item.key
|
||||
)}
|
||||
form={form}
|
||||
save={save}
|
||||
reset={() => reset(role)}
|
||||
|
|
|
@ -140,6 +140,8 @@ export const getBaseUrl = (adminClient: KeycloakAdminClient) => {
|
|||
);
|
||||
};
|
||||
|
||||
export const alphaRegexPattern = /[^A-Za-z]/g;
|
||||
|
||||
export const emailRegexPattern =
|
||||
/^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
|
||||
|
||||
|
|
Loading…
Reference in a new issue