Identity Providers(Mappers): Edit mappers (#1140)

* rebase

dont fetch rolesbyID if mapperId

save attributes

fix cypress test

cypress test updates

fix cancel route

route fix with Erik

Apply suggestions from Jon's code review

Co-authored-by: Jon Koops <jonkoops@gmail.com>

pr feedback from jon

remove unused import

* usefindbytestid

* PR feedback from Jon

* fix tests

* fix save bug and feedback from Jon

* remove unnecessary type

* fix cypress test
This commit is contained in:
Jenny 2021-09-22 16:27:30 -04:00 committed by GitHub
parent 809247a686
commit 83d5624bf4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 395 additions and 171 deletions

View file

@ -19,6 +19,8 @@ describe("Identity provider test", () => {
const createSuccessMsg = "Identity provider successfully created";
const createMapperSuccessMsg = "Mapper created successfully.";
const saveMapperSuccessMsg = "Mapper saved successfully.";
const changeSuccessMsg =
"Successfully changed display order of identity providers";
const deletePrompt = "Delete provider?";
@ -53,17 +55,10 @@ describe("Identity provider test", () => {
listingPage.itemExist(identityProviderName);
});
it("should delete provider", () => {
const modalUtils = new ModalUtils();
listingPage.deleteItem(identityProviderName);
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg);
});
it("should create facebook provider", () => {
createProviderPage
.clickCard("facebook")
.clickCreateDropdown()
.clickItem("facebook")
.fill("facebook", "123")
.clickAdd();
masthead.checkNotificationMessage(createSuccessMsg);
@ -71,18 +66,11 @@ describe("Identity provider test", () => {
it("should change order of providers", () => {
const orderDialog = new OrderDialog();
const providers = ["facebook", identityProviderName, "bitbucket"];
const providers = [identityProviderName, "facebook", "bitbucket"];
sidebarPage.goToIdentityProviders();
listingPage.itemExist("facebook");
createProviderPage
.clickCreateDropdown()
.clickItem(identityProviderName)
.fill(identityProviderName, "123")
.clickAdd();
cy.wait(2000);
sidebarPage.goToIdentityProviders();
listingPage.itemExist(identityProviderName);
@ -93,13 +81,14 @@ describe("Identity provider test", () => {
.clickAdd();
cy.wait(2000);
sidebarPage.goToIdentityProviders();
listingPage.itemExist(identityProviderName);
orderDialog.openDialog().checkOrder(providers);
orderDialog.moveRowTo("facebook", identityProviderName);
orderDialog.checkOrder(["facebook", "bitbucket", identityProviderName]);
orderDialog.checkOrder(["bitbucket", identityProviderName, "facebook"]);
orderDialog.clickSave();
masthead.checkNotificationMessage(changeSuccessMsg);
@ -129,6 +118,14 @@ describe("Identity provider test", () => {
masthead.checkNotificationMessage(createSuccessMsg);
});
it("should delete provider", () => {
const modalUtils = new ModalUtils();
listingPage.deleteItem(identityProviderName);
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg);
});
it("should add facebook social mapper", () => {
sidebarPage.goToIdentityProviders();
@ -159,6 +156,32 @@ describe("Identity provider test", () => {
masthead.checkNotificationMessage(createMapperSuccessMsg);
});
it("should edit facebook mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("facebook");
addMapperPage.goToMappersTab();
listingPage.goToItemDetails("facebook mapper");
addMapperPage.editSocialMapper();
});
it("should edit SAML mapper", () => {
sidebarPage.goToIdentityProviders();
listingPage.goToItemDetails("saml");
addMapperPage.goToMappersTab();
listingPage.goToItemDetails("SAML mapper");
addMapperPage.editSAMLorOIDCMapper();
masthead.checkNotificationMessage(saveMapperSuccessMsg);
});
it("clean up providers", () => {
const modalUtils = new ModalUtils();
@ -172,11 +195,6 @@ describe("Identity provider test", () => {
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg);
sidebarPage.goToIdentityProviders();
listingPage.itemExist("github").deleteItem("github");
modalUtils.checkModalTitle(deletePrompt).confirmModal();
masthead.checkNotificationMessage(deleteSuccessMsg);
sidebarPage.goToIdentityProviders();
listingPage.itemExist("oidc").deleteItem("oidc");
modalUtils.checkModalTitle(deletePrompt).confirmModal();

View file

@ -113,4 +113,42 @@ export default class AddMapperPage {
return this;
}
editSocialMapper() {
cy.get(this.syncmodeSelectToggle).click();
cy.findByTestId("inherit").click();
cy.findByTestId(this.userSessionAttribute).clear();
cy.findByTestId(this.userSessionAttribute).type(
"user session attribute_edited"
);
cy.findByTestId(this.userSessionAttributeValue).clear();
cy.findByTestId(this.userSessionAttributeValue).type(
"user session attribute value_edited"
);
this.saveNewMapper();
return this;
}
editSAMLorOIDCMapper() {
cy.get(this.syncmodeSelectToggle).click();
cy.findByTestId("legacy").click();
cy.get(this.attributesKeyInput).clear();
cy.get(this.attributesKeyInput).type("key_edited");
cy.get(this.attributesValueInput).clear();
cy.get(this.attributesValueInput).type("value_edited");
this.toggleSwitch(this.regexAttributeValuesSwitch);
this.saveNewMapper();
return this;
}
}

View file

@ -68,6 +68,8 @@ export const AttributesForm = ({
const columns = ["Key", "Value"];
const noSaveCancelButtons = !save && !reset;
const watchLast = inConfig
? watch(`config.attributes[${fields.length - 1}].key`, "")
: watch(`attributes[${fields.length - 1}].key`, "");
@ -78,8 +80,6 @@ export const AttributesForm = ({
}
}, [fields]);
const noSaveCancelButtons = !save && !reset;
return (
<FormAccess
role="manage-realm"

View file

@ -1,5 +1,5 @@
import React, { Fragment, useState } from "react";
import { Link, useHistory, useRouteMatch } from "react-router-dom";
import { Link, useHistory } from "react-router-dom";
import { useTranslation } from "react-i18next";
import _ from "lodash";
import {
@ -34,7 +34,8 @@ import { upperCaseFormatter } from "../util";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { ProviderIconMapper } from "./ProviderIconMapper";
import { ManageOderDialog } from "./ManageOrderDialog";
import { toIdentityProviderTab } from "./routes/IdentityProviderTab";
import { toIdentityProvider } from "./routes/IdentityProvider";
import { toIdentityProviderCreate } from "./routes/IdentityProviderCreate";
export const IdentityProvidersSection = () => {
const { t } = useTranslation("identity-providers");
@ -43,7 +44,6 @@ export const IdentityProvidersSection = () => {
"groupName"
);
const { realm } = useRealm();
const { url } = useRouteMatch();
const history = useHistory();
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
@ -73,9 +73,9 @@ export const IdentityProvidersSection = () => {
const DetailLink = (identityProvider: IdentityProviderRepresentation) => (
<Link
key={identityProvider.providerId}
to={toIdentityProviderTab({
to={toIdentityProvider({
realm,
providerId: identityProvider.providerId,
providerId: identityProvider.providerId!,
alias: identityProvider.alias!,
tab: "settings",
})}
@ -96,7 +96,12 @@ export const IdentityProvidersSection = () => {
);
const navigateToCreate = (providerId: string) =>
history.push(`${url}/${providerId}`);
history.push(
toIdentityProviderCreate({
realm,
providerId,
})
);
const identityProviderOptions = () =>
Object.keys(identityProviders).map((group) => (
@ -105,11 +110,18 @@ export const IdentityProvidersSection = () => {
<DropdownItem
key={provider.id}
value={provider.id}
data-testid={provider.id}
onClick={() => navigateToCreate(provider.id)}
>
{provider.name}
</DropdownItem>
component={
<Link
to={toIdentityProviderCreate({
realm,
providerId: provider.id,
})}
data-testid={provider.id}
>
{provider.name}
</Link>
}
/>
))}
</DropdownGroup>
));
@ -243,7 +255,7 @@ export const IdentityProvidersSection = () => {
},
{
name: "providerId",
displayKey: "identity-providers:provider",
displayKey: "identity-providers:providerDetails",
cellFormatters: [upperCaseFormatter()],
},
]}

View file

@ -9,7 +9,6 @@ import {
PageSection,
} from "@patternfly/react-core";
import type { BreadcrumbData } from "use-react-router-breadcrumbs";
import type IdentityProviderRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation";
import { ViewHeader } from "../../components/view-header/ViewHeader";
import { toUpperCase } from "../../util";
@ -18,34 +17,12 @@ import { useAdminClient } from "../../context/auth/AdminClient";
import { useRealm } from "../../context/realm-context/RealmContext";
import { useAlerts } from "../../components/alert/Alerts";
import { GeneralSettings } from "./GeneralSettings";
import { toIdentityProviderTab } from "../routes/IdentityProviderTab";
export const IdentityProviderCrumb = ({ match, location }: BreadcrumbData) => {
const { t } = useTranslation();
const {
params: { id },
} = match as unknown as {
params: { [id: string]: string };
};
return (
<>
{t(
`identity-providers:${
location.pathname.endsWith("settings")
? "provider"
: "addIdentityProvider"
}`,
{
provider: toUpperCase(id),
}
)}
</>
);
};
import { toIdentityProvider } from "../routes/IdentityProvider";
import type { IdentityProviderCreateParams } from "../routes/IdentityProviderCreate";
export const AddIdentityProvider = () => {
const { t } = useTranslation("identity-providers");
const { id } = useParams<{ id: string }>();
const { providerId } = useParams<IdentityProviderCreateParams>();
const form = useForm<IdentityProviderRepresentation>();
const {
handleSubmit,
@ -61,11 +38,18 @@ export const AddIdentityProvider = () => {
try {
await adminClient.identityProviders.create({
...provider,
providerId: id,
alias: id,
providerId,
alias: providerId,
});
addAlert(t("createSuccess"), AlertVariant.success);
history.push(toIdentityProviderTab({ realm, providerId: id, alias: id }));
history.push(
toIdentityProvider({
realm,
providerId: providerId!,
alias: providerId!,
tab: "settings",
})
);
} catch (error) {
addError("identity-providers:createError", error);
}
@ -74,7 +58,9 @@ export const AddIdentityProvider = () => {
return (
<>
<ViewHeader
titleKey={t("addIdentityProvider", { provider: toUpperCase(id) })}
titleKey={t("addIdentityProvider", {
provider: toUpperCase(providerId!),
})}
/>
<PageSection variant="light">
<FormAccess
@ -83,7 +69,7 @@ export const AddIdentityProvider = () => {
onSubmit={handleSubmit(save)}
>
<FormProvider {...form}>
<GeneralSettings id={id} />
<GeneralSettings id={providerId!} />
</FormProvider>
<ActionGroup>
<Button

View file

@ -1,5 +1,5 @@
import React, { useState } from "react";
import { useHistory, useParams } from "react-router-dom";
import { Link, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import {
@ -32,6 +32,9 @@ import _ from "lodash";
import { AssociatedRolesModal } from "../../realm-roles/AssociatedRolesModal";
import type { RoleRepresentation } from "../../model/role-model";
import { useAlerts } from "../../components/alert/Alerts";
import type { IdentityProviderEditMapperParams } from "../routes/EditMapper";
import { convertToFormValues } from "../../util";
import { toIdentityProvider } from "../routes/IdentityProvider";
type IdPMapperRepresentationWithAttributes =
IdentityProviderMapperRepresentation & {
@ -45,61 +48,119 @@ export const AddMapper = () => {
const { handleSubmit, control, register, errors } = form;
const { addAlert, addError } = useAlerts();
const history = useHistory();
const { realm } = useRealm();
const adminClient = useAdminClient();
const { providerId, alias } = useParams<IdentityProviderAddMapperParams>();
const { id } = useParams<IdentityProviderEditMapperParams>();
const isSAMLorOIDC = providerId === "saml" || providerId === "oidc";
const [mapperTypes, setMapperTypes] =
useState<Record<string, IdentityProviderMapperRepresentation>>();
const [currentMapper, setCurrentMapper] =
useState<IdentityProviderMapperRepresentation>();
const [roles, setRoles] = useState<RoleRepresentation[]>([]);
const [rolesModalOpen, setRolesModalOpen] = useState(false);
const save = async (idpMapper: IdentityProviderMapperRepresentation) => {
try {
await adminClient.identityProviders.createMapper({
identityProviderMapper: {
...idpMapper,
identityProviderAlias: alias,
config: {
...idpMapper.config,
attributes: JSON.stringify(idpMapper.config.attributes),
},
if (id) {
const updatedMapper = {
...idpMapper,
identityProviderAlias: alias!,
id: id,
name: currentMapper?.name!,
config: {
...idpMapper.config,
attributes: JSON.stringify(idpMapper.config?.attributes!),
},
alias: alias!,
});
addAlert(t("mapperCreateSuccess"), AlertVariant.success);
} catch (error) {
addError(t("mapperCreateError"), error);
};
try {
await adminClient.identityProviders.updateMapper(
{
id: id!,
alias: alias!,
},
updatedMapper
);
addAlert(t("mapperSaveSuccess"), AlertVariant.success);
} catch (error) {
addError(t("mapperSaveError"), error);
}
} else {
try {
await adminClient.identityProviders.createMapper({
identityProviderMapper: {
...idpMapper,
identityProviderAlias: alias,
config: {
...idpMapper.config,
attributes: JSON.stringify(idpMapper.config.attributes),
},
},
alias: alias!,
});
addAlert(t("mapperCreateSuccess"), AlertVariant.success);
} catch (error) {
addError(t("mapperCreateError"), error);
}
}
};
const { fields, append, remove } = useFieldArray({
const { append, remove, fields } = useFieldArray({
control: form.control,
name: "attributes",
name: "config.attributes",
});
useFetch(
async () => {
const allMapperTypes =
await adminClient.identityProviders.findMapperTypes({
alias: alias!,
});
() =>
Promise.all([
id ? adminClient.identityProviders.findOneMapper({ alias, id }) : null,
adminClient.identityProviders.findMapperTypes({ alias }),
!id ? adminClient.roles.find() : null,
]),
([mapper, mapperTypes, roles]) => {
if (mapper) {
setCurrentMapper(mapper);
setupForm(mapper);
}
const allRoles = await adminClient.roles.find();
return { allMapperTypes, allRoles };
},
({ allMapperTypes, allRoles }) => {
setMapperTypes(allMapperTypes);
setRoles(allRoles);
setMapperTypes(mapperTypes);
if (roles) {
setRoles(roles);
}
},
[]
);
const setupForm = (mapper: IdentityProviderMapperRepresentation) => {
form.reset();
Object.entries(mapper).map(([key, value]) => {
if (key === "config") {
if (mapper.config?.["are-attribute-values-regex"]) {
form.setValue(
"config.are-attribute-values-regex",
value["are-attribute-values-regex"][0]
);
}
if (mapper.config?.attributes) {
form.setValue("config.attributes", JSON.parse(value.attributes));
}
if (mapper.config?.role) {
form.setValue("config.role", value.role[0]);
}
convertToFormValues(value, "config", form.setValue);
}
form.setValue(key, value);
});
};
const syncModes = ["inherit", "import", "legacy", "force"];
const [syncModeOpen, setSyncModeOpen] = useState(false);
const [mapperTypeOpen, setMapperTypeOpen] = useState(false);
@ -113,7 +174,7 @@ export const AddMapper = () => {
<PageSection variant="light">
<ViewHeader
className="kc-add-mapper-title"
titleKey={t("addIdPMapper")}
titleKey={id ? t("editIdPMapper") : t("addIdPMapper")}
divider
/>
<AssociatedRolesModal
@ -122,6 +183,7 @@ export const AddMapper = () => {
open={rolesModalOpen}
omitComposites
isRadio
isMapperId
toggleDialog={toggleModal}
/>
<FormAccess
@ -130,6 +192,29 @@ export const AddMapper = () => {
onSubmit={handleSubmit(save)}
className="pf-u-mt-lg"
>
{id && (
<FormGroup
label={t("common:id")}
fieldId="kc-mapper-id"
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register()}
type="text"
value={currentMapper?.id}
datatest-id="name-input"
id="kc-name"
name="name"
isDisabled={!!id}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
/>
</FormGroup>
)}
<FormGroup
label={t("common:name")}
labelIcon={
@ -153,6 +238,7 @@ export const AddMapper = () => {
datatest-id="name-input"
id="kc-name"
name="name"
isDisabled={!!id}
validated={
errors.name ? ValidatedOptions.error : ValidatedOptions.default
}
@ -217,12 +303,17 @@ export const AddMapper = () => {
>
<Controller
name="identityProviderMapper"
defaultValue={"saml-advanced-role-idp-mapper"}
defaultValue={
providerId === "saml"
? "saml-advanced-role-idp-mapper"
: "oidc-advanced-role-idp-mapper"
}
control={control}
render={({ onChange, value }) => (
<Select
toggleId="identityProviderMapper"
data-testid="idp-mapper-select"
isDisabled={!!id}
required
direction="down"
onToggle={() => setMapperTypeOpen(!mapperTypeOpen)}
@ -342,7 +433,7 @@ export const AddMapper = () => {
helperTextInvalid={t("common:required")}
>
<TextInput
ref={register({ required: true })}
ref={register()}
type="text"
id="kc-role"
data-testid="mapper-role-input"
@ -440,7 +531,17 @@ export const AddMapper = () => {
</Button>
<Button
variant="link"
onClick={() => history.push(`/${realm}/client-scopes`)}
component={(props) => (
<Link
{...props}
to={toIdentityProvider({
realm,
providerId,
alias: alias!,
tab: "settings",
})}
/>
)}
>
{t("common:cancel")}
</Button>

View file

@ -1,4 +1,4 @@
import React from "react";
import React, { useState } from "react";
import { Link, useHistory, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next";
import { Controller, FormProvider, useForm } from "react-hook-form";
@ -11,6 +11,7 @@ import {
DropdownItem,
Form,
PageSection,
Spinner,
Tab,
TabTitleText,
ToolbarItem,
@ -38,7 +39,10 @@ import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTa
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import type IdentityProviderMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/identityProviderMapperRepresentation";
import { toIdentityProviderAddMapper } from "../routes/AddMapper";
import { toIdentityProviderEditMapper } from "../routes/EditMapper";
import { toUpperCase } from "../../util";
import type { IdentityProviderParams } from "../routes/IdentityProvider";
type HeaderProps = {
onChange: (value: boolean) => void;
@ -52,6 +56,7 @@ type IdPWithMapperAttributes = IdentityProviderMapperRepresentation & {
category?: string;
helpText?: string;
type: string;
mapperId: string;
};
const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
@ -95,21 +100,35 @@ const Header = ({ onChange, value, save, toggleDeleteDialog }: HeaderProps) => {
export const DetailSettings = () => {
const { t } = useTranslation("identity-providers");
const { providerId, alias } =
useParams<{ providerId: string; alias: string }>();
const { alias, providerId } = useParams<IdentityProviderParams>();
const form = useForm<IdentityProviderRepresentation>();
const { handleSubmit, getValues, reset } = form;
const [provider, setProvider] = useState<IdentityProviderRepresentation>();
const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts();
const history = useHistory();
const { realm } = useRealm();
const MapperLink = ({ name, mapperId }: IdPWithMapperAttributes) => (
<Link
to={toIdentityProviderEditMapper({
realm,
alias,
providerId: provider?.providerId!,
id: mapperId,
})}
>
{name}
</Link>
);
useFetch(
() => adminClient.identityProviders.findOne({ alias: alias }),
() => adminClient.identityProviders.findOne({ alias }),
(fetchedProvider) => {
reset(fetchedProvider);
setProvider(fetchedProvider);
},
[]
);
@ -143,10 +162,14 @@ export const DetailSettings = () => {
},
});
if (!provider) {
return <Spinner />;
}
const sections = [t("generalSettings"), t("advancedSettings")];
const isOIDC = providerId.includes("oidc");
const isSAML = providerId.includes("saml");
const isOIDC = provider.providerId!.includes("oidc");
const isSAML = provider.providerId!.includes("saml");
const loader = async () => {
const [loaderMappers, loaderMapperTypes] = await Promise.all([
@ -164,6 +187,7 @@ export const DetailSettings = () => {
...mapperType,
name: loaderMapper.name!,
type: mapperType?.name!,
mapperId: loaderMapper.id!,
};
return result;
@ -243,7 +267,7 @@ export const DetailSettings = () => {
isHorizontal
onSubmit={handleSubmit(save)}
>
<AdvancedSettings isOIDC={isOIDC} isSAML={isSAML} />
<AdvancedSettings isOIDC={isOIDC!} isSAML={isSAML!} />
<ActionGroup className="keycloak__form_actions">
<Button data-testid={"save"} type="submit">
@ -279,7 +303,8 @@ export const DetailSettings = () => {
toIdentityProviderAddMapper({
realm,
alias: alias!,
providerId: providerId,
providerId: provider.providerId!,
tab: "mappers",
})
)
}
@ -296,7 +321,8 @@ export const DetailSettings = () => {
to={toIdentityProviderAddMapper({
realm,
alias: alias!,
providerId: providerId,
providerId: provider.providerId!,
tab: "mappers",
})}
datatest-id="add-mapper-button"
>
@ -308,6 +334,7 @@ export const DetailSettings = () => {
{
name: "name",
displayKey: "common:name",
cellRenderer: MapperLink,
},
{
name: "category",

View file

@ -7,13 +7,13 @@ import { HelpItem } from "../../components/help-enabler/HelpItem";
import { RedirectUrl } from "../component/RedirectUrl";
import { TextField } from "../component/TextField";
import { DisplayOrder } from "../component/DisplayOrder";
import type { IdentityProviderTabParams } from "../routes/IdentityProviderTab";
import type { IdentityProviderParams } from "../routes/IdentityProvider";
import { useParams } from "react-router-dom";
export const OIDCGeneralSettings = ({ id }: { id: string }) => {
const { t } = useTranslation("identity-providers");
const { t: th } = useTranslation("identity-providers-help");
const { tab } = useParams<IdentityProviderTabParams>();
const { tab } = useParams<IdentityProviderParams>();
const { register, errors } = useFormContext();

View file

@ -8,12 +8,12 @@ import { RedirectUrl } from "../component/RedirectUrl";
import { TextField } from "../component/TextField";
import { DisplayOrder } from "../component/DisplayOrder";
import { useParams } from "react-router";
import type { IdentityProviderTabParams } from "../routes/IdentityProviderTab";
import type { IdentityProviderParams } from "../routes/IdentityProvider";
export const SamlGeneralSettings = ({ id }: { id: string }) => {
const { t } = useTranslation("identity-providers");
const { t: th } = useTranslation("identity-providers-help");
const { tab } = useParams<IdentityProviderTabParams>();
const { tab } = useParams<IdentityProviderParams>();
const { register, errors } = useFormContext();

View file

@ -3,10 +3,11 @@ export default {
listExplain:
"Through Identity Brokering it's easy to allow users to authenticate to Keycloak using external Identity Provider or Social Networks.",
searchForProvider: "Search for provider",
provider: "Provider details",
providerDetails: "Provider details",
addProvider: "Add provider",
addMapper: "Add mapper",
addIdPMapper: "Add Identity Provider Mapper",
editIdPMapper: "Edit Identity Provider Mapper",
mappersList: "Mappers list",
noMappers: "No Mappers",
noMappersInstructions:
@ -160,6 +161,8 @@ export default {
selectRole: "Select role",
mapperCreateSuccess: "Mapper created successfully.",
mapperCreateError: "Error creating mapper.",
mapperSaveSuccess: "Mapper saved successfully.",
mapperSaveError: "Error saving mapper: {{error}}",
userSessionAttribute: "User Session Attribute",
userSessionAttributeValue: "User Session Attribute Value",
},

View file

@ -4,17 +4,19 @@ import { IdentityProviderKeycloakOidcRoute } from "./routes/IdentityProviderKeyc
import { IdentityProviderOidcRoute } from "./routes/IdentityProviderOidc";
import { IdentityProviderSamlRoute } from "./routes/IdentityProviderSaml";
import { IdentityProvidersRoute } from "./routes/IdentityProviders";
import { IdentityProviderTabRoute } from "./routes/IdentityProviderTab";
import { IdentityProviderAddMapperRoute } from "./routes/AddMapper";
import { IdentityProviderEditMapperRoute } from "./routes/EditMapper";
import { IdentityProviderCreateRoute } from "./routes/IdentityProviderCreate";
const routes: RouteDef[] = [
IdentityProviderAddMapperRoute,
IdentityProviderEditMapperRoute,
IdentityProvidersRoute,
IdentityProviderOidcRoute,
IdentityProviderSamlRoute,
IdentityProviderKeycloakOidcRoute,
IdentityProviderCreateRoute,
IdentityProviderRoute,
IdentityProviderTabRoute,
IdentityProviderAddMapperRoute,
];
export default routes;

View file

@ -7,12 +7,14 @@ export type IdentityProviderAddMapperParams = {
realm: string;
providerId: string;
alias: string;
tab: string;
};
export const IdentityProviderAddMapperRoute: RouteDef = {
path: "/:realm/identity-providers/:providerId/:alias/mappers/create",
path: "/:realm/identity-providers/:providerId/:alias/:tab/create",
component: AddMapper,
access: "manage-identity-providers",
breadcrumb: (t) => t("identity-providers:addIdPMapper"),
};
export const toIdentityProviderAddMapper = (

View file

@ -0,0 +1,24 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { AddMapper } from "../add/AddMapper";
export type IdentityProviderEditMapperParams = {
realm: string;
providerId: string;
alias: string;
id: string;
};
export const IdentityProviderEditMapperRoute: RouteDef = {
path: "/:realm/identity-providers/:providerId/:alias/mappers/:id",
component: AddMapper,
access: "manage-identity-providers",
breadcrumb: (t) => t("identity-providers:editIdPMapper"),
};
export const toIdentityProviderEditMapper = (
params: IdentityProviderEditMapperParams
): LocationDescriptorObject => ({
pathname: generatePath(IdentityProviderEditMapperRoute.path, params),
});

View file

@ -1,20 +1,21 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import {
AddIdentityProvider,
IdentityProviderCrumb,
} from "../add/AddIdentityProvider";
import { DetailSettings } from "../add/DetailSettings";
type IdentityProviderTabs = "settings" | "mappers";
export type IdentityProviderParams = {
realm: string;
id: string;
providerId: string;
alias: string;
tab: IdentityProviderTabs;
};
export const IdentityProviderRoute: RouteDef = {
path: "/:realm/identity-providers/:id",
component: AddIdentityProvider,
breadcrumb: () => IdentityProviderCrumb,
path: "/:realm/identity-providers/:providerId/:alias/:tab",
component: DetailSettings,
breadcrumb: (t) => t("identity-providers:providerDetails"),
access: "manage-identity-providers",
};

View file

@ -0,0 +1,22 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { AddIdentityProvider } from "../add/AddIdentityProvider";
export type IdentityProviderCreateParams = {
realm: string;
providerId: string;
};
export const IdentityProviderCreateRoute: RouteDef = {
path: "/:realm/identity-providers/:providerId/add",
component: AddIdentityProvider,
breadcrumb: (t) => t("identity-providers:addProvider"),
access: "manage-identity-providers",
};
export const toIdentityProviderCreate = (
params: IdentityProviderCreateParams
): LocationDescriptorObject => ({
pathname: generatePath(IdentityProviderCreateRoute.path, params),
});

View file

@ -6,7 +6,7 @@ import { AddOpenIdConnect } from "../add/AddOpenIdConnect";
export type IdentityProviderKeycloakOidcParams = { realm: string };
export const IdentityProviderKeycloakOidcRoute: RouteDef = {
path: "/:realm/identity-providers/keycloak-oidc",
path: "/:realm/identity-providers/keycloak-oidc/add",
component: AddOpenIdConnect,
breadcrumb: (t) => t("identity-providers:addKeycloakOpenIdProvider"),
access: "manage-identity-providers",

View file

@ -6,7 +6,7 @@ import { AddOpenIdConnect } from "../add/AddOpenIdConnect";
export type IdentityProviderOidcParams = { realm: string };
export const IdentityProviderOidcRoute: RouteDef = {
path: "/:realm/identity-providers/oidc",
path: "/:realm/identity-providers/oidc/add",
component: AddOpenIdConnect,
breadcrumb: (t) => t("identity-providers:addOpenIdProvider"),
access: "manage-identity-providers",

View file

@ -6,7 +6,7 @@ import { AddSamlConnect } from "../add/AddSamlConnect";
export type IdentityProviderSamlParams = { realm: string };
export const IdentityProviderSamlRoute: RouteDef = {
path: "/:realm/identity-providers/saml",
path: "/:realm/identity-providers/saml/add",
component: AddSamlConnect,
breadcrumb: (t) => t("identity-providers:addSamlProvider"),
access: "manage-identity-providers",

View file

@ -1,25 +0,0 @@
import type { LocationDescriptorObject } from "history";
import { generatePath } from "react-router-dom";
import type { RouteDef } from "../../route-config";
import { DetailSettings } from "../add/DetailSettings";
export type IdentityProviderTab = "settings";
export type IdentityProviderTabParams = {
realm: string;
providerId?: string;
alias: string;
tab?: IdentityProviderTab;
};
export const IdentityProviderTabRoute: RouteDef = {
path: "/:realm/identity-providers/:providerId?/:alias/:tab?",
component: DetailSettings,
access: "manage-identity-providers",
};
export const toIdentityProviderTab = (
params: IdentityProviderTabParams
): LocationDescriptorObject => ({
pathname: generatePath(IdentityProviderTabRoute.path, params),
});

View file

@ -16,6 +16,7 @@ import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable
import { ListEmptyState } from "../components/list-empty-state/ListEmptyState";
import { CaretDownIcon, FilterIcon } from "@patternfly/react-icons";
import _ from "lodash";
import type { RealmRoleParams } from "./routes/RealmRole";
type Role = RoleRepresentation & {
clientId?: string;
@ -29,6 +30,7 @@ export type AssociatedRolesModalProps = {
allRoles?: RoleRepresentation[];
omitComposites?: boolean;
isRadio?: boolean;
isMapperId?: boolean;
};
export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
@ -42,7 +44,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
const { id } = useParams<{ id: string }>();
const { id } = useParams<RealmRoleParams>();
const alphabetize = (rolesList: RoleRepresentation[]) => {
return _.sortBy(rolesList, (role) => role.name?.toUpperCase());
@ -144,16 +146,11 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
}, [filterType]);
useFetch(
async () => {
if (id) return await adminClient.roles.findOneById({ id });
},
(fetchedRole) => {
if (fetchedRole) {
setName(fetchedRole.name!);
} else {
setName(t("createRole"));
}
},
() =>
!props.isMapperId
? adminClient.roles.findOneById({ id })
: Promise.resolve(null),
(role) => setName(role ? role.name! : t("createRole")),
[]
);
@ -183,7 +180,7 @@ export const AssociatedRolesModal = (props: AssociatedRolesModalProps) => {
key="add"
data-testid="add-associated-roles-button"
variant="primary"
isDisabled={!selectedRows?.length}
isDisabled={!selectedRows.length}
onClick={() => {
props.toggleDialog();
props.onConfirm(selectedRows);

View file

@ -23,7 +23,7 @@ import _ from "lodash";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useAlerts } from "../components/alert/Alerts";
import { UserIdpModal } from "./UserIdPModal";
import { toIdentityProviderTab } from "../identity-providers/routes/IdentityProviderTab";
import { toIdentityProvider } from "../identity-providers/routes/IdentityProvider";
export const UserIdentityProviderLinks = () => {
const [key, setKey] = useState(0);
@ -42,10 +42,25 @@ export const UserIdentityProviderLinks = () => {
setIsLinkIdPModalOpen(!isLinkIdPModalOpen);
};
type withProviderId = FederatedIdentityRepresentation & {
providerId: string;
};
const identityProviders = useServerInfo().identityProviders;
const getFederatedIdentities = async () => {
return await adminClient.users.listFederatedIdentities({ id });
const allProviders = await adminClient.identityProviders.find();
const allFedIds = (await adminClient.users.listFederatedIdentities({
id,
})) as unknown as withProviderId[];
for (const element of allFedIds) {
element.providerId = allProviders.find(
(item) => item.alias === element.identityProvider
)?.providerId!;
}
return allFedIds;
};
const getAvailableIdPs = async () => {
@ -89,11 +104,12 @@ export const UserIdentityProviderLinks = () => {
},
});
const idpLinkRenderer = (idp: FederatedIdentityRepresentation) => {
const idpLinkRenderer = (idp: withProviderId) => {
return (
<Link
to={toIdentityProviderTab({
to={toIdentityProvider({
realm,
providerId: idp.providerId,
alias: idp.identityProvider!,
tab: "settings",
})}