preliminary work - list working

This commit is contained in:
mfrances 2021-03-22 17:04:40 -04:00
parent 926c97002f
commit 95c22fd862
5 changed files with 1035 additions and 3 deletions

View file

@ -10,7 +10,6 @@ import {
PageSection, PageSection,
Tab, Tab,
TabTitleText, TabTitleText,
Text,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { LdapSettingsAdvanced } from "./ldap/LdapSettingsAdvanced"; import { LdapSettingsAdvanced } from "./ldap/LdapSettingsAdvanced";
@ -35,6 +34,7 @@ import { useHistory, useParams } from "react-router-dom";
import { ScrollForm } from "../components/scroll-form/ScrollForm"; import { ScrollForm } from "../components/scroll-form/ScrollForm";
import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs"; import { KeycloakTabs } from "../components/keycloak-tabs/KeycloakTabs";
import {LdapMapperList} from "./ldap/mappers/LdapMapperList";
type LdapSettingsHeaderProps = { type LdapSettingsHeaderProps = {
onChange: (value: string) => void; onChange: (value: string) => void;
@ -331,8 +331,7 @@ export const UserFederationLdapSettings = () => {
eventKey="mappers" eventKey="mappers"
title={<TabTitleText>{t("common:mappers")}</TabTitleText>} title={<TabTitleText>{t("common:mappers")}</TabTitleText>}
> >
{/* <MapperList clientScope={clientScope} refresh={refresh} /> */} <LdapMapperList />
<Text>Coming soon!</Text>
</Tab> </Tab>
</KeycloakTabs> </KeycloakTabs>
</PageSection> </PageSection>

View file

@ -0,0 +1,201 @@
import React, { useState } from "react";
import {
Button,
ButtonVariant,
DataList,
DataListCell,
DataListItem,
DataListItemCells,
DataListItemRow,
Modal,
ModalVariant,
Text,
TextContent,
} from "@patternfly/react-core";
import {
Table,
TableBody,
TableHeader,
TableVariant,
} from "@patternfly/react-table";
import { useTranslation } from "react-i18next";
import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation";
import { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
import { useServerInfo } from "../../../context/server-info/ServerInfoProvider";
import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState";
// export type AddLdapMapperDialogModalProps = {
// protocol: string;
// filter?: ProtocolMapperRepresentation[];
// onConfirm: (
// value: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
// ) => void;
// };
export type AddLdapMapperDialogProps = {
open: boolean;
toggleDialog: () => void;
};
export const AddLdapMapperDialog = (props: AddLdapMapperDialogProps) => {
const { t } = useTranslation("client-scopes");
const serverInfo = useServerInfo();
// const protocol = props.protocol;
// const protocolMappers = serverInfo.protocolMapperTypes![protocol];
// const builtInMappers = serverInfo.builtinProtocolMappers![protocol];
// const [filter, setFilter] = useState<ProtocolMapperRepresentation[]>([]);
// const allRows = builtInMappers.map((mapper) => {
// const mapperType = protocolMappers.filter(
// (type) => type.id === mapper.protocolMapper
// )[0];
// return {
// item: mapper,
// selected: false,
// cells: [mapper.name, mapperType.helpText],
// };
// });
const [rows, setRows] = useState(allRows);
// if (props.filter && props.filter.length !== filter.length) {
// setFilter(props.filter);
// const nameFilter = props.filter.map((f) => f.name);
// setRows([...allRows.filter((row) => !nameFilter.includes(row.item.name))]);
// }
// const selectedRows = rows
// .filter((row) => row.selected)
// .map((row) => row.item);
//const isBuiltIn = !!props.filter;
const isBuiltIn = true;
const mapperList = [
{
"id":"699b72e5-b936-41b9-98fc-5d5b3ec5ea6f",
"name":"username",
"providerId":"user-attribute-ldap-mapper",
"providerType":"org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
"parentId":"f1da61f9-08f7-4dc5-83dd-d774fba518d4",
"config":
{
"ldap.attribute":["cn"],
"is.mandatory.in.ldap":["true"],
"always.read.value.from.ldap":["false"],
"read.only":["true"],
"user.model.attribute":["username"]
}
},
{
"id":"c11788d2-62be-442f-813f-4b00382a1e10",
"name":"last name",
"providerId":"user-attribute-ldap-mapper",
"providerType":"org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
"parentId":"f1da61f9-08f7-4dc5-83dd-d774fba518d4",
"config":
{
"ldap.attribute":["sn"],
"is.mandatory.in.ldap":["true"],
"always.read.value.from.ldap":["true"],
"read.only":["true"],
"user.model.attribute":["lastName"]
}
}
]
return (
<Modal
variant={ModalVariant.medium}
title={t("chooseAMapperType")}
isOpen={props.open}
onClose={props.toggleDialog}
actions={
isBuiltIn
? [
<Button
id="modal-confirm"
key="confirm"
// isDisabled={rows.length === 0 || selectedRows.length === 0}
onClick={() => {
// props.onConfirm(selectedRows);
props.toggleDialog();
}}
>
{t("common:add")}
</Button>,
<Button
id="modal-cancel"
key="cancel"
variant={ButtonVariant.secondary}
onClick={() => {
props.toggleDialog();
}}
>
{t("common:cancel")}
</Button>,
]
: []
}
>
<TextContent>
<Text>{t("predefinedMappingDescription")}</Text>
</TextContent>
{!isBuiltIn && (
<DataList
onSelectDataListItem={(id) => {
const mapper = mapperList.find((mapper) => mapper.id === id);
// props.onConfirm(mapper!);
props.toggleDialog();
}}
aria-label={t("chooseAMapperType")}
isCompact
>
{(
<DataListItem
aria-labelledby={mapperList[0].name}
key={mapperList[0].id}
id={mapperList[0].id}
>
<DataListItemRow>
<DataListItemCells
dataListCells={[
<DataListCell key={`name-${mapperList[0].id}`}>
<>{mapperList[0].name}</>
</DataListCell>,
<DataListCell key={`helpText-${mapperList[0].id}`}>
<>{mapperList[0].helpText}</>
</DataListCell>,
]}
/>
</DataListItemRow>
</DataListItem>
))}
</DataList>
)}
{isBuiltIn && rows.length > 0 && (
<Table
variant={TableVariant.compact}
cells={[t("common:name"), t("common:description")]}
onSelect={(_, isSelected, rowIndex) => {
rows[rowIndex].selected = isSelected;
setRows([...rows]);
}}
canSelectAll={false}
rows={rows}
aria-label={t("chooseAMapperType")}
>
<TableHeader />
<TableBody />
</Table>
)}
{isBuiltIn && rows.length === 0 && (
<ListEmptyState
message={t("emptyMappers")}
instructions={t("emptyBuiltInMappersInstructions")}
/>
)}
</Modal>
);
};

View file

@ -0,0 +1,142 @@
import React, { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import { AlertVariant } from "@patternfly/react-core";
import {
Table,
TableBody,
TableHeader,
TableVariant,
} from "@patternfly/react-table";
import { useErrorHandler } from "react-error-boundary";
import { TableToolbar } from "../../../components/table-toolbar/TableToolbar";
import { ListEmptyState } from "../../../components/list-empty-state/ListEmptyState";
import { useAlerts } from "../../../components/alert/Alerts";
import {
useAdminClient,
asyncStateFetch,
} from "../../../context/auth/AdminClient";
import { useParams } from "react-router-dom";
interface ComponentMapperRepresentation {
config?: Record<string, any>;
id?: string;
name?: string;
providerId?: string;
providerType?: string;
parentID?: string;
}
type Row = {
name: JSX.Element;
type: string;
};
export const LdapMapperList = () => {
const [mappers, setMappers] = useState<ComponentMapperRepresentation[]>();
const { t } = useTranslation("client-scopes");
const adminClient = useAdminClient();
const { addAlert } = useAlerts();
const handleError = useErrorHandler();
const [key, setKey] = useState(0);
const { id } = useParams<{ id: string }>();
useEffect(() => {
return asyncStateFetch(
() => {
const testParams: { [name: string]: string | number } = {
parent: id,
type: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
};
return adminClient.components.find(testParams);
},
(mappers) => {
setMappers(mappers);
console.log(mappers);
},
handleError
);
}, [key]);
if (!mappers) {
return (
<>
<ListEmptyState
message={t("emptyMappers")}
instructions={t("emptyMappersInstructions")}
primaryActionText={t("emptyPrimaryAction")}
/>
</>
);
}
return (
<TableToolbar
inputGroupName="clientsScopeToolbarTextInput"
inputGroupPlaceholder={t("mappersSearchFor")}
>
<Table
variant={TableVariant.compact}
cells={[
t("common:name"),
t("common:type"),
]}
rows={mappers.map((cell) => {
return {
cells: Object.values([cell.name, cell.providerId]),
};
})}
aria-label={t("clientScopeList")}
actions={[
{
title: t("common:delete"),
onClick: () => {
addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
},
},
]}
>
<TableHeader />
<TableBody />
</Table>
</TableToolbar>
);
};
/*
Sample responses:
const mapperList = [
{
id: "699b72e5-b936-41b9-98fc-5d5b3ec5ea6f",
name: "username",
providerId: "user-attribute-ldap-mapper",
providerType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
parentId: "f1da61f9-08f7-4dc5-83dd-d774fba518d4",
config: {
"ldap.attribute": ["cn"],
"is.mandatory.in.ldap": ["true"],
"always.read.value.from.ldap": ["false"],
"read.only": ["true"],
"user.model.attribute": ["username"],
},
},
{
id: "c11788d2-62be-442f-813f-4b00382a1e10",
name: "last name",
providerId: "user-attribute-ldap-mapper",
providerType: "org.keycloak.storage.ldap.mappers.LDAPStorageMapper",
parentId: "f1da61f9-08f7-4dc5-83dd-d774fba518d4",
config: {
"ldap.attribute": ["sn"],
"is.mandatory.in.ldap": ["true"],
"always.read.value.from.ldap": ["true"],
"read.only": ["true"],
"user.model.attribute": ["lastName"],
},
},
];
*/

View file

@ -0,0 +1,376 @@
import React, { useEffect, useState } from "react";
import { useHistory, useParams, useRouteMatch } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import {
ActionGroup,
AlertVariant,
Button,
ButtonVariant,
Checkbox,
DropdownItem,
Flex,
FlexItem,
FormGroup,
PageSection,
Select,
SelectOption,
SelectVariant,
Switch,
TextInput,
ValidatedOptions,
} from "@patternfly/react-core";
import { ConfigPropertyRepresentation } from "keycloak-admin/lib/defs/configPropertyRepresentation";
import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation";
import { ViewHeader } from "../../../components/view-header/ViewHeader";
import {
useAdminClient,
asyncStateFetch,
} from "../../../context/auth/AdminClient";
import { Controller, useForm } from "react-hook-form";
import { useConfirmDialog } from "../../../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../../../components/alert/Alerts";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
import { useServerInfo } from "../../../context/server-info/ServerInfoProvider";
import { convertFormValuesToObject, convertToFormValues } from "../../../util";
import { FormAccess } from "../../../components/form-access/FormAccess";
type Params = {
id: string;
mapperId: string;
};
export const LdapMappingDetails = () => {
const { t } = useTranslation("client-scopes");
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts();
const { id, mapperId } = useParams<Params>();
const { register, errors, setValue, control, handleSubmit } = useForm();
const [mapping, setMapping] = useState<ProtocolMapperRepresentation>();
const [typeOpen, setTypeOpen] = useState(false);
const [configProperties, setConfigProperties] = useState<
ConfigPropertyRepresentation[]
>();
const history = useHistory();
const serverInfo = useServerInfo();
const { url } = useRouteMatch();
const isGuid = /^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$/;
useEffect(() => {
return asyncStateFetch(
async () => {
if (mapperId.match(isGuid)) {
const data = await adminClient.clientScopes.findProtocolMapper({
id,
mapperId,
});
if (data) {
Object.entries(data).map((entry) => {
convertToFormValues(entry[1], "config", setValue);
});
}
const mapperTypes = serverInfo.protocolMapperTypes![data!.protocol!];
const properties = mapperTypes.find(
(type) => type.id === data!.protocolMapper
)?.properties!;
return {
configProperties: properties,
mapping: data,
};
} else {
const scope = await adminClient.clientScopes.findOne({ id });
const protocolMappers = serverInfo.protocolMapperTypes![
scope.protocol!
];
const mapping = protocolMappers.find(
(mapper) => mapper.id === mapperId
)!;
return {
mapping: {
name: mapping.name,
protocol: scope.protocol,
protocolMapper: mapperId,
},
};
}
},
(result) => {
setConfigProperties(result.configProperties);
setMapping(result.mapping);
},
handleError
);
}, []);
const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({
titleKey: "client-scopes:deleteMappingTitle",
messageKey: "client-scopes:deleteMappingConfirm",
continueButtonLabel: "common:delete",
continueButtonVariant: ButtonVariant.danger,
onConfirm: async () => {
try {
await adminClient.clientScopes.delClientScopeMappings(
{ client: id, id: mapperId },
[]
);
addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
history.push(`${url}/${id}`);
} catch (error) {
addAlert(t("mappingDeletedError", { error }), AlertVariant.danger);
}
},
});
const save = async (formMapping: ProtocolMapperRepresentation) => {
const config = convertFormValuesToObject(formMapping.config);
const map = { ...mapping, ...formMapping, config };
const key = mapperId.match(isGuid) ? "Updated" : "Created";
try {
if (mapperId.match(isGuid)) {
await adminClient.clientScopes.updateProtocolMapper(
{ id, mapperId },
map
);
} else {
await adminClient.clientScopes.addProtocolMapper({ id }, map);
}
addAlert(t(`mapping${key}Success`), AlertVariant.success);
} catch (error) {
addAlert(t(`mapping${key}Error`, { error }), AlertVariant.danger);
}
};
return (
<>
<DeleteConfirm />
<ViewHeader
titleKey={mapping ? mapping.name! : t("addMapper")}
subKey={mapperId.match(isGuid) ? mapperId : ""}
badge={mapping?.protocol}
dropdownItems={
mapperId.match(isGuid)
? [
<DropdownItem
key="delete"
value="delete"
onClick={toggleDeleteDialog}
>
{t("common:delete")}
</DropdownItem>,
]
: undefined
}
/>
<PageSection variant="light">
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-clients"
>
<>
{!mapperId.match(isGuid) && (
<FormGroup
label={t("common:name")}
labelIcon={
<HelpItem
helpText="client-scopes-help:mapperName"
forLabel={t("common:name")}
forID="name"
/>
}
fieldId="name"
isRequired
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register({ required: true })}
type="text"
id="name"
name="name"
validated={
errors.name
? ValidatedOptions.error
: ValidatedOptions.default
}
/>
</FormGroup>
)}
</>
<FormGroup
label={t("realmRolePrefix")}
labelIcon={
<HelpItem
helpText="client-scopes-help:prefix"
forLabel={t("realmRolePrefix")}
forID="prefix"
/>
}
fieldId="prefix"
>
<TextInput
ref={register()}
type="text"
id="prefix"
name="config.usermodel-realmRoleMapping-rolePrefix"
/>
</FormGroup>
<FormGroup
label={t("multiValued")}
labelIcon={
<HelpItem
helpText="client-scopes-help:multiValued"
forLabel={t("multiValued")}
forID="multiValued"
/>
}
fieldId="multiValued"
>
<Controller
name="config.multivalued"
control={control}
defaultValue="false"
render={({ onChange, value }) => (
<Switch
id="multiValued"
label={t("common:on")}
labelOff={t("common:off")}
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
/>
)}
/>
</FormGroup>
<FormGroup
label={t("tokenClaimName")}
labelIcon={
<HelpItem
helpText="client-scopes-help:tokenClaimName"
forLabel={t("tokenClaimName")}
forID="claimName"
/>
}
fieldId="claimName"
>
<TextInput
ref={register()}
type="text"
id="claimName"
name="config.claim-name"
/>
</FormGroup>
<FormGroup
label={t("claimJsonType")}
labelIcon={
<HelpItem
helpText="client-scopes-help:claimJsonType"
forLabel={t("claimJsonType")}
forID="claimJsonType"
/>
}
fieldId="claimJsonType"
>
<Controller
name="config.jsonType-label"
defaultValue=""
control={control}
render={({ onChange, value }) => (
<Select
toggleId="claimJsonType"
onToggle={() => setTypeOpen(!typeOpen)}
onSelect={(_, value) => {
onChange(value as string);
setTypeOpen(false);
}}
selections={value}
variant={SelectVariant.single}
aria-label={t("claimJsonType")}
isOpen={typeOpen}
>
{configProperties &&
configProperties
.find((property) => property.name! === "jsonType.label")
?.options!.map((option) => (
<SelectOption
selected={option === value}
key={option}
value={option}
/>
))}
</Select>
)}
/>
</FormGroup>
<FormGroup
hasNoPaddingTop
label={t("addClaimTo")}
fieldId="addClaimTo"
>
<Flex>
<FlexItem>
<Controller
name="config.id-token-claim"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("idToken")}
id="idToken"
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
/>
)}
/>
</FlexItem>
<FlexItem>
<Controller
name="config.access-token-claim"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("accessToken")}
id="accessToken"
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
/>
)}
/>
</FlexItem>
<FlexItem>
<Controller
name="config.userinfo-token-claim"
defaultValue="false"
control={control}
render={({ onChange, value }) => (
<Checkbox
label={t("userInfo")}
id="userInfo"
isChecked={value === "true"}
onChange={(value) => onChange("" + value)}
/>
)}
/>
</FlexItem>
</Flex>
</FormGroup>
<ActionGroup>
<Button variant="primary" type="submit">
{t("common:save")}
</Button>
<Button variant="link">{t("common:cancel")}</Button>
</ActionGroup>
</FormAccess>
</PageSection>
</>
);
};

