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", () => {
|
describe("Keys tab test", () => {
|
||||||
const keysName = "keys-client";
|
const keysName = "keys-client";
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
14
package-lock.json
generated
14
package-lock.json
generated
|
@ -7,7 +7,7 @@
|
||||||
"name": "keycloak-admin-ui",
|
"name": "keycloak-admin-ui",
|
||||||
"license": "Apache",
|
"license": "Apache",
|
||||||
"dependencies": {
|
"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/patternfly": "^4.144.5",
|
||||||
"@patternfly/react-code-editor": "^4.3.85",
|
"@patternfly/react-code-editor": "^4.3.85",
|
||||||
"@patternfly/react-core": "^4.162.3",
|
"@patternfly/react-core": "^4.162.3",
|
||||||
|
@ -3385,9 +3385,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/@keycloak/keycloak-admin-client": {
|
"node_modules/@keycloak/keycloak-admin-client": {
|
||||||
"version": "16.0.0-dev.32",
|
"version": "16.0.0-dev.34",
|
||||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.32.tgz",
|
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.34.tgz",
|
||||||
"integrity": "sha512-VGecbeyhwK9oKaNm4LvmryDG+JSZCzUFsbBjv3jwuhNd/nxIP8AXpKBz+8sw3I6GgJojC4wCgGIgfQPqLeLquQ==",
|
"integrity": "sha512-Pd1s8l05QGtRgTrDPXi3BQXlyjGlOCdw4P8czNyJg5v7IyvqGuEhws2twRnFliiZFdZ1RwOzpchWfyv8E4LcCA==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"camelize-ts": "^1.0.8",
|
"camelize-ts": "^1.0.8",
|
||||||
|
@ -23701,9 +23701,9 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@keycloak/keycloak-admin-client": {
|
"@keycloak/keycloak-admin-client": {
|
||||||
"version": "16.0.0-dev.32",
|
"version": "16.0.0-dev.34",
|
||||||
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.32.tgz",
|
"resolved": "https://registry.npmjs.org/@keycloak/keycloak-admin-client/-/keycloak-admin-client-16.0.0-dev.34.tgz",
|
||||||
"integrity": "sha512-VGecbeyhwK9oKaNm4LvmryDG+JSZCzUFsbBjv3jwuhNd/nxIP8AXpKBz+8sw3I6GgJojC4wCgGIgfQPqLeLquQ==",
|
"integrity": "sha512-Pd1s8l05QGtRgTrDPXi3BQXlyjGlOCdw4P8czNyJg5v7IyvqGuEhws2twRnFliiZFdZ1RwOzpchWfyv8E4LcCA==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"axios": "^0.21.0",
|
"axios": "^0.21.0",
|
||||||
"camelize-ts": "^1.0.8",
|
"camelize-ts": "^1.0.8",
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
"prepare": "husky install"
|
"prepare": "husky install"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"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/patternfly": "^4.144.5",
|
||||||
"@patternfly/react-code-editor": "^4.3.85",
|
"@patternfly/react-code-editor": "^4.3.85",
|
||||||
"@patternfly/react-core": "^4.162.3",
|
"@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 { ListComponent } from "./ListComponent";
|
||||||
import { RoleComponent } from "./RoleComponent";
|
import { RoleComponent } from "./RoleComponent";
|
||||||
import { ScriptComponent } from "./ScriptComponent";
|
import { ScriptComponent } from "./ScriptComponent";
|
||||||
|
import { ClientSelectComponent } from "./ClientSelectComponent";
|
||||||
|
|
||||||
export type ComponentProps = Omit<ConfigPropertyRepresentation, "type">;
|
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];
|
export type Components = typeof ComponentTypes[number];
|
||||||
|
|
||||||
|
@ -20,4 +28,5 @@ export const COMPONENTS: {
|
||||||
List: ListComponent,
|
List: ListComponent,
|
||||||
Role: RoleComponent,
|
Role: RoleComponent,
|
||||||
Script: ScriptComponent,
|
Script: ScriptComponent,
|
||||||
|
ClientList: ClientSelectComponent,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
|
@ -1,31 +1,27 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link, useHistory } from "react-router-dom";
|
import type { LocationDescriptorObject } from "history";
|
||||||
import {
|
import { Link } from "react-router-dom";
|
||||||
AlertVariant,
|
import { Dropdown, DropdownItem, DropdownToggle } from "@patternfly/react-core";
|
||||||
Dropdown,
|
|
||||||
DropdownItem,
|
|
||||||
DropdownToggle,
|
|
||||||
} from "@patternfly/react-core";
|
|
||||||
import { CaretDownIcon } from "@patternfly/react-icons";
|
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 ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
|
||||||
import type ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation";
|
import type ProtocolMapperRepresentation from "@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation";
|
||||||
import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
|
import type { ProtocolMapperTypeRepresentation } from "@keycloak/keycloak-admin-client/lib/defs/serverInfoRepesentation";
|
||||||
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
|
|
||||||
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
|
||||||
import { useAlerts } from "../../components/alert/Alerts";
|
|
||||||
import { AddMapperDialog } from "../add/MapperDialog";
|
import { AddMapperDialog } from "../add/MapperDialog";
|
||||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
|
||||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
|
||||||
import { toMapper } from "../routes/Mapper";
|
|
||||||
|
|
||||||
type MapperListProps = {
|
type MapperListProps = {
|
||||||
clientScope: ClientScopeRepresentation;
|
model: ClientScopeRepresentation | ClientRepresentation;
|
||||||
type: string;
|
onAdd: (
|
||||||
refresh: () => void;
|
mappers: ProtocolMapperTypeRepresentation | ProtocolMapperRepresentation[]
|
||||||
|
) => void;
|
||||||
|
onDelete: (mapper: ProtocolMapperRepresentation) => void;
|
||||||
|
detailLink: (id: string) => LocationDescriptorObject;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Row = ProtocolMapperRepresentation & {
|
type Row = ProtocolMapperRepresentation & {
|
||||||
|
@ -34,24 +30,23 @@ type Row = ProtocolMapperRepresentation & {
|
||||||
priority: number;
|
priority: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const MapperList = ({ clientScope, type, refresh }: MapperListProps) => {
|
export const MapperList = ({
|
||||||
|
model,
|
||||||
|
onAdd,
|
||||||
|
onDelete,
|
||||||
|
detailLink,
|
||||||
|
}: MapperListProps) => {
|
||||||
const { t } = useTranslation("client-scopes");
|
const { t } = useTranslation("client-scopes");
|
||||||
const adminClient = useAdminClient();
|
|
||||||
const { addAlert, addError } = useAlerts();
|
|
||||||
|
|
||||||
const history = useHistory();
|
|
||||||
const { realm } = useRealm();
|
|
||||||
|
|
||||||
const [mapperAction, setMapperAction] = useState(false);
|
const [mapperAction, setMapperAction] = useState(false);
|
||||||
const mapperList = clientScope.protocolMappers;
|
const mapperList = model.protocolMappers;
|
||||||
const mapperTypes =
|
const mapperTypes = useServerInfo().protocolMapperTypes![model.protocol!];
|
||||||
useServerInfo().protocolMapperTypes![clientScope.protocol!];
|
|
||||||
|
|
||||||
const [key, setKey] = useState(0);
|
const [key, setKey] = useState(0);
|
||||||
useEffect(() => setKey(new Date().getTime()), [mapperList]);
|
useEffect(() => setKey(key + 1), [mapperList]);
|
||||||
|
|
||||||
const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false);
|
const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false);
|
||||||
const [filter, setFilter] = useState(clientScope.protocolMappers);
|
const [filter, setFilter] = useState(model.protocolMappers);
|
||||||
const toggleAddMapperDialog = (buildIn: boolean) => {
|
const toggleAddMapperDialog = (buildIn: boolean) => {
|
||||||
if (buildIn) {
|
if (buildIn) {
|
||||||
setFilter(mapperList || []);
|
setFilter(mapperList || []);
|
||||||
|
@ -61,33 +56,6 @@ export const MapperList = ({ clientScope, type, refresh }: MapperListProps) => {
|
||||||
setAddMapperDialogOpen(!addMapperDialogOpen);
|
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 () =>
|
const loader = async () =>
|
||||||
Promise.resolve(
|
Promise.resolve(
|
||||||
(mapperList || [])
|
(mapperList || [])
|
||||||
|
@ -106,17 +74,15 @@ export const MapperList = ({ clientScope, type, refresh }: MapperListProps) => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const MapperLink = ({ id, name }: Row) => (
|
const MapperLink = ({ id, name }: Row) => (
|
||||||
<Link to={toMapper({ realm, id: clientScope.id!, type, mapperId: id! })}>
|
<Link to={detailLink(id!)}>{name}</Link>
|
||||||
{name}
|
|
||||||
</Link>
|
|
||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AddMapperDialog
|
<AddMapperDialog
|
||||||
protocol={clientScope.protocol!}
|
protocol={model.protocol!}
|
||||||
filter={filter}
|
filter={filter}
|
||||||
onConfirm={addMappers}
|
onConfirm={onAdd}
|
||||||
open={addMapperDialogOpen}
|
open={addMapperDialogOpen}
|
||||||
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
|
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
|
||||||
/>
|
/>
|
||||||
|
@ -159,22 +125,7 @@ export const MapperList = ({ clientScope, type, refresh }: MapperListProps) => {
|
||||||
actions={[
|
actions={[
|
||||||
{
|
{
|
||||||
title: t("common:delete"),
|
title: t("common:delete"),
|
||||||
onRowClick: async (mapper) => {
|
onRowClick: onDelete,
|
||||||
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;
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
columns={[
|
columns={[
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React, { useState } from "react";
|
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 { useTranslation } from "react-i18next";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
|
@ -25,11 +25,11 @@ import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
|
||||||
import { convertFormValuesToObject, convertToFormValues } from "../../util";
|
import { convertFormValuesToObject, convertToFormValues } from "../../util";
|
||||||
import { FormAccess } from "../../components/form-access/FormAccess";
|
import { FormAccess } from "../../components/form-access/FormAccess";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
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 { Components, COMPONENTS } from "../add/components/components";
|
||||||
|
import { toClientScope } from "../routes/ClientScope";
|
||||||
|
|
||||||
import "./mapping-details.css";
|
import "./mapping-details.css";
|
||||||
import { toClientScope } from "../routes/ClientScope";
|
|
||||||
|
|
||||||
export const MappingDetails = () => {
|
export const MappingDetails = () => {
|
||||||
const { t } = useTranslation("client-scopes");
|
const { t } = useTranslation("client-scopes");
|
||||||
|
@ -47,15 +47,29 @@ export const MappingDetails = () => {
|
||||||
const { realm } = useRealm();
|
const { realm } = useRealm();
|
||||||
const serverInfo = useServerInfo();
|
const serverInfo = useServerInfo();
|
||||||
const isGuid = /^[{]?[0-9a-fA-F]{8}-([0-9a-fA-F]{4}-){3}[0-9a-fA-F]{12}[}]?$/;
|
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(
|
useFetch(
|
||||||
async () => {
|
async () => {
|
||||||
|
let data: ProtocolMapperRepresentation | undefined;
|
||||||
if (isUpdating) {
|
if (isUpdating) {
|
||||||
const data = await adminClient.clientScopes.findProtocolMapper({
|
if (isOnClientScope) {
|
||||||
|
data = await adminClient.clientScopes.findProtocolMapper({
|
||||||
id,
|
id,
|
||||||
mapperId,
|
mapperId,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
data = await adminClient.clients.findProtocolMapperById({
|
||||||
|
id,
|
||||||
|
mapperId,
|
||||||
|
});
|
||||||
|
}
|
||||||
if (!data) {
|
if (!data) {
|
||||||
throw new Error(t("common:notFound"));
|
throw new Error(t("common:notFound"));
|
||||||
}
|
}
|
||||||
|
@ -74,12 +88,14 @@ export const MappingDetails = () => {
|
||||||
data,
|
data,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
const scope = await adminClient.clientScopes.findOne({ id });
|
const model = type
|
||||||
if (!scope) {
|
? await adminClient.clientScopes.findOne({ id })
|
||||||
|
: await adminClient.clients.findOne({ id });
|
||||||
|
if (!model) {
|
||||||
throw new Error(t("common:notFound"));
|
throw new Error(t("common:notFound"));
|
||||||
}
|
}
|
||||||
const protocolMappers =
|
const protocolMappers =
|
||||||
serverInfo.protocolMapperTypes![scope.protocol!];
|
serverInfo.protocolMapperTypes![model.protocol!];
|
||||||
const mapping = protocolMappers.find(
|
const mapping = protocolMappers.find(
|
||||||
(mapper) => mapper.id === mapperId
|
(mapper) => mapper.id === mapperId
|
||||||
);
|
);
|
||||||
|
@ -89,7 +105,7 @@ export const MappingDetails = () => {
|
||||||
return {
|
return {
|
||||||
mapping,
|
mapping,
|
||||||
config: {
|
config: {
|
||||||
protocol: scope.protocol,
|
protocol: model.protocol,
|
||||||
protocolMapper: mapperId,
|
protocolMapper: mapperId,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@ -117,12 +133,19 @@ export const MappingDetails = () => {
|
||||||
continueButtonVariant: ButtonVariant.danger,
|
continueButtonVariant: ButtonVariant.danger,
|
||||||
onConfirm: async () => {
|
onConfirm: async () => {
|
||||||
try {
|
try {
|
||||||
|
if (isOnClientScope) {
|
||||||
await adminClient.clientScopes.delProtocolMapper({
|
await adminClient.clientScopes.delProtocolMapper({
|
||||||
id,
|
id,
|
||||||
mapperId: mapperId,
|
mapperId,
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await adminClient.clients.delProtocolMapper({
|
||||||
|
id,
|
||||||
|
mapperId,
|
||||||
|
});
|
||||||
|
}
|
||||||
addAlert(t("common:mappingDeletedSuccess"), AlertVariant.success);
|
addAlert(t("common:mappingDeletedSuccess"), AlertVariant.success);
|
||||||
history.push(`/${realm}/client-scopes/${id}/mappers`);
|
history.push(toDetails());
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
addError("common:mappingDeletedError", error);
|
addError("common:mappingDeletedError", error);
|
||||||
}
|
}
|
||||||
|
@ -133,16 +156,21 @@ export const MappingDetails = () => {
|
||||||
const configAttributes = convertFormValuesToObject(formMapping.config);
|
const configAttributes = convertFormValuesToObject(formMapping.config);
|
||||||
const key = isUpdating ? "Updated" : "Created";
|
const key = isUpdating ? "Updated" : "Created";
|
||||||
try {
|
try {
|
||||||
|
const mapping = { ...formMapping, ...config, config: configAttributes };
|
||||||
if (isUpdating) {
|
if (isUpdating) {
|
||||||
await adminClient.clientScopes.updateProtocolMapper(
|
isOnClientScope
|
||||||
|
? await adminClient.clientScopes.updateProtocolMapper(
|
||||||
{ id, mapperId },
|
{ id, mapperId },
|
||||||
{ ...formMapping, config: configAttributes }
|
{ id: mapperId, ...mapping }
|
||||||
|
)
|
||||||
|
: await adminClient.clients.updateProtocolMapper(
|
||||||
|
{ id, mapperId },
|
||||||
|
{ id: mapperId, ...mapping }
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
await adminClient.clientScopes.addProtocolMapper(
|
isOnClientScope
|
||||||
{ id },
|
? await adminClient.clientScopes.addProtocolMapper({ id }, mapping)
|
||||||
{ ...formMapping, ...config, config: configAttributes }
|
: await adminClient.clients.addProtocolMapper({ id }, mapping);
|
||||||
);
|
|
||||||
}
|
}
|
||||||
addAlert(t(`common:mapping${key}Success`), AlertVariant.success);
|
addAlert(t(`common:mapping${key}Success`), AlertVariant.success);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -180,8 +208,6 @@ export const MappingDetails = () => {
|
||||||
role="manage-clients"
|
role="manage-clients"
|
||||||
className="keycloak__client-scope-mapping-details__form"
|
className="keycloak__client-scope-mapping-details__form"
|
||||||
>
|
>
|
||||||
{!mapperId.match(isGuid) && (
|
|
||||||
<>
|
|
||||||
<FormGroup label={t("common:mapperType")} fieldId="mapperType">
|
<FormGroup label={t("common:mapperType")} fieldId="mapperType">
|
||||||
<TextInput
|
<TextInput
|
||||||
type="text"
|
type="text"
|
||||||
|
@ -203,9 +229,7 @@ export const MappingDetails = () => {
|
||||||
fieldId="name"
|
fieldId="name"
|
||||||
isRequired
|
isRequired
|
||||||
validated={
|
validated={
|
||||||
errors.name
|
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
}
|
||||||
helperTextInvalid={t("common:required")}
|
helperTextInvalid={t("common:required")}
|
||||||
>
|
>
|
||||||
|
@ -214,15 +238,12 @@ export const MappingDetails = () => {
|
||||||
type="text"
|
type="text"
|
||||||
id="name"
|
id="name"
|
||||||
name="name"
|
name="name"
|
||||||
|
isReadOnly={isUpdating}
|
||||||
validated={
|
validated={
|
||||||
errors.name
|
errors.name ? ValidatedOptions.error : ValidatedOptions.default
|
||||||
? ValidatedOptions.error
|
|
||||||
: ValidatedOptions.default
|
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
{mapping?.properties.map((property) => {
|
{mapping?.properties.map((property) => {
|
||||||
const componentType = property.type!;
|
const componentType = property.type!;
|
||||||
|
@ -242,12 +263,7 @@ export const MappingDetails = () => {
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
component={(props) => (
|
component={(props) => <Link {...props} to={toDetails()} />}
|
||||||
<Link
|
|
||||||
{...props}
|
|
||||||
to={toClientScope({ realm, id, type, tab: "mappers" })}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
>
|
>
|
||||||
{t("common:cancel")}
|
{t("common:cancel")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -25,12 +25,15 @@ import {
|
||||||
Row,
|
Row,
|
||||||
} from "../../components/role-mapping/RoleMapping";
|
} from "../../components/role-mapping/RoleMapping";
|
||||||
import type { RoleMappingPayload } from "@keycloak/keycloak-admin-client/lib/defs/roleRepresentation";
|
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 {
|
import {
|
||||||
AllClientScopes,
|
AllClientScopes,
|
||||||
changeScope,
|
changeScope,
|
||||||
ClientScopeDefaultOptionalType,
|
ClientScopeDefaultOptionalType,
|
||||||
} from "../../components/client-scope/ClientScopeTypes";
|
} from "../../components/client-scope/ClientScopeTypes";
|
||||||
import { useRealm } from "../../context/realm-context/RealmContext";
|
import { useRealm } from "../../context/realm-context/RealmContext";
|
||||||
|
import { toMapper } from "../routes/Mapper";
|
||||||
|
|
||||||
export const ClientScopeForm = () => {
|
export const ClientScopeForm = () => {
|
||||||
const { t } = useTranslation("client-scopes");
|
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) {
|
if (id && !clientScope) {
|
||||||
return (
|
return (
|
||||||
<div className="pf-u-text-align-center">
|
<div className="pf-u-text-align-center">
|
||||||
|
@ -236,9 +280,12 @@ export const ClientScopeForm = () => {
|
||||||
title={<TabTitleText>{t("common:mappers")}</TabTitleText>}
|
title={<TabTitleText>{t("common:mappers")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<MapperList
|
<MapperList
|
||||||
clientScope={clientScope}
|
model={clientScope}
|
||||||
type={type}
|
onAdd={addMappers}
|
||||||
refresh={refresh}
|
onDelete={onDelete}
|
||||||
|
detailLink={(id) =>
|
||||||
|
toMapper({ realm, id: clientScope.id!, type, mapperId: id! })
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Tab>
|
</Tab>
|
||||||
<Tab
|
<Tab
|
||||||
|
|
|
@ -54,6 +54,10 @@ import { EvaluateScopes } from "./scopes/EvaluateScopes";
|
||||||
import { ServiceAccount } from "./service-account/ServiceAccount";
|
import { ServiceAccount } from "./service-account/ServiceAccount";
|
||||||
import { isRealmClient, getProtocolName } from "./utils";
|
import { isRealmClient, getProtocolName } from "./utils";
|
||||||
import { SamlKeys } from "./keys/SamlKeys";
|
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 = {
|
type ClientDetailHeaderProps = {
|
||||||
onChange: (value: boolean) => void;
|
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) {
|
if (!client) {
|
||||||
return (
|
return (
|
||||||
<div className="pf-u-text-align-center">
|
<div className="pf-u-text-align-center">
|
||||||
|
@ -367,6 +416,22 @@ export const ClientDetails = () => {
|
||||||
<Credentials clientId={clientId} save={() => save()} />
|
<Credentials clientId={clientId} save={() => save()} />
|
||||||
</Tab>
|
</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
|
<Tab
|
||||||
id="roles"
|
id="roles"
|
||||||
eventKey="roles"
|
eventKey="roles"
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
} from "@patternfly/react-core";
|
} from "@patternfly/react-core";
|
||||||
import { cellWidth, IRowData, TableText } from "@patternfly/react-table";
|
import { cellWidth, IRowData, TableText } from "@patternfly/react-table";
|
||||||
import type ClientRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientRepresentation";
|
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 React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { Link } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
|
@ -44,13 +45,13 @@ export const ClientsSection = () => {
|
||||||
const [selectedClient, setSelectedClient] = useState<ClientRepresentation>();
|
const [selectedClient, setSelectedClient] = useState<ClientRepresentation>();
|
||||||
|
|
||||||
const loader = async (first?: number, max?: number, search?: string) => {
|
const loader = async (first?: number, max?: number, search?: string) => {
|
||||||
const params: { [name: string]: string | number } = {
|
const params: ClientQuery = {
|
||||||
first: first!,
|
first: first!,
|
||||||
max: max!,
|
max: max!,
|
||||||
};
|
};
|
||||||
if (search) {
|
if (search) {
|
||||||
params.clientId = search;
|
params.clientId = search;
|
||||||
params.search = "true";
|
params.search = true;
|
||||||
}
|
}
|
||||||
return await adminClient.clients.find({ ...params });
|
return await adminClient.clients.find({ ...params });
|
||||||
};
|
};
|
||||||
|
|
|
@ -294,5 +294,6 @@ export default {
|
||||||
expires: "Expires in",
|
expires: "Expires in",
|
||||||
never: "Never expires",
|
never: "Never expires",
|
||||||
},
|
},
|
||||||
|
mappers: "Mappers",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { ClientRoute } from "./routes/Client";
|
||||||
import { ClientsRoute } from "./routes/Clients";
|
import { ClientsRoute } from "./routes/Clients";
|
||||||
import { CreateInitialAccessTokenRoute } from "./routes/CreateInitialAccessToken";
|
import { CreateInitialAccessTokenRoute } from "./routes/CreateInitialAccessToken";
|
||||||
import { ImportClientRoute } from "./routes/ImportClient";
|
import { ImportClientRoute } from "./routes/ImportClient";
|
||||||
|
import { MapperRoute } from "./routes/Mapper";
|
||||||
|
|
||||||
const routes: RouteDef[] = [
|
const routes: RouteDef[] = [
|
||||||
AddClientRoute,
|
AddClientRoute,
|
||||||
|
@ -11,6 +12,7 @@ const routes: RouteDef[] = [
|
||||||
ClientsRoute,
|
ClientsRoute,
|
||||||
CreateInitialAccessTokenRoute,
|
CreateInitialAccessTokenRoute,
|
||||||
ClientRoute,
|
ClientRoute,
|
||||||
|
MapperRoute,
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|
|
@ -3,7 +3,12 @@ import { generatePath } from "react-router-dom";
|
||||||
import type { RouteDef } from "../../route-config";
|
import type { RouteDef } from "../../route-config";
|
||||||
import { ClientDetails } from "../ClientDetails";
|
import { ClientDetails } from "../ClientDetails";
|
||||||
|
|
||||||
export type ClientTab = "settings" | "roles" | "clientScopes" | "advanced";
|
export type ClientTab =
|
||||||
|
| "settings"
|
||||||
|
| "roles"
|
||||||
|
| "clientScopes"
|
||||||
|
| "advanced"
|
||||||
|
| "mappers";
|
||||||
|
|
||||||
export type ClientParams = {
|
export type ClientParams = {
|
||||||
realm: string;
|
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