preliminary work - list working
This commit is contained in:
parent
926c97002f
commit
95c22fd862
5 changed files with 1035 additions and 3 deletions
|
@ -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>
|
||||||
|
|
201
src/user-federation/ldap/mappers/LdapMapperDialog.tsx
Normal file
201
src/user-federation/ldap/mappers/LdapMapperDialog.tsx
Normal 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>
|
||||||
|
);
|
||||||
|
};
|
142
src/user-federation/ldap/mappers/LdapMapperList.tsx
Normal file
142
src/user-federation/ldap/mappers/LdapMapperList.tsx
Normal 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"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
*/
|
376
src/user-federation/ldap/mappers/LdapMappingDetails.tsx
Normal file
376
src/user-federation/ldap/mappers/LdapMappingDetails.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
314
src/user-federation/ldap/mappers/LdapRoleMappingForm.tsx
Normal file
314
src/user-federation/ldap/mappers/LdapRoleMappingForm.tsx
Normal 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>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
Loading…
Reference in a new issue