View file

@ -0,0 +1,314 @@
import React, { useContext, useEffect, useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Controller, useForm } from "react-hook-form";
import { useErrorHandler } from "react-error-boundary";
import {
FormGroup,
PageSection,
Select,
SelectVariant,
TextInput,
SelectOption,
ActionGroup,
Button,
SelectGroup,
Split,
SplitItem,
Divider,
ValidatedOptions,
} from "@patternfly/react-core";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapperRepresentation";
import { useAlerts } from "../../../components/alert/Alerts";
import { RealmContext } from "../../../context/realm-context/RealmContext";
import {
useAdminClient,
asyncStateFetch,
} from "../../../context/auth/AdminClient";
import { ViewHeader } from "../../../components/view-header/ViewHeader";
import { HelpItem } from "../../../components/help-enabler/HelpItem";
import { FormAccess } from "../../../components/form-access/FormAccess";
export const RoleMappingForm = () => {
const { realm } = useContext(RealmContext);
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const history = useHistory();
const { addAlert } = useAlerts();
const { t } = useTranslation("client-scopes");
const { register, handleSubmit, control, errors } = useForm();
const { id } = useParams<{ id: string }>();
const [roleOpen, setRoleOpen] = useState(false);
const [clientsOpen, setClientsOpen] = useState(false);
const [clients, setClients] = useState<ClientRepresentation[]>([]);
const [selectedClient, setSelectedClient] = useState<ClientRepresentation>();
const [clientRoles, setClientRoles] = useState<RoleRepresentation[]>([]);
useEffect(() => {
return asyncStateFetch(
async () => {
const clients = await adminClient.clients.find();
const asyncFilter = async (
predicate: (client: ClientRepresentation) => Promise<boolean>
) => {
const results = await Promise.all(clients.map(predicate));
return clients.filter((_, index) => results[index]);
};
const filteredClients = await asyncFilter(
async (client) =>
(await adminClient.clients.listRoles({ id: client.id! })).length > 0
);
filteredClients.map(
(client) =>
(client.toString = function () {
return this.clientId!;
})
);
return filteredClients;
},
(filteredClients) => setClients(filteredClients),
handleError
);
}, []);
useEffect(() => {
return asyncStateFetch(
async () => {
const client = selectedClient as ClientRepresentation;
if (client && client.name !== "realmRoles") {
const clientRoles = await adminClient.clients.listRoles({
id: client.id!,
});
return clientRoles;
} else {
return await adminClient.roles.find();
}
},
(clientRoles) => setClientRoles(clientRoles),
handleError
);
}, [selectedClient]);
const save = async (mapping: ProtocolMapperRepresentation) => {
try {
await adminClient.clientScopes.addProtocolMapper({ id }, mapping);
addAlert(t("mapperCreateSuccess"));
} catch (error) {
addAlert(t("mapperCreateError", error));
}
};
const createSelectGroup = (clients: ClientRepresentation[]) => {
return [
<SelectGroup key="role" label={t("roleGroup")}>
<SelectOption
key="realmRoles"
value={
{
name: "realmRoles",
toString: () => t("realmRoles"),
} as ClientRepresentation
}
>
{realm}
</SelectOption>
</SelectGroup>,
<Divider key="divider" />,
<SelectGroup key="group" label={t("clientGroup")}>
{clients.map((client) => (
<SelectOption key={client.id} value={client}>
{client.clientId}
</SelectOption>
))}
</SelectGroup>,
];
};
const roleSelectOptions = () => {
const createItem = (role: RoleRepresentation) => (
<SelectOption key={role.id} value={role}>
{role.name}
</SelectOption>
);
return clientRoles.map((role) => createItem(role));
};
return (
<>
<ViewHeader
titleKey="client-scopes:addMapper"
subKey="client-scopes:addMapperExplain"
/>
<PageSection variant="light">
<FormAccess
isHorizontal
onSubmit={handleSubmit(save)}
role="manage-clients"
>
<FormGroup
label={t("protocolMapper")}
labelIcon={
<HelpItem
helpText="client-scopes-help:protocolMapper"
forLabel={t("protocolMapper")}
forID="protocolMapper"
/>
}
fieldId="protocolMapper"
>
<TextInput
ref={register()}
type="text"
id="protocolMapper"
name="protocolMapper"
isReadOnly
/>
</FormGroup>
<FormGroup
label={t("common:name")}
labelIcon={
<HelpItem
helpText="client-scopes-help:mapperName"
forLabel={t("common:name")}
forID="name"
/>
}
fieldId="name"
isRequired
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register({ required: true })}
type="text"
id="name"
name="name"
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
/>
</FormGroup>
<FormGroup
label={t("common:role")}
labelIcon={
<HelpItem
helpText="client-scopes-help:role"
forLabel={t("common:role")}
forID="role"
/>
}
validated={errors["config.role"] ? "error" : "default"}
helperTextInvalid={t("common:required")}
fieldId="role"
>
<Split hasGutter>
<SplitItem>
<Select
toggleId="role"
onToggle={() => setClientsOpen(!clientsOpen)}
isOpen={clientsOpen}
variant={SelectVariant.typeahead}
typeAheadAriaLabel={t("selectASourceOfRoles")}
placeholderText={t("selectASourceOfRoles")}
isGrouped
onFilter={(evt) => {
const textInput = evt?.target.value || "";
if (textInput === "") {
return createSelectGroup(clients);
} else {
return createSelectGroup(
clients.filter((client) =>
client
.name!.toLowerCase()
.includes(textInput.toLowerCase())
)
);
}
}}
selections={selectedClient}
onClear={() => setSelectedClient(undefined)}
onSelect={(_, value) => {
if (value) {
setSelectedClient(value as ClientRepresentation);
setClientsOpen(false);
}
}}
>
{createSelectGroup(clients)}
</Select>
</SplitItem>
<SplitItem>
<Controller
name="config.role"
defaultValue=""
control={control}
rules={{ required: true }}
render={({ onChange, value }) => (
<Select
onToggle={() => setRoleOpen(!roleOpen)}
isOpen={roleOpen}
variant={SelectVariant.typeahead}
placeholderText={
selectedClient && selectedClient.name !== "realmRoles"
? t("clientRoles")
: t("selectARole")
}
isDisabled={!selectedClient}
typeAheadAriaLabel={t("selectARole")}
selections={value.name}
onSelect={(_, value) => {
onChange(value);
setRoleOpen(false);
}}
onClear={() => onChange("")}
>
{roleSelectOptions()}
</Select>
)}
/>
</SplitItem>
</Split>
</FormGroup>
<FormGroup
label={t("newRoleName")}
labelIcon={
<HelpItem
helpText="client-scopes-help:newRoleName"
forLabel={t("newRoleName")}
forID="newRoleName"
/>
}
fieldId="newRoleName"
>
<TextInput
ref={register()}
type="text"
id="newRoleName"
name="config.new-role-name"
/>
</FormGroup>
<ActionGroup>
<Button variant="primary" type="submit">
{t("common:save")}
</Button>
<Button variant="link" onClick={() => history.push("..")}>
{t("common:cancel")}
</Button>
</ActionGroup>
</FormAccess>
</PageSection>
</>
);
};