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 { useTranslation } from "react-i18next";
import { Link, useHistory, useRouteMatch } from "react-router-dom"; import { Link, useHistory, useRouteMatch } from "react-router-dom";
import { import {
@ -8,12 +8,6 @@ import {
DropdownItem, DropdownItem,
DropdownToggle, DropdownToggle,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import {
Table,
TableBody,
TableHeader,
TableVariant,
} from "@patternfly/react-table";
import { CaretDownIcon } from "@patternfly/react-icons"; import { CaretDownIcon } from "@patternfly/react-icons";
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; 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 { ProtocolMapperTypeRepresentation } from "keycloak-admin/lib/defs/serverInfoRepesentation";
import { useServerInfo } from "../../context/server-info/ServerInfoProvider"; import { useServerInfo } from "../../context/server-info/ServerInfoProvider";
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { AddMapperDialog } from "../add/MapperDialog"; import { AddMapperDialog } from "../add/MapperDialog";
import { useAdminClient } from "../../context/auth/AdminClient"; import { useAdminClient } from "../../context/auth/AdminClient";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
type MapperListProps = { type MapperListProps = {
clientScope: ClientScopeRepresentation; clientScope: ClientScopeRepresentation;
refresh: () => void; refresh: () => void;
}; };
type Row = { type Row = ProtocolMapperRepresentation & {
name: JSX.Element;
category: string; category: string;
type: string; type: string;
priority: number; priority: number;
@ -46,15 +39,15 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
const history = useHistory(); const history = useHistory();
const { url } = useRouteMatch(); const { url } = useRouteMatch();
const [filteredData, setFilteredData] = useState<
{ mapper: ProtocolMapperRepresentation; cells: Row }[]
>();
const [mapperAction, setMapperAction] = useState(false); const [mapperAction, setMapperAction] = useState(false);
const mapperList = clientScope.protocolMappers!; const mapperList = clientScope.protocolMappers!;
const mapperTypes = useServerInfo().protocolMapperTypes![ const mapperTypes = useServerInfo().protocolMapperTypes![
clientScope.protocol! clientScope.protocol!
]; ];
const [key, setKey] = useState(0);
useEffect(() => setKey(new Date().getTime()), [mapperList]);
const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false); const [addMapperDialogOpen, setAddMapperDialogOpen] = useState(false);
const [filter, setFilter] = useState(clientScope.protocolMappers); const [filter, setFilter] = useState(clientScope.protocolMappers);
const toggleAddMapperDialog = (buildIn: boolean) => { const toggleAddMapperDialog = (buildIn: boolean) => {
@ -86,98 +79,31 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
} }
}; };
if (!mapperList) { const loader = async () =>
return ( Promise.resolve(
<> (mapperList || [])
<AddMapperDialog .map((mapper) => {
protocol={clientScope.protocol!} const mapperType = mapperTypes.filter(
filter={filter} (type) => type.id === mapper.protocolMapper
onConfirm={addMappers} )[0];
open={addMapperDialogOpen} return {
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)} ...mapper,
/> category: mapperType.category,
<ListEmptyState type: mapperType.name,
message={t("emptyMappers")} priority: mapperType.priority,
instructions={t("emptyMappersInstructions")} } as Row;
primaryActionText={t("emptyPrimaryAction")} })
onPrimaryAction={() => toggleAddMapperDialog(true)} .sort((a, b) => a.priority - b.priority)
secondaryActions={[
{
text: t("emptySecondaryAction"),
onClick: () => toggleAddMapperDialog(false),
type: ButtonVariant.secondary,
},
]}
/>
</>
); );
}
const data = mapperList const MapperLink = (mapper: Row) => (
.map((mapper) => { <>
const mapperType = mapperTypes.filter( <Link to={`${url}/${mapper.id}`}>{mapper.name}</Link>
(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())
)
);
};
return ( 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 <AddMapperDialog
protocol={clientScope.protocol!} protocol={clientScope.protocol!}
filter={filter} filter={filter}
@ -185,42 +111,92 @@ export const MapperList = ({ clientScope, refresh }: MapperListProps) => {
open={addMapperDialogOpen} open={addMapperDialogOpen}
toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)} toggleDialog={() => setAddMapperDialogOpen(!addMapperDialogOpen)}
/> />
<Table
variant={TableVariant.compact} <KeycloakDataTable
cells={[ key={key}
t("common:name"), loader={loader}
t("common:category"), ariaLabelKey="client-scopes:clientScopeList"
t("common:type"), searchPlaceholderKey="client-scopes:mappersSearchFor"
t("common:priority"), toolbarItem={
]} <Dropdown
rows={(filteredData || data).map((cell) => { onSelect={() => setMapperAction(false)}
return { cells: Object.values(cell.cells), mapper: cell.mapper }; toggle={
})} <DropdownToggle
aria-label={t("clientScopeList")} 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={[ actions={[
{ {
title: t("common:delete"), title: t("common:delete"),
onClick: async (_, rowId) => { onRowClick: async (mapper) => {
try { try {
await adminClient.clientScopes.delProtocolMapper({ await adminClient.clientScopes.delProtocolMapper({
id: clientScope.id!, id: clientScope.id!,
mapperId: data[rowId].mapper.id!, mapperId: mapper.id!,
}); });
refresh();
addAlert(t("mappingDeletedSuccess"), AlertVariant.success); addAlert(t("mappingDeletedSuccess"), AlertVariant.success);
refresh();
} catch (error) { } catch (error) {
addAlert( addAlert(
t("mappingDeletedError", { error }), t("mappingDeletedError", { error }),
AlertVariant.danger AlertVariant.danger
); );
} }
return true;
}, },
}, },
]} ]}
> columns={[
<TableHeader /> {
<TableBody /> name: "name",
</Table> cellRenderer: MapperLink,
</TableToolbar> },
{ 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 { useTranslation } from "react-i18next";
import { useErrorHandler } from "react-error-boundary";
import {
IFormatter,
IFormatterValueType,
Table,
TableBody,
TableHeader,
TableVariant,
} from "@patternfly/react-table";
import { import {
AlertVariant, AlertVariant,
Button, Button,
@ -17,19 +8,14 @@ import {
DropdownToggle, DropdownToggle,
KebabToggle, KebabToggle,
Select, Select,
Spinner,
ToolbarItem, ToolbarItem,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { FilterIcon } from "@patternfly/react-icons"; import { FilterIcon } from "@patternfly/react-icons";
import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation"; import ClientScopeRepresentation from "keycloak-admin/lib/defs/clientScopeRepresentation";
import KeycloakAdminClient from "keycloak-admin"; import KeycloakAdminClient from "keycloak-admin";
import { import { useAdminClient } from "../../context/auth/AdminClient";
useAdminClient,
asyncStateFetch,
} from "../../context/auth/AdminClient";
import { toUpperCase } from "../../util"; import { toUpperCase } from "../../util";
import { TableToolbar } from "../../components/table-toolbar/TableToolbar";
import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState"; import { ListEmptyState } from "../../components/list-empty-state/ListEmptyState";
import { AddScopeDialog } from "./AddScopeDialog"; import { AddScopeDialog } from "./AddScopeDialog";
import { import {
@ -38,12 +24,18 @@ import {
ClientScope, ClientScope,
} from "./ClientScopeTypes"; } from "./ClientScopeTypes";
import { useAlerts } from "../../components/alert/Alerts"; import { useAlerts } from "../../components/alert/Alerts";
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
export type ClientScopesProps = { export type ClientScopesProps = {
clientId: string; clientId: string;
protocol: string; protocol: string;
}; };
type Row = ClientScopeRepresentation & {
type: ClientScopeType;
description: string;
};
const castAdminClient = (adminClient: KeycloakAdminClient) => const castAdminClient = (adminClient: KeycloakAdminClient) =>
(adminClient.clients as unknown) as { (adminClient.clients as unknown) as {
[index: string]: Function; [index: string]: Function;
@ -124,7 +116,6 @@ type TableRow = {
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const handleError = useErrorHandler();
const { addAlert } = useAlerts(); const { addAlert } = useAlerts();
const [searchToggle, setSearchToggle] = useState(false); const [searchToggle, setSearchToggle] = useState(false);
@ -133,100 +124,73 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
const [addDialogOpen, setAddDialogOpen] = useState(false); const [addDialogOpen, setAddDialogOpen] = useState(false);
const [kebabOpen, setKebabOpen] = useState(false); const [kebabOpen, setKebabOpen] = useState(false);
const [rows, setRows] = useState<TableRow[]>();
const [rest, setRest] = useState<ClientScopeRepresentation[]>(); const [rest, setRest] = useState<ClientScopeRepresentation[]>();
const [selectedRows, setSelectedRows] = useState<Row[]>([]);
const [key, setKey] = useState(0); const [key, setKey] = useState(0);
const refresh = () => setKey(new Date().getTime()); const refresh = () => setKey(new Date().getTime());
useEffect(() => { const loader = async () => {
return asyncStateFetch( const defaultClientScopes = await adminClient.clients.listDefaultClientScopes(
async () => { { id: clientId }
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
); );
}, [key]); const optionalClientScopes = await adminClient.clients.listOptionalClientScopes(
{ id: clientId }
);
const clientScopes = await adminClient.clientScopes.find();
const dropdown = (): IFormatter => (data?: IFormatterValueType) => { const find = (id: string) =>
if (!data) { clientScopes.find((clientScope) => id === clientScope.id)!;
return <></>;
} const optional = optionalClientScopes.map((c) => {
const row = rows?.find((row) => row.clientScope.id === data.toString())!; const scope = find(c.id!);
return ( 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 <CellDropdown
clientScope={row.clientScope} clientScope={scope}
type={row.type} type={scope.type}
onSelect={async (value) => { onSelect={async (value) => {
try { try {
await changeScope( await changeScope(adminClient, clientId, scope, scope.type, value);
adminClient,
clientId,
row.clientScope,
row.type,
value
);
addAlert(t("clientScopeSuccess"), AlertVariant.success); addAlert(t("clientScopeSuccess"), AlertVariant.success);
await refresh(); refresh();
} catch (error) { } catch (error) {
addAlert(t("clientScopeError", { error }), AlertVariant.danger); addAlert(t("clientScopeError", { error }), AlertVariant.danger);
} }
}} }}
/> />
); </>
}; );
const filterData = () => {};
return ( return (
<> <>
{!rows && (
<div className="pf-u-text-align-center">
<Spinner />
</div>
)}
{rest && ( {rest && (
<AddScopeDialog <AddScopeDialog
clientScopes={rest} clientScopes={rest}
@ -254,202 +218,154 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
/> />
)} )}
{rows && rows.length > 0 && ( <KeycloakDataTable
<TableToolbar key={key}
searchTypeComponent={ loader={loader}
<Dropdown ariaLabelKey="clients:clientScopeList"
toggle={ searchPlaceholderKey="clients:searchByName"
<DropdownToggle onSelect={(rows) => setSelectedRows([...rows])}
id="toggle-id" searchTypeComponent={
onToggle={() => setSearchToggle(!searchToggle)} <Dropdown
> toggle={
<FilterIcon /> {t(`clientScopeSearch.${searchType}`)} <DropdownToggle
</DropdownToggle> id="toggle-id"
} onToggle={() => setSearchToggle(!searchToggle)}
aria-label="Select Input" >
isOpen={searchToggle} <FilterIcon /> {t(`clientScopeSearch.${searchType}`)}
dropdownItems={[ </DropdownToggle>
<DropdownItem }
key="client" aria-label="Select Input"
onClick={() => { isOpen={searchToggle}
setSearchType("client"); dropdownItems={[
setSearchToggle(false); <DropdownItem
}} key="client"
> onClick={() => {
{t("clientScopeSearch.client")} setSearchType("client");
</DropdownItem>, setSearchToggle(false);
<DropdownItem }}
key="assigned" >
onClick={() => { {t("clientScopeSearch.client")}
setSearchType("assigned"); </DropdownItem>,
setSearchToggle(false); <DropdownItem
}} key="assigned"
> onClick={() => {
{t("clientScopeSearch.assigned")} setSearchType("assigned");
</DropdownItem>, setSearchToggle(false);
]} }}
/> >
} {t("clientScopeSearch.assigned")}
inputGroupName="clientsScopeToolbarTextInput" </DropdownItem>,
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"),
]} ]}
rows={rows} />
actions={[ }
{ toolbarItem={
title: t("common:remove"), <>
onClick: async (_, rowId) => { <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 { try {
await removeScope( await Promise.all(
adminClient, selectedRows.map((row) => {
clientId, return changeScope(
rows[rowId].clientScope, adminClient,
rows[rowId].type clientId,
); { ...row },
addAlert( row.type,
t("clientScopeRemoveSuccess"), value as ClientScope
AlertVariant.success );
})
); );
setAddToggle(false);
refresh(); refresh();
addAlert(t("clientScopeSuccess"), AlertVariant.success);
} catch (error) { } catch (error) {
addAlert( addAlert(
t("clientScopeRemoveError", { error }), t("clientScopeError", { error }),
AlertVariant.danger AlertVariant.danger
); );
} }
}, }}
}, >
]} {clientScopeTypesSelectOptions(t)}
aria-label={t("clientScopeList")} </Select>
> </ToolbarItem>
<TableHeader /> <ToolbarItem>
<TableBody /> <Dropdown
</Table> onSelect={() => {}}
</TableToolbar> toggle={
)} <KebabToggle onToggle={() => setKebabOpen(!kebabOpen)} />
{rows && rows.length === 0 && ( }
<ListEmptyState isOpen={kebabOpen}
message={t("clients:emptyClientScopes")} isPlain
instructions={t("clients:emptyClientScopesInstructions")} dropdownItems={[
primaryActionText={t("clients:emptyClientScopesPrimaryAction")} <DropdownItem
onPrimaryAction={() => setAddDialogOpen(true)} 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 { useTranslation } from "react-i18next";
import {
Table,
TableBody,
TableHeader,
TableVariant,
} from "@patternfly/react-table";
import { Badge, Button, Checkbox, ToolbarItem } from "@patternfly/react-core"; 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 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 { emptyFormatter } from "../../util";
import "./service-account.css"; import "./service-account.css";
@ -21,6 +15,11 @@ type ServiceAccountProps = {
clientId: string; clientId: string;
}; };
type Row = {
client: ClientRepresentation;
role: CompositeRole;
};
type CompositeRole = RoleRepresentation & { type CompositeRole = RoleRepresentation & {
parent: RoleRepresentation; parent: RoleRepresentation;
}; };
@ -66,6 +65,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
}; };
const clientRolesFlat = clientRoles.map((row) => row.roles).flat(); const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
console.log(clientRolesFlat);
const addInherentData = await (async () => const addInherentData = await (async () =>
Promise.all( Promise.all(
@ -92,35 +92,34 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
] as CompositeRole[]) ] as CompositeRole[])
.sort((r1, r2) => r1.name!.localeCompare(r2.name!)) .sort((r1, r2) => r1.name!.localeCompare(r2.name!))
.map((role) => { .map((role) => {
const client = findClient(role);
return { return {
cells: [ client: findClient(role),
<Fragment key={role.id}> role,
{client && ( } as Row;
<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,
],
};
}); });
}; };
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 ( return (
<TableToolbar <KeycloakDataTable
inputGroupName="clientsServiceAccountRoleToolbarTextInput" loader={loader}
inputGroupPlaceholder={t("searchByName")} onSelect={() => {}}
inputGroupOnChange={filterData} searchPlaceholderKey="clients:searchByName"
ariaLabelKey="clients:clientScopeList"
toolbarItem={ toolbarItem={
<> <>
<ToolbarItem> <ToolbarItem>
@ -136,34 +135,23 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
</ToolbarItem> </ToolbarItem>
</> </>
} }
> columns={[
<DataLoader loader={loader} deps={[clientId]}> {
{(clientRoles) => ( name: "role.name",
<> displayKey: t("name"),
{hide ? "" : " "} cellRenderer: RoleLink,
<Table },
onSelect={() => {}} {
variant={TableVariant.compact} name: "role.parent.name",
cells={[ displayKey: t("inherentFrom"),
t("roles:roleName"), cellFormatters: [emptyFormatter()],
{ },
title: t("inherentFrom"), {
cellFormatters: [emptyFormatter()], name: "role.description",
}, displayKey: t("description"),
{ cellFormatters: [emptyFormatter()],
title: t("common:description"), },
cellFormatters: [emptyFormatter()], ]}
}, />
]}
rows={clientRoles}
aria-label="roleList"
>
<TableHeader />
<TableBody />
</Table>
</>
)}
</DataLoader>
</TableToolbar>
); );
}; };

View file

@ -92,13 +92,13 @@ export const routes: RoutesFn = (t: TFunction) => [
access: "manage-clients", 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, component: RoleMappingForm,
breadcrumb: t("client-scopes:mappingDetails"), breadcrumb: t("client-scopes:mappingDetails"),
access: "view-clients", access: "view-clients",
}, },
{ {
path: "/:realm/client-scopes/:id/mappers/:mapperId", path: "/:realm/client-scopes/:id/:tab/:mapperId",
component: MappingDetails, component: MappingDetails,
breadcrumb: t("client-scopes:mappingDetails"), breadcrumb: t("client-scopes:mappingDetails"),
access: "view-clients", access: "view-clients",