Added mapping tab to clients (#1375)
* initial version mapper tab for clients * added missing client select component * added test * Update src/client-scopes/add/components/ClientSelectComponent.tsx Co-authored-by: Jon Koops <jonkoops@gmail.com> * Update src/client-scopes/add/components/ClientSelectComponent.tsx Co-authored-by: Jon Koops <jonkoops@gmail.com> * now uses adminClient type for client search Co-authored-by: Jon Koops <jonkoops@gmail.com>
This commit is contained in:
parent
82c04d2e4a
commit
acd8921b20
14 changed files with 396 additions and 162 deletions
|
@ -188,6 +188,36 @@ describe("Clients test", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("Mapping tab", () => {
|
||||
const mappingClient = "mapping-client";
|
||||
beforeEach(() => {
|
||||
keycloakBefore();
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToClients();
|
||||
listingPage.searchItem(mappingClient).goToItemDetails(mappingClient);
|
||||
});
|
||||
|
||||
before(() => {
|
||||
new AdminClient().createClient({
|
||||
protocol: "openid-connect",
|
||||
clientId: mappingClient,
|
||||
publicClient: false,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
new AdminClient().deleteClient(mappingClient);
|
||||
});
|
||||
|
||||
it("add mapping to openid client", () => {
|
||||
cy.get("#pf-tab-mappers-mappers").click();
|
||||
cy.findByText("Add predefined mapper").click();
|
||||
cy.get("table input").first().click();
|
||||
cy.findByTestId("modalConfirm").click();
|
||||
masthead.checkNotificationMessage("Mapping successfully created");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Keys tab test", () => {
|
||||
const keysName = "keys-client";
|
||||
beforeEach(() => {
|
||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -7,7 +7,7 @@
|
|||
"name": "keycloak-admin-ui",
|
||||
"license": "Apache",
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-admin-client": "^16.0.0-dev.32",
|
||||
"@keycloak/keycloak-admin-client": "^16.0.0-dev.34",
|
||||
"@patternfly/patternfly": "^4.144.5",
|
||||
"@patternfly/react-code-editor": "^4.3.85",
|
||||
"@patternfly/react-core": "^4.162.3",
|
||||
|
@ -3385,9 +3385,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/@keycloak/keycloak-admin-client": {
|
||||
"version": "16.0.0-dev.32",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.32.tgz",
|
||||
"integrity": "sha512-VGecbeyhwK9oKaNm4LvmryDG+JSZCzUFsbBjv3jwuhNd/nxIP8AXpKBz+8sw3I6GgJojC4wCgGIgfQPqLeLquQ==",
|
||||
"version": "16.0.0-dev.34",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.34.tgz",
|
||||
"integrity": "sha512-Pd1s8l05QGtRgTrDPXi3BQXlyjGlOCdw4P8czNyJg5v7IyvqGuEhws2twRnFliiZFdZ1RwOzpchWfyv8E4LcCA==",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.0",
|
||||
"camelize-ts": "^1.0.8",
|
||||
|
@ -23701,9 +23701,9 @@
|
|||
}
|
||||
},
|
||||
"@keycloak/keycloak-admin-client": {
|
||||
"version": "16.0.0-dev.32",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.32.tgz",
|
||||
"integrity": "sha512-VGecbeyhwK9oKaNm4LvmryDG+JSZCzUFsbBjv3jwuhNd/nxIP8AXpKBz+8sw3I6GgJojC4wCgGIgfQPqLeLquQ==",
|
||||
"version": "16.0.0-dev.34",
|
||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.34.tgz",
|
||||
"integrity": "sha512-Pd1s8l05QGtRgTrDPXi3BQXlyjGlOCdw4P8czNyJg5v7IyvqGuEhws2twRnFliiZFdZ1RwOzpchWfyv8E4LcCA==",
|
||||
"requires": {
|
||||
"axios": "^0.21.0",
|
||||
"camelize-ts": "^1.0.8",
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
"prepare": "husky install"
|
||||
},
|
||||
"dependencies": {
|
||||
"@keycloak/keycloak-admin-client": "^16.0.0-dev.32",
|
||||
"@keycloak/keycloak-admin-client": "^16.0.0-dev.34",
|
||||
"@patternfly/patternfly": "^4.144.5",
|
||||
"@patternfly/react-code-editor": "^4.3.85",
|
||||
"@patternfly/react-core": "^4.162.3",
|
||||
|
|
86
src/client-scopes/add/components/ClientSelectComponent.tsx
Normal file
86
src/client-scopes/add/components/ClientSelectComponent.tsx
Normal file
|
@ -0,0 +1,86 @@
|
|||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import {
|
||||
FormGroup,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||
import { useAdminClient, useFetch } from "../../../context/auth/AdminClient";
|
||||
import { HelpItem } from "../../../components/help-enabler/HelpItem";
|
||||
import type { ComponentProps } from "./components";
|
||||
|
||||
export const ClientSelectComponent = ({
|
||||
name,
|
||||
label,
|
||||
helpText,
|
||||
defaultValue,
|
||||
}: ComponentProps) => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
const { control } = useFormContext();
|
||||
|
||||
const [open, setOpen] = useState(false);
|
||||
const [clients, setClients] = useState<JSX.Element[]>();
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const adminClient = useAdminClient();
|
||||
|
||||
useFetch(
|
||||
() => {
|
||||
const params: ClientQuery = {
|
||||
max: 20,
|
||||
};
|
||||
if (search) {
|
||||
params.clientId = search;
|
||||
params.search = true;
|
||||
}
|
||||
return adminClient.clients.find(params);
|
||||
},
|
||||
(clients) =>
|
||||
setClients(
|
||||
clients.map((option) => (
|
||||
<SelectOption key={option.id} value={option.clientId} />
|
||||
))
|
||||
),
|
||||
[search]
|
||||
);
|
||||
|
||||
return (
|
||||
<FormGroup
|
||||
label={t(label!)}
|
||||
labelIcon={
|
||||
<HelpItem helpText={t(helpText!)} forLabel={t(label!)} forID={name!} />
|
||||
}
|
||||
fieldId={name!}
|
||||
>
|
||||
<Controller
|
||||
name={`config.${name?.replaceAll(".", "-")}`}
|
||||
defaultValue={defaultValue || ""}
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Select
|
||||
toggleId={name}
|
||||
variant={SelectVariant.typeahead}
|
||||
onToggle={(open) => setOpen(open)}
|
||||
isOpen={open}
|
||||
selections={value}
|
||||
onFilter={(_, value) => {
|
||||
setSearch(value);
|
||||
return clients;
|
||||
}}
|
||||
onSelect={(_, value) => {
|
||||
onChange(value.toString());
|
||||
setOpen(false);
|
||||
}}
|
||||
aria-label={t(label!)}
|
||||
>
|
||||
{clients}
|
||||
</Select>
|
||||
)}
|
||||
/>
|
||||
</FormGroup>
|
||||
);
|
||||
};
|
|
@ -6,9 +6,17 @@ import { BooleanComponent } from "./BooleanComponent";
|
|||
import { ListComponent } from "./ListComponent";
|
||||
import { RoleComponent } from "./RoleComponent";
|
||||
import { ScriptComponent } from "./ScriptComponent";
|
||||
import { ClientSelectComponent } from "./ClientSelectComponent";
|
||||
|
||||
export type ComponentProps = Omit<ConfigPropertyRepresentation, "type">;
|
||||
const ComponentTypes = ["String", "boolean", "List", "Role", "Script"] as const;
|
||||
const ComponentTypes = [
|
||||
"String",
|
||||
"boolean",
|
||||
"List",
|
||||
"Role",
|
||||
"Script",
|
||||
"ClientList",
|
||||
] as const;
|
||||
|
||||
export type Components = typeof ComponentTypes[number];
|
||||
|
||||
|
@ -20,4 +28,5 @@ export const COMPONENTS: {
|
|||
List: ListComponent,
|
||||
Role: RoleComponent,
|
||||
Script: ScriptComponent,
|
||||
ClientList: ClientSelectComponent,
|
||||
} as const;
|
||||
|
|
|
@ -1,31 +1,27 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link, useHistory } from "react-router-dom";
|
||||
import {
|
||||
AlertVariant,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownToggle,
|
||||
} from "@patternfly/react-core";
|
||||
import type { LocationDescriptorObject } from "history";
|
||||
import { Link } from "react-router-dom";
|
||||
import { Dropdown, DropdownItem, DropdownToggle } from "@patternfly/react-core";
|
||||
import { CaretDownIcon } from "@patternfly/react-icons";
|
||||
|
||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||
import type ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation";
|
||||
import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
|
||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||
|
||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
import { AddMapperDialog } from "../add/MapperDialog";
|
||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { toMapper } from "../routes/Mapper";
|
||||
|
||||
type MapperListProps = {
|
||||
clientScope: ClientScopeRepresentation;
|
||||
type: string;
|
||||
refresh: () => void;
|
||||
model: ClientScopeRepresentation | ClientRepresentation;
|
||||
onAdd: (
|
||||
mappers: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
|
||||
) => void;
|
||||
onDelete: (mapper: ProtocolMapperRepresentation) => void;
|
||||
detailLink: (id: string) => LocationDescriptorObject;
|
||||
};
|
||||
|
||||
type Row = ProtocolMapperRepresentation & {
|
||||
|
@ -34,24 +30,23 @@ type Row = ProtocolMapperRepresentation & {
|
|||
priority: number;
|
||||
};
|
||||
|
||||
export const MapperList = ({ clientScope, type, refresh }: MapperListProps) => {
|
||||
export const MapperList = ({
|
||||
model,
|
||||
onAdd,
|
||||
onDelete,
|
||||
detailLink,
|
||||
}: MapperListProps) => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
const adminClient = useAdminClient();
|
||||
const { addAlert, addError } = useAlerts();
|
||||
|
||||
const history = useHistory();
|
||||
const { realm } = useRealm();
|
||||
|
||||
const [mapperAction, setMapperAction] = useState(false);
|
||||
const mapperList = clientScope.protocolMappers;
|
||||
const mapperTypes =
|
||||
useServerInfo().protocolMapperTypes![clientScope.protocol!];
|
||||
const mapperList = model.protocolMappers;
|
||||
const mapperTypes = useServerInfo().protocolMapperTypes![model.protocol!];
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
useEffect(() => setKey(new Date().getTime()), [mapperList]);
|
||||
useEffect(() => setKey(key + 1), [mapperList]);
|
||||
|
||||
const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false);
|
||||
const [filter, setFilter] = useState(clientScope.protocolMappers);
|
||||
const [filter, setFilter] = useState(model.protocolMappers);
|
||||
const toggleAddMapperDialog = (buildIn: boolean) => {
|
||||
if (buildIn) {
|
||||
setFilter(mapperList || []);
|
||||
|
@ -61,33 +56,6 @@ export const MapperList = ({ clientScope, type, refresh }: MapperListProps) => {
|
|||
setAddMapperDialogOpen(!addMapperDialogOpen);
|
||||
};
|
||||
|
||||
const addMappers = async (
|
||||
mappers: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
|
||||
): Promise<void> => {
|
||||
if (filter === undefined) {
|
||||
const mapper = mappers as ProtocolMapperTypeRepresentation;
|
||||
history.push(
|
||||
toMapper({
|
||||
realm,
|
||||
id: clientScope.id!,
|
||||
type,
|
||||
mapperId: mapper.id!,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await adminClient.clientScopes.addMultipleProtocolMappers(
|
||||
{ id: clientScope.id! },
|
||||
mappers as ProtocolMapperRepresentation[]
|
||||
);
|
||||
refresh();
|
||||
addAlert(t("common:mappingCreatedSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("common:mappingCreatedError", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const loader = async () =>
|
||||
Promise.resolve(
|
||||
(mapperList || [])
|
||||
|
@ -106,17 +74,15 @@ export const MapperList = ({ clientScope, type, refresh }: MapperListProps) => {
|
|||
);
|
||||
|
||||
const MapperLink = ({ id, name }: Row) => (
|
||||
<Link to={toMapper({ realm, id: clientScope.id!, type, mapperId: id! })}>
|
||||
{name}
|
||||
</Link>
|
||||
<Link to={detailLink(id!)}>{name}</Link>
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<AddMapperDialog
|
||||
protocol={clientScope.protocol!}
|
||||
protocol={model.protocol!}
|
||||
filter={filter}
|
||||
onConfirm={addMappers}
|
||||
onConfirm={onAdd}
|
||||
open={addMapperDialogOpen}
|
||||
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
|
||||
/>
|
||||
|
@ -159,22 +125,7 @@ export const MapperList = ({ clientScope, type, refresh }: MapperListProps) => {
|
|||
actions={[
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: async (mapper) => {
|
||||
try {
|
||||
await adminClient.clientScopes.delProtocolMapper({
|
||||
id: clientScope.id!,
|
||||
mapperId: mapper.id!,
|
||||
});
|
||||
addAlert(
|
||||
t("common:mappingDeletedSuccess"),
|
||||
AlertVariant.success
|
||||
);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("common:mappingDeletedError", error);
|
||||
}
|
||||
return true;
|
||||
},
|
||||
onRowClick: onDelete,
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React, { useState } from "react";
|
||||
import { Link, useHistory, useParams } from "react-router-dom";
|
||||
import { Link, useHistory, useParams, useRouteMatch } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { FormProvider, useForm } from "react-hook-form";
|
||||
import {
|
||||
|
@ -25,11 +25,11 @@ import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
|||
import { convertFormValuesToObject, convertToFormValues } from "../../util";
|
||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import type { MapperParams } from "../routes/Mapper";
|
||||
import { MapperParams, MapperRoute } from "../routes/Mapper";
|
||||
import { Components, COMPONENTS } from "../add/components/components";
|
||||
import { toClientScope } from "../routes/ClientScope";
|
||||
|
||||
import "./mapping-details.css";
|
||||
import { toClientScope } from "../routes/ClientScope";
|
||||
|
||||
export const MappingDetails = () => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
|
@ -47,15 +47,29 @@ export const MappingDetails = () => {
|
|||
const { realm } = useRealm();
|
||||
const serverInfo = useServerInfo();
|
||||
const isGuid = /^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$/;
|
||||
const isUpdating = mapperId.match(isGuid);
|
||||
const isUpdating = !!mapperId.match(isGuid);
|
||||
|
||||
const isOnClientScope = !!useRouteMatch(MapperRoute.path);
|
||||
const toDetails = () =>
|
||||
isOnClientScope
|
||||
? toClientScope({ realm, id, type: type!, tab: "mappers" })
|
||||
: `/${realm}/clients/${id}/mappers`;
|
||||
|
||||
useFetch(
|
||||
async () => {
|
||||
let data: ProtocolMapperRepresentation | undefined;
|
||||
if (isUpdating) {
|
||||
const data = await adminClient.clientScopes.findProtocolMapper({
|
||||
id,
|
||||
mapperId,
|
||||
});
|
||||
if (isOnClientScope) {
|
||||
data = await adminClient.clientScopes.findProtocolMapper({
|
||||
id,
|
||||
mapperId,
|
||||
});
|
||||
} else {
|
||||
data = await adminClient.clients.findProtocolMapperById({
|
||||
id,
|
||||
mapperId,
|
||||
});
|
||||
}
|
||||
if (!data) {
|
||||
throw new Error(t("common:notFound"));
|
||||
}
|
||||
|
@ -74,12 +88,14 @@ export const MappingDetails = () => {
|
|||
data,
|
||||
};
|
||||
} else {
|
||||
const scope = await adminClient.clientScopes.findOne({ id });
|
||||
if (!scope) {
|
||||
const model = type
|
||||
? await adminClient.clientScopes.findOne({ id })
|
||||
: await adminClient.clients.findOne({ id });
|
||||
if (!model) {
|
||||
throw new Error(t("common:notFound"));
|
||||
}
|
||||
const protocolMappers =
|
||||
serverInfo.protocolMapperTypes![scope.protocol!];
|
||||
serverInfo.protocolMapperTypes![model.protocol!];
|
||||
const mapping = protocolMappers.find(
|
||||
(mapper) => mapper.id === mapperId
|
||||
);
|
||||
|
@ -89,7 +105,7 @@ export const MappingDetails = () => {
|
|||
return {
|
||||
mapping,
|
||||
config: {
|
||||
protocol: scope.protocol,
|
||||
protocol: model.protocol,
|
||||
protocolMapper: mapperId,
|
||||
},
|
||||
};
|
||||
|
@ -117,12 +133,19 @@ export const MappingDetails = () => {
|
|||
continueButtonVariant: ButtonVariant.danger,
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
await adminClient.clientScopes.delProtocolMapper({
|
||||
id,
|
||||
mapperId: mapperId,
|
||||
});
|
||||
if (isOnClientScope) {
|
||||
await adminClient.clientScopes.delProtocolMapper({
|
||||
id,
|
||||
mapperId,
|
||||
});
|
||||
} else {
|
||||
await adminClient.clients.delProtocolMapper({
|
||||
id,
|
||||
mapperId,
|
||||
});
|
||||
}
|
||||
addAlert(t("common:mappingDeletedSuccess"), AlertVariant.success);
|
||||
history.push(`/${realm}/client-scopes/${id}/mappers`);
|
||||
history.push(toDetails());
|
||||
} catch (error) {
|
||||
addError("common:mappingDeletedError", error);
|
||||
}
|
||||
|
@ -133,16 +156,21 @@ export const MappingDetails = () => {
|
|||
const configAttributes = convertFormValuesToObject(formMapping.config);
|
||||
const key = isUpdating ? "Updated" : "Created";
|
||||
try {
|
||||
const mapping = { ...formMapping, ...config, config: configAttributes };
|
||||
if (isUpdating) {
|
||||
await adminClient.clientScopes.updateProtocolMapper(
|
||||
{ id, mapperId },
|
||||
{ ...formMapping, config: configAttributes }
|
||||
);
|
||||
isOnClientScope
|
||||
? await adminClient.clientScopes.updateProtocolMapper(
|
||||
{ id, mapperId },
|
||||
{ id: mapperId, ...mapping }
|
||||
)
|
||||
: await adminClient.clients.updateProtocolMapper(
|
||||
{ id, mapperId },
|
||||
{ id: mapperId, ...mapping }
|
||||
);
|
||||
} else {
|
||||
await adminClient.clientScopes.addProtocolMapper(
|
||||
{ id },
|
||||
{ ...formMapping, ...config, config: configAttributes }
|
||||
);
|
||||
isOnClientScope
|
||||
? await adminClient.clientScopes.addProtocolMapper({ id }, mapping)
|
||||
: await adminClient.clients.addProtocolMapper({ id }, mapping);
|
||||
}
|
||||
addAlert(t(`common:mapping${key}Success`), AlertVariant.success);
|
||||
} catch (error) {
|
||||
|
@ -180,49 +208,42 @@ export const MappingDetails = () => {
|
|||
role="manage-clients"
|
||||
className="keycloak__client-scope-mapping-details__form"
|
||||
>
|
||||
{!mapperId.match(isGuid) && (
|
||||
<>
|
||||
<FormGroup label={t("common:mapperType")} fieldId="mapperType">
|
||||
<TextInput
|
||||
type="text"
|
||||
id="mapperType"
|
||||
name="mapperType"
|
||||
isReadOnly
|
||||
value={mapping?.name}
|
||||
/>
|
||||
</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:mapperType")} fieldId="mapperType">
|
||||
<TextInput
|
||||
type="text"
|
||||
id="mapperType"
|
||||
name="mapperType"
|
||||
isReadOnly
|
||||
value={mapping?.name}
|
||||
/>
|
||||
</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"
|
||||
isReadOnly={isUpdating}
|
||||
validated={
|
||||
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||
}
|
||||
/>
|
||||
</FormGroup>
|
||||
<FormProvider {...form}>
|
||||
{mapping?.properties.map((property) => {
|
||||
const componentType = property.type!;
|
||||
|
@ -242,12 +263,7 @@ export const MappingDetails = () => {
|
|||
</Button>
|
||||
<Button
|
||||
variant="link"
|
||||
component={(props) => (
|
||||
<Link
|
||||
{...props}
|
||||
to={toClientScope({ realm, id, type, tab: "mappers" })}
|
||||
/>
|
||||
)}
|
||||
component={(props) => <Link {...props} to={toDetails()} />}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>
|
||||
|
|
|
@ -25,12 +25,15 @@ import {
|
|||
Row,
|
||||
} from "../../components/role-mapping/RoleMapping";
|
||||
import type { RoleMappingPayload } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
||||
import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
|
||||
import type ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation";
|
||||
import {
|
||||
AllClientScopes,
|
||||
changeScope,
|
||||
ClientScopeDefaultOptionalType,
|
||||
} from "../../components/client-scope/ClientScopeTypes";
|
||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||
import { toMapper } from "../routes/Mapper";
|
||||
|
||||
export const ClientScopeForm = () => {
|
||||
const { t } = useTranslation("client-scopes");
|
||||
|
@ -187,6 +190,47 @@ export const ClientScopeForm = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const addMappers = async (
|
||||
mappers: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
|
||||
): Promise<void> => {
|
||||
if (!Array.isArray(mappers)) {
|
||||
const mapper = mappers as ProtocolMapperTypeRepresentation;
|
||||
history.push(
|
||||
toMapper({
|
||||
realm,
|
||||
id: clientScope!.id!,
|
||||
type,
|
||||
mapperId: mapper.id!,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await adminClient.clientScopes.addMultipleProtocolMappers(
|
||||
{ id: clientScope!.id! },
|
||||
mappers as ProtocolMapperRepresentation[]
|
||||
);
|
||||
refresh();
|
||||
addAlert(t("common:mappingCreatedSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("common:mappingCreatedError", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDelete = async (mapper: ProtocolMapperRepresentation) => {
|
||||
try {
|
||||
await adminClient.clientScopes.delProtocolMapper({
|
||||
id: clientScope!.id!,
|
||||
mapperId: mapper.id!,
|
||||
});
|
||||
addAlert(t("common:mappingDeletedSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addError("common:mappingDeletedError", error);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (id && !clientScope) {
|
||||
return (
|
||||
<div className="pf-u-text-align-center">
|
||||
|
@ -236,9 +280,12 @@ export const ClientScopeForm = () => {
|
|||
title={<TabTitleText>{t("common:mappers")}</TabTitleText>}
|
||||
>
|
||||
<MapperList
|
||||
clientScope={clientScope}
|
||||
type={type}
|
||||
refresh={refresh}
|
||||
model={clientScope}
|
||||
onAdd={addMappers}
|
||||
onDelete={onDelete}
|
||||
detailLink={(id) =>
|
||||
toMapper({ realm, id: clientScope.id!, type, mapperId: id! })
|
||||
}
|
||||
/>
|
||||
</Tab>
|
||||
<Tab
|
||||
|
|
|
@ -54,6 +54,10 @@ import { EvaluateScopes } from "./scopes/EvaluateScopes";
|
|||
import { ServiceAccount } from "./service-account/ServiceAccount";
|
||||
import { isRealmClient, getProtocolName } from "./utils";
|
||||
import { SamlKeys } from "./keys/SamlKeys";
|
||||
import { MapperList } from "../client-scopes/details/MapperList";
|
||||
import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
|
||||
import type ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation";
|
||||
import { toMapper } from "./routes/Mapper";
|
||||
|
||||
type ClientDetailHeaderProps = {
|
||||
onChange: (value: boolean) => void;
|
||||
|
@ -279,6 +283,51 @@ export const ClientDetails = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const addMappers = async (
|
||||
mappers: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
|
||||
): Promise<void> => {
|
||||
if (!Array.isArray(mappers)) {
|
||||
const mapper = mappers as ProtocolMapperTypeRepresentation;
|
||||
history.push(
|
||||
toMapper({
|
||||
realm,
|
||||
id: client!.id!,
|
||||
mapperId: mapper.id!,
|
||||
})
|
||||
);
|
||||
} else {
|
||||
try {
|
||||
await adminClient.clients.addMultipleProtocolMappers(
|
||||
{ id: client!.id! },
|
||||
mappers as ProtocolMapperRepresentation[]
|
||||
);
|
||||
setClient(await adminClient.clients.findOne({ id: client!.id! }));
|
||||
addAlert(t("common:mappingCreatedSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("common:mappingCreatedError", error);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const onDeleteMapper = async (mapper: ProtocolMapperRepresentation) => {
|
||||
try {
|
||||
await adminClient.clients.delProtocolMapper({
|
||||
id: client!.id!,
|
||||
mapperId: mapper.id!,
|
||||
});
|
||||
setClient({
|
||||
...client,
|
||||
protocolMappers: client?.protocolMappers?.filter(
|
||||
(m) => m.id !== mapper.id
|
||||
),
|
||||
});
|
||||
addAlert(t("common:mappingDeletedSuccess"), AlertVariant.success);
|
||||
} catch (error) {
|
||||
addError("common:mappingDeletedError", error);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
if (!client) {
|
||||
return (
|
||||
<div className="pf-u-text-align-center">
|
||||
|
@ -367,6 +416,22 @@ export const ClientDetails = () => {
|
|||
<Credentials clientId={clientId} save={() => save()} />
|
||||
</Tab>
|
||||
)}
|
||||
{!isRealmClient(client) && (
|
||||
<Tab
|
||||
id="mappers"
|
||||
eventKey="mappers"
|
||||
title={<TabTitleText>{t("mappers")}</TabTitleText>}
|
||||
>
|
||||
<MapperList
|
||||
model={client}
|
||||
onAdd={addMappers}
|
||||
onDelete={onDeleteMapper}
|
||||
detailLink={(mapperId) =>
|
||||
toMapper({ realm, id: client.id!, mapperId })
|
||||
}
|
||||
/>
|
||||
</Tab>
|
||||
)}
|
||||
<Tab
|
||||
id="roles"
|
||||
eventKey="roles"
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
} from "@patternfly/react-core";
|
||||
import { cellWidth, IRowData, TableText } from "@patternfly/react-table";
|
||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
||||
import type { ClientQuery } from "@keycloak/keycloak-admin-client/lib/resources/clients";
|
||||
import React, { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Link } from "react-router-dom";
|
||||
|
@ -44,13 +45,13 @@ export const ClientsSection = () => {
|
|||
const [selectedClient, setSelectedClient] = useState<ClientRepresentation>();
|
||||
|
||||
const loader = async (first?: number, max?: number, search?: string) => {
|
||||
const params: { [name: string]: string | number } = {
|
||||
const params: ClientQuery = {
|
||||
first: first!,
|
||||
max: max!,
|
||||
};
|
||||
if (search) {
|
||||
params.clientId = search;
|
||||
params.search = "true";
|
||||
params.search = true;
|
||||
}
|
||||
return await adminClient.clients.find({ ...params });
|
||||
};
|
||||
|
|
|
@ -294,5 +294,6 @@ export default {
|
|||
expires: "Expires in",
|
||||
never: "Never expires",
|
||||
},
|
||||
mappers: "Mappers",
|
||||
},
|
||||
};
|
||||
|
|
|
@ -4,6 +4,7 @@ import { ClientRoute } from "./routes/Client";
|
|||
import { ClientsRoute } from "./routes/Clients";
|
||||
import { CreateInitialAccessTokenRoute } from "./routes/CreateInitialAccessToken";
|
||||
import { ImportClientRoute } from "./routes/ImportClient";
|
||||
import { MapperRoute } from "./routes/Mapper";
|
||||
|
||||
const routes: RouteDef[] = [
|
||||
AddClientRoute,
|
||||
|
@ -11,6 +12,7 @@ const routes: RouteDef[] = [
|
|||
ClientsRoute,
|
||||
CreateInitialAccessTokenRoute,
|
||||
ClientRoute,
|
||||
MapperRoute,
|
||||
];
|
||||
|
||||
export default routes;
|
||||
|
|
|
@ -3,7 +3,12 @@ import { generatePath } from "react-router-dom";
|
|||
import type { RouteDef } from "../../route-config";
|
||||
import { ClientDetails } from "../ClientDetails";
|
||||
|
||||
export type ClientTab = "settings" | "roles" | "clientScopes" | "advanced";
|
||||
export type ClientTab =
|
||||
| "settings"
|
||||
| "roles"
|
||||
| "clientScopes"
|
||||
| "advanced"
|
||||
| "mappers";
|
||||
|
||||
export type ClientParams = {
|
||||
realm: string;
|
||||
|
|
21
src/clients/routes/Mapper.ts
Normal file
21
src/clients/routes/Mapper.ts
Normal file
|
@ -0,0 +1,21 @@
|
|||
import type { LocationDescriptorObject } from "history";
|
||||
import { generatePath } from "react-router-dom";
|
||||
import type { RouteDef } from "../../route-config";
|
||||
import { MappingDetails } from "../../client-scopes/details/MappingDetails";
|
||||
|
||||
export type MapperParams = {
|
||||
realm: string;
|
||||
id: string;
|
||||
mapperId: string;
|
||||
};
|
||||
|
||||
export const MapperRoute: RouteDef = {
|
||||
path: "/:realm/clients/:id/mappers/:mapperId",
|
||||
component: MappingDetails,
|
||||
breadcrumb: (t) => t("common:mappingDetails"),
|
||||
access: "view-clients",
|
||||
};
|
||||
|
||||
export const toMapper = (params: MapperParams): LocationDescriptorObject => ({
|
||||
pathname: generatePath(MapperRoute.path, params),
|
||||
});
|
Loading…
Reference in a new issue