diff --git a/src/client-scopes/ClientScopesSection.tsx b/src/client-scopes/ClientScopesSection.tsx index a72509f1c7..f9bb79a6ff 100644 --- a/src/client-scopes/ClientScopesSection.tsx +++ b/src/client-scopes/ClientScopesSection.tsx @@ -56,11 +56,7 @@ export const ClientScopesSection = () => { inputGroupPlaceholder={t("searchFor")} inputGroupOnChange={filterData} toolbarItem={ - } diff --git a/src/client-scopes/details/MapperList.tsx b/src/client-scopes/details/MapperList.tsx index 0d5616dc0a..e668c1288a 100644 --- a/src/client-scopes/details/MapperList.tsx +++ b/src/client-scopes/details/MapperList.tsx @@ -2,6 +2,7 @@ import React, { useContext, useState } from "react"; import { useTranslation } from "react-i18next"; import { AlertVariant, + ButtonVariant, Dropdown, DropdownItem, DropdownToggle, @@ -24,13 +25,14 @@ import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState import { HttpClientContext } from "../../context/http-service/HttpClientContext"; import { RealmContext } from "../../context/realm-context/RealmContext"; import { useAlerts } from "../../components/alert/Alerts"; +import { Link } from "react-router-dom"; type MapperListProps = { clientScope: ClientScopeRepresentation; }; type Row = { - name: string; + name: JSX.Element; category: string; type: string; priority: number; @@ -58,6 +60,13 @@ export const MapperList = ({ clientScope }: MapperListProps) => { instructions={t("emptyMappersInstructions")} primaryActionText={t("emptyPrimaryAction")} onPrimaryAction={() => {}} + secondaryActions={[ + { + text: t("emptySecondaryAction"), + onClick: () => {}, + type: ButtonVariant.secondary, + }, + ]} /> ); } @@ -70,7 +79,13 @@ export const MapperList = ({ clientScope }: MapperListProps) => { return { mapper, cells: { - name: mapper.name, + name: ( + <> + + {mapper.name} + + + ), category: mapperType.category, type: mapperType.name, priority: mapperType.priority, @@ -82,7 +97,7 @@ export const MapperList = ({ clientScope }: MapperListProps) => { const filterData = (search: string) => { setFilteredData( data.filter((column) => - column.cells.name.toLowerCase().includes(search.toLowerCase()) + column.mapper.name!.toLowerCase().includes(search.toLowerCase()) ) ); }; diff --git a/src/client-scopes/details/MappingDetails.tsx b/src/client-scopes/details/MappingDetails.tsx new file mode 100644 index 0000000000..44aa4ea511 --- /dev/null +++ b/src/client-scopes/details/MappingDetails.tsx @@ -0,0 +1,293 @@ +import React, { useContext, useEffect, useState } from "react"; +import { useParams } from "react-router-dom"; +import { useTranslation } from "react-i18next"; +import { + ActionGroup, + AlertVariant, + Button, + ButtonVariant, + Checkbox, + DropdownItem, + Flex, + FlexItem, + Form, + FormGroup, + PageSection, + Select, + SelectOption, + SelectVariant, + Switch, + TextInput, +} from "@patternfly/react-core"; + +import { ViewHeader } from "../../components/view-header/ViewHeader"; +import { HttpClientContext } from "../../context/http-service/HttpClientContext"; +import { RealmContext } from "../../context/realm-context/RealmContext"; +import { ProtocolMapperRepresentation } from "../models/client-scope"; +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 { ConfigPropertyRepresentation } from "../../context/server-info/server-info"; + +export const MappingDetails = () => { + const { t } = useTranslation("client-scopes"); + const httpClient = useContext(HttpClientContext)!; + const { realm } = useContext(RealmContext); + const { addAlert } = useAlerts(); + + const { scopeId, id } = useParams<{ scopeId: string; id: string }>(); + const { register, setValue, control, handleSubmit } = useForm(); + const [mapping, setMapping] = useState(); + const [typeOpen, setTypeOpen] = useState(false); + const [configProperties, setConfigProperties] = useState< + ConfigPropertyRepresentation[] + >(); + + const serverInfo = useServerInfo(); + const url = `/admin/realms/${realm}/client-scopes/${scopeId}/protocol-mappers/models/${id}`; + + useEffect(() => { + (async () => { + const response = await httpClient.doGet( + url + ); + if (response.data) { + Object.entries(response.data).map((entry) => { + if (entry[0] === "config") { + Object.keys(entry[1]).map((key) => { + const newKey = key.replace(/\./g, "_"); + setValue("config." + newKey, entry[1][key]); + }); + } + setValue(entry[0], entry[1]); + }); + } + setMapping(response.data); + const mapperTypes = + serverInfo.protocolMapperTypes[response.data!.protocol!]; + const properties = mapperTypes.find( + (type) => type.id === mapping?.protocolMapper + )?.properties; + setConfigProperties(properties); + })(); + }, []); + + const [toggleDeleteDialog, DeleteConfirm] = useConfirmDialog({ + titleKey: "client-scopes:deleteMappingTitle", + messageKey: "client-scopes:deleteMappingConfirm", + continueButtonLabel: "common:delete", + continueButtonVariant: ButtonVariant.danger, + onConfirm: () => { + try { + httpClient.doDelete(url); + addAlert(t("mappingDeletedSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("mappingDeletedError", { error }), AlertVariant.danger); + } + }, + }); + + const save = async (formMapping: ProtocolMapperRepresentation) => { + const keyValues = Object.keys(formMapping.config!).map((key) => { + const newKey = key.replace(/_/g, "."); + return { [newKey]: formMapping.config![key] }; + }); + + const map = { ...mapping, config: Object.assign({}, ...keyValues) }; + try { + await httpClient.doPut(url, map); + addAlert(t("mappingUpdatedSuccess"), AlertVariant.success); + } catch (error) { + addAlert(t("mappingUpdatedError", { error }), AlertVariant.danger); + } + }; + + return ( + <> + + + {t("common:delete")} + , + ]} + /> + +
+ + } + fieldId="prefix" + > + + + + } + fieldId="multiValued" + > + ( + onChange("" + value)} + /> + )} + /> + + + } + fieldId="claimName" + > + + + + } + fieldId="claimJsonType" + > + ( + + )} + /> + + + + + ( + + )} + /> + + + ( + + )} + /> + + + ( + + )} + /> + + + + + + + +
+
+ + ); +}; diff --git a/src/client-scopes/form/ClientScopeForm.tsx b/src/client-scopes/form/ClientScopeForm.tsx index f64db88f13..d9fd0904d1 100644 --- a/src/client-scopes/form/ClientScopeForm.tsx +++ b/src/client-scopes/form/ClientScopeForm.tsx @@ -11,6 +11,9 @@ import { SelectOption, SelectVariant, Switch, + Tab, + Tabs, + TabTitleText, TextInput, } from "@patternfly/react-core"; import { useTranslation } from "react-i18next"; @@ -24,14 +27,16 @@ import { useAlerts } from "../../components/alert/Alerts"; import { useLoginProviders } from "../../context/server-info/ServerInfoProvider"; import { ViewHeader } from "../../components/view-header/ViewHeader"; import { convertFormValuesToObject, convertToFormValues } from "../../util"; +import { MapperList } from "../details/MapperList"; export const ClientScopeForm = () => { const { t } = useTranslation("client-scopes"); - const helpText = useTranslation("client-scopes-help").t; const { register, control, handleSubmit, errors, setValue } = useForm< ClientScopeRepresentation >(); const history = useHistory(); + const [clientScope, setClientScope] = useState(); + const [activeTab, setActiveTab] = useState(0); const httpClient = useContext(HttpClientContext)!; const { realm } = useContext(RealmContext); @@ -55,6 +60,8 @@ export const ClientScopeForm = () => { setValue(entry[0], entry[1]); }); } + + setClientScope(response.data); } })(); }, []); @@ -83,194 +90,213 @@ export const ClientScopeForm = () => { return ( <> -
- - } - fieldId="kc-name" - isRequired - validated={errors.name ? "error" : "default"} - helperTextInvalid={t("common:required")} + setActiveTab(key as number)} + isBox + > + {t("settings")}} > - - - - } - fieldId="kc-description" - > - - - - } - fieldId="kc-protocol" - > - ( - + } + fieldId="kc-protocol" + > + ( + + )} + /> + )} - /> - - - } - fieldId="kc-display.on.consent.screen" - > - ( - + } + fieldId="kc-display.on.consent.screen" + > + ( + onChange("" + value)} + /> + )} /> - )} - /> - - - } - fieldId="kc-consent-screen-text" - > - - - - } - fieldId="includeInTokenScope" - > - ( - onChange("" + value)} + + + } + fieldId="kc-consent-screen-text" + > + - )} - /> - - - } - fieldId="kc-gui-order" - > - - - - - - -
+ + + } + fieldId="includeInTokenScope" + > + ( + onChange("" + value)} + /> + )} + /> + + + } + fieldId="kc-gui-order" + > + + + + + + + + + {t("mappers")}}> + {clientScope && } + +
); diff --git a/src/client-scopes/help.json b/src/client-scopes/help.json index bbb6e4e2b7..a3f43769a6 100644 --- a/src/client-scopes/help.json +++ b/src/client-scopes/help.json @@ -6,6 +6,10 @@ "displayOnConsentScreen": "If on, and this client scope is added to some client with consent required, the text specified by 'Consent Screen Text' will be displayed on consent screen. If off, this client scope will not be displayed on the consent screen", "consentScreenText": "Text that will be shown on the consent screen when this client scope is added to some client with consent required. Defaults to name of client scope if it is not filled", "includeInTokenScope": "If on, the name of this client scope will be added to the access token property 'scope' as well as to the Token Introspection Endpoint response. If off, this client scope will be omitted from the token and from the Token Introspection Endpoint response.", - "guiOrder": "Specify order of the provider in GUI (such as in Consent page) as integer" + "guiOrder": "Specify order of the provider in GUI (such as in Consent page) as integer", + "prefix": "A prefix for each Realm Role (optional).", + "multiValued": "Indicates if attribute supports multiple values. If true, the list of all values of this attribute will be set as claim. If false, just first value will be set as claim", + "tokenClaimName": "Name of the claim to insert into the token. This can be a fully qualified name like 'address.street'. In this case, a nested json object will be created. To prevent nesting and use dot literally, escape the dot with backslash (\\.).", + "claimJsonType": "JSON type that should be used to populate the json claim in the token. long, int, boolean, String and JSON are valid values." } } diff --git a/src/client-scopes/messages.json b/src/client-scopes/messages.json index 58a6d0ea14..0ca9cd183d 100644 --- a/src/client-scopes/messages.json +++ b/src/client-scopes/messages.json @@ -12,6 +12,31 @@ "priority": "Priority", "protocol": "Protocol", "includeInTokenScope": "Include in token scope", + "settings": "Settings", + "mappers": "Mappers", + "mappersSearchFor": "Search for mapper", + "addMapper": "Add mapper", + "fromPredefinedMapper": "From predefined mappers", + "byConfiguration": "By configuration", + "emptyMappers": "No mappers", + "emptyMappersInstructions": "If you want to add mappers, please click the button below to add some predefined mappers or to configure a new mapper.", + "emptyPrimaryAction": "Add predefined mapper", + "emptySecondaryAction": "Configure a new mapper", + "mappingDetails": "Mapper details", + "deleteMappingTitle": "Delete mapping?", + "deleteMappingConfirm": "Are you sure you want to delete this mapping?", + "mappingDeletedSuccess": "Mapping successfully deleted", + "mappingDeletedError": "Could not delete mapping: '{{error}}'", + "mappingUpdatedSuccess": "Mapping successfully updated", + "mappingUpdatedError": "Could not update mapping: '{{error}}'", + "realmRolePrefix": "Realm role prefix", + "multiValued": "Multivalued", + "tokenClaimName": "Token claim name", + "claimJsonType": "Claim JSON type", + "addClaimTo": "Add claim to", + "idToken": "ID token", + "accessToken": "Access token", + "userInfo": "User info", "createSuccess": "Client scope created", "createError": "Could not create client scope: '{{error}}'", "updateSuccess": "Client scope updated", diff --git a/src/components/help-enabler/HelpItem.tsx b/src/components/help-enabler/HelpItem.tsx index 07219829b1..7bfc43f5e0 100644 --- a/src/components/help-enabler/HelpItem.tsx +++ b/src/components/help-enabler/HelpItem.tsx @@ -16,7 +16,7 @@ export const HelpItem = ({ helpText, forLabel, forID }: HelpItemProps) => { return ( <> {enabled && ( - +