changed to use the keycloak datatable (#432)

* changed to use the keycloak datatable

so it will benifid from a general way tables work in the admin console

* null check

* changed to use any active tab

seems sometimes the tab doesn't get set properly
so instead of "mappers" it's still "settings"
this change makes it work with both
This commit is contained in:
Erik Jan de Wit 2021-03-19 13:43:32 +01:00 committed by GitHub
parent b56788d942
commit dfc4beced4
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 345 additions and 465 deletions

View file

@ -1,4 +1,4 @@
import React, { useState } from "react";
import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next";
import { Link, useHistory, useRouteMatch } from "react-router-dom";
import {
@ -8,12 +8,6 @@ import {
DropdownItem,
DropdownToggle,
} from "@patternfly/react-core";
import {
Table,
TableBody,
TableHeader,
TableVariant,
} from "@patternfly/react-table";
import { CaretDownIcon } from "@patternfly/react-icons";
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
@ -21,19 +15,18 @@ import ProtocolMapperRepresentation from "keycloak-admin/lib/defs/protocolMapper
import { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
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";
type MapperListProps = {
clientScope: ClientScopeRepresentation;
refresh: () => void;
};
type Row = {
name: JSX.Element;
type Row = ProtocolMapperRepresentation & {
category: string;
type: string;
priority: number;
@ -46,15 +39,15 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
const history = useHistory();
const { url } = useRouteMatch();
const [filteredData, setFilteredData] = useState<
{ mapper: ProtocolMapperRepresentation; cells: Row }[]
>();
const [mapperAction, setMapperAction] = useState(false);
const mapperList = clientScope.protocolMappers!;
const mapperTypes = useServerInfo().protocolMapperTypes![
clientScope.protocol!
];
const [key, setKey] = useState(0);
useEffect(() => setKey(new Date().getTime()), [mapperList]);
const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false);
const [filter, setFilter] = useState(clientScope.protocolMappers);
const toggleAddMapperDialog = (buildIn: boolean) => {
@ -86,98 +79,31 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
}
};
if (!mapperList) {
return (
<>
<AddMapperDialog
protocol={clientScope.protocol!}
filter={filter}
onConfirm={addMappers}
open={addMapperDialogOpen}
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
/>
<ListEmptyState
message={t("emptyMappers")}
instructions={t("emptyMappersInstructions")}
primaryActionText={t("emptyPrimaryAction")}
onPrimaryAction={() => toggleAddMapperDialog(true)}
secondaryActions={[
{
text: t("emptySecondaryAction"),
onClick: () => toggleAddMapperDialog(false),
type: ButtonVariant.secondary,
},
]}
/>
</>
const loader = async () =>
Promise.resolve(
(mapperList || [])
.map((mapper) => {
const mapperType = mapperTypes.filter(
(type) => type.id === mapper.protocolMapper
)[0];
return {
...mapper,
category: mapperType.category,
type: mapperType.name,
priority: mapperType.priority,
} as Row;
})
.sort((a, b) => a.priority - b.priority)
);
}
const data = mapperList
.map((mapper) => {
const mapperType = mapperTypes.filter(
(type) => type.id === mapper.protocolMapper
)[0];
return {
mapper,
cells: {
name: (
<>
<Link to={`${url}/${mapper.id}`}>{mapper.name}</Link>
</>
),
category: mapperType.category,
type: mapperType.name,
priority: mapperType.priority,
} as Row,
};
})
.sort((a, b) => a.cells.priority - b.cells.priority);
const filterData = (search: string) => {
setFilteredData(
data.filter((column) =>
column.mapper.name!.toLowerCase().includes(search.toLowerCase())
)
);
};
const MapperLink = (mapper: Row) => (
<>
<Link to={`${url}/${mapper.id}`}>{mapper.name}</Link>
</>
);
return (
<TableToolbar
inputGroupName="clientsScopeToolbarTextInput"
inputGroupPlaceholder={t("mappersSearchFor")}
inputGroupOnChange={filterData}
toolbarItem={
<Dropdown
onSelect={() => setMapperAction(false)}
toggle={
<DropdownToggle
isPrimary
id="mapperAction"
onToggle={() => setMapperAction(!mapperAction)}
toggleIndicator={CaretDownIcon}
>
{t("addMapper")}
</DropdownToggle>
}
isOpen={mapperAction}
dropdownItems={[
<DropdownItem
key="predefined"
onClick={() => toggleAddMapperDialog(true)}
>
{t("fromPredefinedMapper")}
</DropdownItem>,
<DropdownItem
key="byConfiguration"
onClick={() => toggleAddMapperDialog(false)}
>
{t("byConfiguration")}
</DropdownItem>,
]}
/>
}
>
<>
<AddMapperDialog
protocol={clientScope.protocol!}
filter={filter}
@ -185,42 +111,92 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
open={addMapperDialogOpen}
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
/>
<Table
variant={TableVariant.compact}
cells={[
t("common:name"),
t("common:category"),
t("common:type"),
t("common:priority"),
]}
rows={(filteredData || data).map((cell) => {
return { cells: Object.values(cell.cells), mapper: cell.mapper };
})}
aria-label={t("clientScopeList")}
<KeycloakDataTable
key={key}
loader={loader}
ariaLabelKey="client-scopes:clientScopeList"
searchPlaceholderKey="client-scopes:mappersSearchFor"
toolbarItem={
<Dropdown
onSelect={() => setMapperAction(false)}
toggle={
<DropdownToggle
isPrimary
id="mapperAction"
onToggle={() => setMapperAction(!mapperAction)}
toggleIndicator={CaretDownIcon}
>
{t("addMapper")}
</DropdownToggle>
}
isOpen={mapperAction}
dropdownItems={[
<DropdownItem
key="predefined"
onClick={() => toggleAddMapperDialog(true)}
>
{t("fromPredefinedMapper")}
</DropdownItem>,
<DropdownItem
key="byConfiguration"
onClick={() => toggleAddMapperDialog(false)}
>
{t("byConfiguration")}
</DropdownItem>,
]}
/>
}
actions={[
{
title: t("common:delete"),
onClick: async (_, rowId) => {
onRowClick: async (mapper) => {
try {
await adminClient.clientScopes.delProtocolMapper({
id: clientScope.id!,
mapperId: data[rowId].mapper.id!,
mapperId: mapper.id!,
});
refresh();
addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
refresh();
} catch (error) {
addAlert(
t("mappingDeletedError", { error }),
AlertVariant.danger
);
}
return true;
},
},
]}
>
<TableHeader />
<TableBody />
</Table>
</TableToolbar>
columns={[
{
name: "name",
cellRenderer: MapperLink,
},
{ name: "category" },
{
name: "type",
},
{
name: "priority",
},
]}
emptyState={
<ListEmptyState
message={t("emptyMappers")}
instructions={t("emptyMappersInstructions")}
primaryActionText={t("emptyPrimaryAction")}
onPrimaryAction={() => toggleAddMapperDialog(true)}
secondaryActions={[
{
text: t("emptySecondaryAction"),
onClick: () => toggleAddMapperDialog(false),
type: ButtonVariant.secondary,
},
]}
/>
}
/>
</>
);
};

View file

@ -1,14 +1,5 @@
import React, { useState, useEffect } from "react";
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import {
IFormatter,
IFormatterValueType,
Table,
TableBody,
TableHeader,
TableVariant,
} from "@patternfly/react-table";
import {
AlertVariant,
Button,
@ -17,19 +8,14 @@ import {
DropdownToggle,
KebabToggle,
Select,
Spinner,
ToolbarItem,
} from "@patternfly/react-core";
import { FilterIcon } from "@patternfly/react-icons";
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import KeycloakAdminClient from "keycloak-admin";
import {
useAdminClient,
asyncStateFetch,
} from "../../context/auth/AdminClient";
import { useAdminClient } from "../../context/auth/AdminClient";
import { toUpperCase } from "../../util";
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { AddScopeDialog } from "./AddScopeDialog";
import {
@ -38,12 +24,18 @@ import {
ClientScope,
} from "./ClientScopeTypes";
import { useAlerts } from "../../components/alert/Alerts";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
export type ClientScopesProps = {
clientId: string;
protocol: string;
};
type Row = ClientScopeRepresentation & {
type: ClientScopeType;
description: string;
};
const castAdminClient = (adminClient: KeycloakAdminClient) =>
(adminClient.clients as unknown) as {
[index: string]: Function;
@ -124,7 +116,6 @@ type TableRow = {
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const { t } = useTranslation("clients");
const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts();
const [searchToggle, setSearchToggle] = useState(false);
@ -133,100 +124,73 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const [addDialogOpen, setAddDialogOpen] = useState(false);
const [kebabOpen, setKebabOpen] = useState(false);
const [rows, setRows] = useState<TableRow[]>();
const [rest, setRest] = useState<ClientScopeRepresentation[]>();
const [selectedRows, setSelectedRows] = useState<Row[]>([]);
const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime());
useEffect(() => {
return asyncStateFetch(
async () => {
const defaultClientScopes = await adminClient.clients.listDefaultClientScopes(
{ id: clientId }
);
const optionalClientScopes = await adminClient.clients.listOptionalClientScopes(
{ id: clientId }
);
const clientScopes = await adminClient.clientScopes.find();
const find = (id: string) =>
clientScopes.find((clientScope) => id === clientScope.id)!;
const optional = optionalClientScopes.map((c) => {
const scope = find(c.id!);
return {
selected: false,
clientScope: c,
type: ClientScope.optional,
cells: [c.name, c.id, scope.description],
};
});
const defaultScopes = defaultClientScopes.map((c) => {
const scope = find(c.id!);
return {
selected: false,
clientScope: c,
type: ClientScope.default,
cells: [c.name, c.id, scope.description],
};
});
const rows = [...optional, ...defaultScopes];
const names = rows.map((row) => row.cells[0]);
const rest = clientScopes
.filter((scope) => !names.includes(scope.name))
.filter((scope) => scope.protocol === protocol);
return { rows, rest };
},
({ rows, rest }) => {
setRows(rows);
setRest(rest);
},
handleError
const loader = async () => {
const defaultClientScopes = await adminClient.clients.listDefaultClientScopes(
{ id: clientId }
);
}, [key]);
const optionalClientScopes = await adminClient.clients.listOptionalClientScopes(
{ id: clientId }
);
const clientScopes = await adminClient.clientScopes.find();
const dropdown = (): IFormatter => (data?: IFormatterValueType) => {
if (!data) {
return <></>;
}
const row = rows?.find((row) => row.clientScope.id === data.toString())!;
return (
const find = (id: string) =>
clientScopes.find((clientScope) => id === clientScope.id)!;
const optional = optionalClientScopes.map((c) => {
const scope = find(c.id!);
return {
...c,
type: ClientScope.optional,
description: scope.description,
} as Row;
});
const defaultScopes = defaultClientScopes.map((c) => {
const scope = find(c.id!);
return {
...c,
type: ClientScope.default,
description: scope.description,
} as Row;
});
const rows = [...optional, ...defaultScopes];
const names = rows.map((row) => row.name);
setRest(
clientScopes
.filter((scope) => !names.includes(scope.name))
.filter((scope) => scope.protocol === protocol)
);
return rows;
};
const TypeSelector = (scope: Row) => (
<>
<CellDropdown
clientScope={row.clientScope}
type={row.type}
clientScope={scope}
type={scope.type}
onSelect={async (value) => {
try {
await changeScope(
adminClient,
clientId,
row.clientScope,
row.type,
value
);
await changeScope(adminClient, clientId, scope, scope.type, value);
addAlert(t("clientScopeSuccess"), AlertVariant.success);
await refresh();
refresh();
} catch (error) {
addAlert(t("clientScopeError", { error }), AlertVariant.danger);
}
}}
/>
);
};
const filterData = () => {};
</>
);
return (
<>
{!rows && (
<div className="pf-u-text-align-center">
<Spinner />
</div>
)}
{rest && (
<AddScopeDialog
clientScopes={rest}
@ -254,202 +218,154 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
/>
)}
{rows && rows.length > 0 && (
<TableToolbar
searchTypeComponent={
<Dropdown
toggle={
<DropdownToggle
id="toggle-id"
onToggle={() => setSearchToggle(!searchToggle)}
>
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
</DropdownToggle>
}
aria-label="Select Input"
isOpen={searchToggle}
dropdownItems={[
<DropdownItem
key="client"
onClick={() => {
setSearchType("client");
setSearchToggle(false);
}}
>
{t("clientScopeSearch.client")}
</DropdownItem>,
<DropdownItem
key="assigned"
onClick={() => {
setSearchType("assigned");
setSearchToggle(false);
}}
>
{t("clientScopeSearch.assigned")}
</DropdownItem>,
]}
/>
}
inputGroupName="clientsScopeToolbarTextInput"
inputGroupPlaceholder={t("searchByName")}
inputGroupOnChange={filterData}
toolbarItem={
<>
<ToolbarItem>
<Button onClick={() => setAddDialogOpen(true)}>
{t("addClientScope")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Select
id="add-dropdown"
key="add-dropdown"
isOpen={addToggle}
selections={[]}
placeholderText={t("changeTypeTo")}
onToggle={() => setAddToggle(!addToggle)}
onSelect={async (_, value) => {
try {
await Promise.all(
rows.map((row) => {
if (row.selected) {
return changeScope(
adminClient,
clientId,
row.clientScope,
row.type,
value as ClientScope
);
}
return Promise.resolve();
})
);
setAddToggle(false);
await refresh();
addAlert(t("clientScopeSuccess"), AlertVariant.success);
} catch (error) {
addAlert(
t("clientScopeError", { error }),
AlertVariant.danger
);
}
}}
>
{clientScopeTypesSelectOptions(t)}
</Select>
</ToolbarItem>
<ToolbarItem>
<Dropdown
onSelect={() => {}}
toggle={
<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
}
isOpen={kebabOpen}
isPlain
dropdownItems={[
<DropdownItem
key="deleteAll"
isDisabled={
rows.filter((row) => row.selected).length === 0
}
onClick={async () => {
try {
await Promise.all(
rows.map(async (row) => {
if (row.selected) {
await removeScope(
adminClient,
clientId,
row.clientScope,
row.type
);
}
})
);
setKebabOpen(false);
addAlert(
t("clientScopeRemoveSuccess"),
AlertVariant.success
);
refresh();
} catch (error) {
addAlert(
t("clientScopeRemoveError", { error }),
AlertVariant.danger
);
}
}}
>
{t("common:remove")}
</DropdownItem>,
]}
/>
</ToolbarItem>
</>
}
>
<Table
onSelect={(_, isSelected, rowIndex) => {
if (rowIndex === -1) {
setRows(
rows.map((row) => {
row.selected = isSelected;
return row;
})
);
} else {
rows[rowIndex].selected = isSelected;
setRows([...rows]);
}
}}
variant={TableVariant.compact}
cells={[
t("common:name"),
{ title: t("assignedType"), cellFormatters: [dropdown()] },
t("common:description"),
<KeycloakDataTable
key={key}
loader={loader}
ariaLabelKey="clients:clientScopeList"
searchPlaceholderKey="clients:searchByName"
onSelect={(rows) => setSelectedRows([...rows])}
searchTypeComponent={
<Dropdown
toggle={
<DropdownToggle
id="toggle-id"
onToggle={() => setSearchToggle(!searchToggle)}
>
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
</DropdownToggle>
}
aria-label="Select Input"
isOpen={searchToggle}
dropdownItems={[
<DropdownItem
key="client"
onClick={() => {
setSearchType("client");
setSearchToggle(false);
}}
>
{t("clientScopeSearch.client")}
</DropdownItem>,
<DropdownItem
key="assigned"
onClick={() => {
setSearchType("assigned");
setSearchToggle(false);
}}
>
{t("clientScopeSearch.assigned")}
</DropdownItem>,
]}
rows={rows}
actions={[
{
title: t("common:remove"),
onClick: async (_, rowId) => {
/>
}
toolbarItem={
<>
<ToolbarItem>
<Button onClick={() => setAddDialogOpen(true)}>
{t("addClientScope")}
</Button>
</ToolbarItem>
<ToolbarItem>
<Select
id="add-dropdown"
key="add-dropdown"
isOpen={addToggle}
selections={[]}
placeholderText={t("changeTypeTo")}
onToggle={() => setAddToggle(!addToggle)}
onSelect={async (_, value) => {
try {
await removeScope(
adminClient,
clientId,
rows[rowId].clientScope,
rows[rowId].type
);
addAlert(
t("clientScopeRemoveSuccess"),
AlertVariant.success
await Promise.all(
selectedRows.map((row) => {
return changeScope(
adminClient,
clientId,
{ ...row },
row.type,
value as ClientScope
);
})
);
setAddToggle(false);
refresh();
addAlert(t("clientScopeSuccess"), AlertVariant.success);
} catch (error) {
addAlert(
t("clientScopeRemoveError", { error }),
t("clientScopeError", { error }),
AlertVariant.danger
);
}
},
},
]}
aria-label={t("clientScopeList")}
>
<TableHeader />
<TableBody />
</Table>
</TableToolbar>
)}
{rows && rows.length === 0 && (
<ListEmptyState
message={t("clients:emptyClientScopes")}
instructions={t("clients:emptyClientScopesInstructions")}
primaryActionText={t("clients:emptyClientScopesPrimaryAction")}
onPrimaryAction={() => setAddDialogOpen(true)}
/>
)}
}}
>
{clientScopeTypesSelectOptions(t)}
</Select>
</ToolbarItem>
<ToolbarItem>
<Dropdown
onSelect={() => {}}
toggle={
<KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
}
isOpen={kebabOpen}
isPlain
dropdownItems={[
<DropdownItem
key="deleteAll"
isDisabled={selectedRows.length === 0}
onClick={async () => {
try {
await Promise.all(
selectedRows.map(async (row) => {
await removeScope(
adminClient,
clientId,
{ ...row },
row.type
);
})
);
setKebabOpen(false);
addAlert(
t("clientScopeRemoveSuccess"),
AlertVariant.success
);
refresh();
} catch (error) {
addAlert(
t("clientScopeRemoveError", { error }),
AlertVariant.danger
);
}
}}
>
{t("common:remove")}
</DropdownItem>,
]}
/>
</ToolbarItem>
</>
}
columns={[
{
name: "name",
},
{
name: "type",
displayKey: "clients:assignedType",
cellRenderer: TypeSelector,
},
{ name: "description" },
]}
emptyState={
<ListEmptyState
message={t("clients:emptyClientScopes")}
instructions={t("clients:emptyClientScopesInstructions")}
primaryActionText={t("clients:emptyClientScopesPrimaryAction")}
onPrimaryAction={() => setAddDialogOpen(true)}
/>
}
/>
</>
);
};

View file

@ -1,18 +1,12 @@
import React, { Fragment, useContext, useState } from "react";
import React, { useContext, useState } from "react";
import { useTranslation } from "react-i18next";
import {
Table,
TableBody,
TableHeader,
TableVariant,
} from "@patternfly/react-table";
import { Badge, Button, Checkbox, ToolbarItem } from "@patternfly/react-core";
import { useAdminClient } from "../../context/auth/AdminClient";
import { DataLoader } from "../../components/data-loader/DataLoader";
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
import { RealmContext } from "../../context/realm-context/RealmContext";
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
import { useAdminClient } from "../../context/auth/AdminClient";
import { RealmContext } from "../../context/realm-context/RealmContext";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import { emptyFormatter } from "../../util";
import "./service-account.css";
@ -21,6 +15,11 @@ type ServiceAccountProps = {
clientId: string;
};
type Row = {
client: ClientRepresentation;
role: CompositeRole;
};
type CompositeRole = RoleRepresentation & {
parent: RoleRepresentation;
};
@ -66,6 +65,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
};
const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
console.log(clientRolesFlat);
const addInherentData = await (async () =>
Promise.all(
@ -92,35 +92,34 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
] as CompositeRole[])
.sort((r1, r2) => r1.name!.localeCompare(r2.name!))
.map((role) => {
const client = findClient(role);
return {
cells: [
<Fragment key={role.id}>
{client && (
<Badge
key={client.id}
isRead
className="keycloak-admin--service-account__client-name"
>
{client.clientId}
</Badge>
)}
{role.name}
</Fragment>,
role.parent ? role.parent.name : "",
role.description,
],
};
client: findClient(role),
role,
} as Row;
});
};
const filterData = () => {};
const RoleLink = ({ role, client }: Row) => (
<>
{client && (
<Badge
key={client.id}
isRead
className="keycloak-admin--service-account__client-name"
>
{client.clientId}
</Badge>
)}
{role.name}
</>
);
return (
<TableToolbar
inputGroupName="clientsServiceAccountRoleToolbarTextInput"
inputGroupPlaceholder={t("searchByName")}
inputGroupOnChange={filterData}
<KeycloakDataTable
loader={loader}
onSelect={() => {}}
searchPlaceholderKey="clients:searchByName"
ariaLabelKey="clients:clientScopeList"
toolbarItem={
<>
<ToolbarItem>
@ -136,34 +135,23 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
</ToolbarItem>
</>
}
>
<DataLoader loader={loader} deps={[clientId]}>
{(clientRoles) => (
<>
{hide ? "" : " "}
<Table
onSelect={() => {}}
variant={TableVariant.compact}
cells={[
t("roles:roleName"),
{
title: t("inherentFrom"),
cellFormatters: [emptyFormatter()],
},
{
title: t("common:description"),
cellFormatters: [emptyFormatter()],
},
]}
rows={clientRoles}
aria-label="roleList"
>
<TableHeader />
<TableBody />
</Table>
</>
)}
</DataLoader>
</TableToolbar>
columns={[
{
name: "role.name",
displayKey: t("name"),
cellRenderer: RoleLink,
},
{
name: "role.parent.name",
displayKey: t("inherentFrom"),
cellFormatters: [emptyFormatter()],
},
{
name: "role.description",
displayKey: t("description"),
cellFormatters: [emptyFormatter()],
},
]}
/>
);
};

View file

@ -92,13 +92,13 @@ export const routes: RoutesFn = (t: TFunction) => [
access: "manage-clients",
},
{
path: "/:realm/client-scopes/:id/mappers/oidc-role-name-mapper",
path: "/:realm/client-scopes/:id/:tab/oidc-role-name-mapper",
component: RoleMappingForm,
breadcrumb: t("client-scopes:mappingDetails"),
access: "view-clients",
},
{
path: "/:realm/client-scopes/:id/mappers/:mapperId",
path: "/:realm/client-scopes/:id/:tab/:mapperId",
component: MappingDetails,
breadcrumb: t("client-scopes:mappingDetails"),
access: "view-clients",