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:
Erik Jan de Wit 2021-10-20 16:26:05 +02:00 committed by GitHub
parent 82c04d2e4a
commit acd8921b20
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 396 additions and 162 deletions

View file

@ -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
View file

@ -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",

View file

@ -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",

View 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>
);
};

View file

@ -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;

View file

@ -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={[

View file

@ -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>

View file

@ -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

View file

@ -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"

View file

@ -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 });
};

View file

@ -294,5 +294,6 @@ export default {
expires: "Expires in",
never: "Never expires",
},
mappers: "Mappers",
},
};

View file

@ -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;

View file

@ -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;

View 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),
});