commit
d90b1842ce
11 changed files with 210 additions and 164 deletions
|
@ -19,7 +19,11 @@ import { convertFormValuesToObject } from "../../util";
|
||||||
import { MapperList } from "../details/MapperList";
|
import { MapperList } from "../details/MapperList";
|
||||||
import { ScopeForm } from "../details/ScopeForm";
|
import { ScopeForm } from "../details/ScopeForm";
|
||||||
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
import { useConfirmDialog } from "../../components/confirm-dialog/ConfirmDialog";
|
||||||
import { RoleMapping, Row } from "../../components/role-mapping/RoleMapping";
|
import {
|
||||||
|
mapRoles,
|
||||||
|
RoleMapping,
|
||||||
|
Row,
|
||||||
|
} from "../../components/role-mapping/RoleMapping";
|
||||||
import type { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation";
|
import type { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation";
|
||||||
import {
|
import {
|
||||||
AllClientScopes,
|
AllClientScopes,
|
||||||
|
@ -61,41 +65,35 @@ export const ClientScopeForm = () => {
|
||||||
);
|
);
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const assignedRoles = hide
|
const assignedRoles = (
|
||||||
? await adminClient.clientScopes.listRealmScopeMappings({ id })
|
await adminClient.clientScopes.listRealmScopeMappings({ id })
|
||||||
: await adminClient.clientScopes.listCompositeRealmScopeMappings({ id });
|
).map((role) => ({ role }));
|
||||||
|
const effectiveRoles = (
|
||||||
|
await adminClient.clientScopes.listCompositeRealmScopeMappings({ id })
|
||||||
|
).map((role) => ({ role }));
|
||||||
const clients = await adminClient.clients.find();
|
const clients = await adminClient.clients.find();
|
||||||
|
|
||||||
const clientRoles = (
|
const clientRoles = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
clients.map(async (client) => {
|
clients.map(async (client) => {
|
||||||
const clientScope = hide
|
const clientAssignedRoles = (
|
||||||
? await adminClient.clientScopes.listClientScopeMappings({
|
await adminClient.clientScopes.listClientScopeMappings({
|
||||||
id,
|
id,
|
||||||
client: client.id!,
|
client: client.id!,
|
||||||
})
|
})
|
||||||
: await adminClient.clientScopes.listCompositeClientScopeMappings({
|
).map((role) => ({ role, client }));
|
||||||
id,
|
const clientEffectiveRoles = (
|
||||||
client: client.id!,
|
await adminClient.clientScopes.listCompositeClientScopeMappings({
|
||||||
});
|
id,
|
||||||
return clientScope.map((scope) => {
|
client: client.id!,
|
||||||
return {
|
})
|
||||||
client,
|
).map((role) => ({ role, client }));
|
||||||
role: scope,
|
return mapRoles(clientAssignedRoles, clientEffectiveRoles, hide);
|
||||||
};
|
|
||||||
});
|
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).flat();
|
).flat();
|
||||||
|
|
||||||
return [
|
return [...mapRoles(assignedRoles, effectiveRoles, hide), ...clientRoles];
|
||||||
...assignedRoles.map((role) => {
|
|
||||||
return {
|
|
||||||
role,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
...clientRoles,
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const save = async (clientScopes: ClientScopeDefaultOptionalType) => {
|
const save = async (clientScopes: ClientScopeDefaultOptionalType) => {
|
||||||
|
|
|
@ -81,6 +81,7 @@ const ClientDetailHeader = ({
|
||||||
subKey="clients:clientsExplain"
|
subKey="clients:clientsExplain"
|
||||||
badge={client.protocol}
|
badge={client.protocol}
|
||||||
divider={false}
|
divider={false}
|
||||||
|
helpTextKey="clients-help:enableDisable"
|
||||||
dropdownItems={[
|
dropdownItems={[
|
||||||
<DropdownItem key="download" onClick={() => toggleDownloadDialog()}>
|
<DropdownItem key="download" onClick={() => toggleDownloadDialog()}>
|
||||||
{t("downloadAdapterConfig")}
|
{t("downloadAdapterConfig")}
|
||||||
|
@ -358,7 +359,7 @@ export const ClientDetails = () => {
|
||||||
eventKey="serviceAccount"
|
eventKey="serviceAccount"
|
||||||
title={<TabTitleText>{t("serviceAccount")}</TabTitleText>}
|
title={<TabTitleText>{t("serviceAccount")}</TabTitleText>}
|
||||||
>
|
>
|
||||||
<ServiceAccount clientId={clientId} />
|
<ServiceAccount client={client} />
|
||||||
</Tab>
|
</Tab>
|
||||||
)}
|
)}
|
||||||
<Tab
|
<Tab
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
{
|
{
|
||||||
"clients-help": {
|
"clients-help": {
|
||||||
|
"enableDisable": "Disabled clients cannot initiate a login or have obtained access tokens.",
|
||||||
"clientType": "'OpenID connect' allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server.'SAML' enables web-based authentication and authorization scenarios including cross-domain single sign-on (SSO) and uses security tokens containing assertions to pass information.",
|
"clientType": "'OpenID connect' allows Clients to verify the identity of the End-User based on the authentication performed by an Authorization Server.'SAML' enables web-based authentication and authorization scenarios including cross-domain single sign-on (SSO) and uses security tokens containing assertions to pass information.",
|
||||||
"serviceAccount": "Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. In terms of OAuth2 specification, this enables support of 'Client Credentials Grant' for this client.",
|
"serviceAccount": "Allows you to authenticate this client to Keycloak and retrieve access token dedicated to this client. In terms of OAuth2 specification, this enables support of 'Client Credentials Grant' for this client.",
|
||||||
"authentication": "This defines the type of the OIDC client. When it's ON, the OIDC type is set to confidential access type. When it's OFF, it is set to public access type",
|
"authentication": "This defines the type of the OIDC client. When it's ON, the OIDC type is set to confidential access type. When it's OFF, it is set to public access type",
|
||||||
|
|
|
@ -105,7 +105,7 @@
|
||||||
"authenticationFlow": "Authentication flow",
|
"authenticationFlow": "Authentication flow",
|
||||||
"standardFlow": "Standard flow",
|
"standardFlow": "Standard flow",
|
||||||
"directAccess": "Direct access grants",
|
"directAccess": "Direct access grants",
|
||||||
"serviceAccount": "Service accounts",
|
"serviceAccount": "Service accounts roles",
|
||||||
"enableServiceAccount": "Enable service account roles",
|
"enableServiceAccount": "Enable service account roles",
|
||||||
"assignRolesTo": "Assign roles to {{client}} account",
|
"assignRolesTo": "Assign roles to {{client}} account",
|
||||||
"searchByRoleName": "Search by role name",
|
"searchByRoleName": "Search by role name",
|
||||||
|
|
|
@ -1,99 +1,74 @@
|
||||||
import React, { useContext, useState } from "react";
|
import React, { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { AlertVariant } from "@patternfly/react-core";
|
import { AlertVariant } from "@patternfly/react-core";
|
||||||
|
|
||||||
import type RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
import type UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||||
import type { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation";
|
import type { RoleMappingPayload } from "keycloak-admin/lib/defs/roleRepresentation";
|
||||||
import { useAdminClient } from "../../context/auth/AdminClient";
|
import type ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||||
import { RealmContext } from "../../context/realm-context/RealmContext";
|
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
|
||||||
import { useAlerts } from "../../components/alert/Alerts";
|
import { useAlerts } from "../../components/alert/Alerts";
|
||||||
import {
|
import {
|
||||||
CompositeRole,
|
mapRoles,
|
||||||
RoleMapping,
|
RoleMapping,
|
||||||
Row,
|
Row,
|
||||||
} from "../../components/role-mapping/RoleMapping";
|
} from "../../components/role-mapping/RoleMapping";
|
||||||
|
|
||||||
type ServiceAccountProps = {
|
type ServiceAccountProps = {
|
||||||
clientId: string;
|
client: ClientRepresentation;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
export const ServiceAccount = ({ client }: ServiceAccountProps) => {
|
||||||
const { t } = useTranslation("clients");
|
const { t } = useTranslation("clients");
|
||||||
const adminClient = useAdminClient();
|
const adminClient = useAdminClient();
|
||||||
const { realm } = useContext(RealmContext);
|
|
||||||
const { addAlert } = useAlerts();
|
const { addAlert } = useAlerts();
|
||||||
|
|
||||||
const [hide, setHide] = useState(false);
|
const [hide, setHide] = useState(false);
|
||||||
const [serviceAccountId, setServiceAccountId] = useState("");
|
const [serviceAccount, setServiceAccount] = useState<UserRepresentation>();
|
||||||
const [name, setName] = useState("");
|
|
||||||
|
useFetch(
|
||||||
|
() =>
|
||||||
|
adminClient.clients.getServiceAccountUser({
|
||||||
|
id: client.id!,
|
||||||
|
}),
|
||||||
|
(serviceAccount) => setServiceAccount(serviceAccount),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const loader = async () => {
|
const loader = async () => {
|
||||||
const serviceAccount = await adminClient.clients.getServiceAccountUser({
|
const serviceAccount = await adminClient.clients.getServiceAccountUser({
|
||||||
id: clientId,
|
id: client.id!,
|
||||||
});
|
|
||||||
setServiceAccountId(serviceAccount.id!);
|
|
||||||
const effectiveRoles = await adminClient.users.listCompositeRealmRoleMappings(
|
|
||||||
{ id: serviceAccount.id! }
|
|
||||||
);
|
|
||||||
const assignedRoles = await adminClient.users.listRealmRoleMappings({
|
|
||||||
id: serviceAccount.id!,
|
|
||||||
});
|
});
|
||||||
|
const id = serviceAccount.id!;
|
||||||
|
|
||||||
|
const assignedRoles = (
|
||||||
|
await adminClient.users.listRealmRoleMappings({ id })
|
||||||
|
).map((role) => ({ role }));
|
||||||
|
const effectiveRoles = (
|
||||||
|
await adminClient.users.listCompositeRealmRoleMappings({ id })
|
||||||
|
).map((role) => ({ role }));
|
||||||
|
|
||||||
const clients = await adminClient.clients.find();
|
const clients = await adminClient.clients.find();
|
||||||
setName(clients.find((c) => c.id === clientId)?.clientId!);
|
|
||||||
const clientRoles = (
|
const clientRoles = (
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
clients.map(async (client) => {
|
clients.map(async (client) => {
|
||||||
return {
|
const clientAssignedRoles = (
|
||||||
client,
|
await adminClient.users.listClientRoleMappings({
|
||||||
roles: await adminClient.users.listClientRoleMappings({
|
id,
|
||||||
id: serviceAccount.id!,
|
|
||||||
clientUniqueId: client.id!,
|
clientUniqueId: client.id!,
|
||||||
}),
|
})
|
||||||
};
|
).map((role) => ({ role, client }));
|
||||||
|
const clientEffectiveRoles = (
|
||||||
|
await adminClient.users.listCompositeClientRoleMappings({
|
||||||
|
id,
|
||||||
|
clientUniqueId: client.id!,
|
||||||
|
})
|
||||||
|
).map((role) => ({ role, client }));
|
||||||
|
return mapRoles(clientAssignedRoles, clientEffectiveRoles, hide);
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
).filter((rows) => rows.roles.length > 0);
|
).flat();
|
||||||
|
|
||||||
const findClient = (role: RoleRepresentation) => {
|
return [...mapRoles(assignedRoles, effectiveRoles, hide), ...clientRoles];
|
||||||
const row = clientRoles.filter((row) =>
|
|
||||||
row.roles.find((r) => r.id === role.id)
|
|
||||||
)[0];
|
|
||||||
return row ? row.client : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
|
|
||||||
|
|
||||||
const addInherentData = await (async () =>
|
|
||||||
Promise.all(
|
|
||||||
effectiveRoles.map(async (role) => {
|
|
||||||
const compositeRoles = await adminClient.roles.getCompositeRolesForRealm(
|
|
||||||
{ realm, id: role.id! }
|
|
||||||
);
|
|
||||||
return compositeRoles.length > 0
|
|
||||||
? compositeRoles.map((r) => {
|
|
||||||
return { ...r, parent: role };
|
|
||||||
})
|
|
||||||
: { ...role, parent: undefined };
|
|
||||||
})
|
|
||||||
))();
|
|
||||||
const uniqueRolesWithParent = addInherentData
|
|
||||||
.flat()
|
|
||||||
.filter(
|
|
||||||
(role, index, array) =>
|
|
||||||
array.findIndex((r) => r.id === role.id) === index
|
|
||||||
);
|
|
||||||
return ([
|
|
||||||
...(hide ? assignedRoles : uniqueRolesWithParent),
|
|
||||||
...clientRolesFlat,
|
|
||||||
] as CompositeRole[])
|
|
||||||
.sort((r1, r2) => r1.name!.localeCompare(r2.name!))
|
|
||||||
.map((role) => {
|
|
||||||
return {
|
|
||||||
client: findClient(role),
|
|
||||||
role,
|
|
||||||
} as Row;
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const assignRoles = async (rows: Row[]) => {
|
const assignRoles = async (rows: Row[]) => {
|
||||||
|
@ -103,7 +78,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
.map((row) => row.role as RoleMappingPayload)
|
.map((row) => row.role as RoleMappingPayload)
|
||||||
.flat();
|
.flat();
|
||||||
adminClient.users.addRealmRoleMappings({
|
adminClient.users.addRealmRoleMappings({
|
||||||
id: serviceAccountId,
|
id: serviceAccount?.id!,
|
||||||
roles: realmRoles,
|
roles: realmRoles,
|
||||||
});
|
});
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
|
@ -111,7 +86,7 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
.filter((row) => row.client !== undefined)
|
.filter((row) => row.client !== undefined)
|
||||||
.map((row) =>
|
.map((row) =>
|
||||||
adminClient.users.addClientRoleMappings({
|
adminClient.users.addClientRoleMappings({
|
||||||
id: serviceAccountId,
|
id: serviceAccount?.id!,
|
||||||
clientUniqueId: row.client!.id!,
|
clientUniqueId: row.client!.id!,
|
||||||
roles: [row.role as RoleMappingPayload],
|
roles: [row.role as RoleMappingPayload],
|
||||||
})
|
})
|
||||||
|
@ -128,13 +103,17 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
<RoleMapping
|
<>
|
||||||
name={name}
|
{serviceAccount && (
|
||||||
id={serviceAccountId}
|
<RoleMapping
|
||||||
type={"service-account"}
|
name={client.clientId!}
|
||||||
loader={loader}
|
id={serviceAccount.id!}
|
||||||
save={assignRoles}
|
type="service-account"
|
||||||
onHideRolesToggle={() => setHide(!hide)}
|
loader={loader}
|
||||||
/>
|
save={assignRoles}
|
||||||
|
onHideRolesToggle={() => setHide(!hide)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
|
@ -248,60 +248,64 @@ export const AddRoleMappingModal = ({
|
||||||
</Button>,
|
</Button>,
|
||||||
]}
|
]}
|
||||||
>
|
>
|
||||||
<Select
|
|
||||||
toggleId="role"
|
|
||||||
onToggle={() => setSearchToggle(!searchToggle)}
|
|
||||||
isOpen={searchToggle}
|
|
||||||
variant={isRadio ? SelectVariant.single : SelectVariant.checkbox}
|
|
||||||
hasInlineFilter
|
|
||||||
menuAppendTo="parent"
|
|
||||||
placeholderText={
|
|
||||||
<>
|
|
||||||
<FilterIcon /> {t("filterByOrigin")}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
isGrouped
|
|
||||||
onFilter={(evt) => {
|
|
||||||
const value = evt?.target.value || "";
|
|
||||||
return createSelectGroup(
|
|
||||||
clients.filter((client) => client.clientId?.includes(value))
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
selections={selectedClients}
|
|
||||||
onClear={() => setSelectedClients([])}
|
|
||||||
onSelect={(_, selection) => {
|
|
||||||
const client = selection as ClientRole;
|
|
||||||
if (selectedClients.includes(client)) {
|
|
||||||
removeClient(client);
|
|
||||||
} else {
|
|
||||||
setSelectedClients([...selectedClients, client]);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{createSelectGroup(clients)}
|
|
||||||
</Select>
|
|
||||||
<ToolbarItem variant="chip-group">
|
|
||||||
<ChipGroup>
|
|
||||||
{selectedClients.map((client) => (
|
|
||||||
<Chip
|
|
||||||
key={`chip-${client.id}`}
|
|
||||||
onClick={() => {
|
|
||||||
removeClient(client);
|
|
||||||
refresh();
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{client.clientId || t("realmRoles")}
|
|
||||||
<Badge isRead={true}>{client.numberOfRoles}</Badge>
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</ChipGroup>
|
|
||||||
</ToolbarItem>
|
|
||||||
|
|
||||||
<KeycloakDataTable
|
<KeycloakDataTable
|
||||||
key={key}
|
key={key}
|
||||||
onSelect={(rows) => setSelectedRows([...rows])}
|
onSelect={(rows) => setSelectedRows([...rows])}
|
||||||
searchPlaceholderKey="clients:searchByRoleName"
|
searchPlaceholderKey="clients:searchByRoleName"
|
||||||
canSelectAll={false}
|
searchTypeComponent={
|
||||||
|
<ToolbarItem>
|
||||||
|
<Select
|
||||||
|
toggleId="role"
|
||||||
|
onToggle={() => setSearchToggle(!searchToggle)}
|
||||||
|
isOpen={searchToggle}
|
||||||
|
variant={isRadio ? SelectVariant.single : SelectVariant.checkbox}
|
||||||
|
hasInlineFilter
|
||||||
|
placeholderText={
|
||||||
|
<>
|
||||||
|
<FilterIcon /> {t("filterByOrigin")}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
isGrouped
|
||||||
|
onFilter={(evt) => {
|
||||||
|
const value = evt?.target.value || "";
|
||||||
|
return createSelectGroup(
|
||||||
|
clients.filter((client) => client.clientId?.includes(value))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
selections={selectedClients}
|
||||||
|
onClear={() => setSelectedClients([])}
|
||||||
|
onSelect={(_, selection) => {
|
||||||
|
const client = selection as ClientRole;
|
||||||
|
if (selectedClients.includes(client)) {
|
||||||
|
removeClient(client);
|
||||||
|
} else {
|
||||||
|
setSelectedClients([...selectedClients, client]);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{createSelectGroup(clients)}
|
||||||
|
</Select>
|
||||||
|
</ToolbarItem>
|
||||||
|
}
|
||||||
|
subToolbar={
|
||||||
|
<ToolbarItem widths={{ default: "100%" }}>
|
||||||
|
<ChipGroup>
|
||||||
|
{selectedClients.map((client) => (
|
||||||
|
<Chip
|
||||||
|
key={`chip-${client.id}`}
|
||||||
|
onClick={() => {
|
||||||
|
removeClient(client);
|
||||||
|
refresh();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{client.clientId || t("realmRoles")}
|
||||||
|
<Badge isRead={true}>{client.numberOfRoles}</Badge>
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ChipGroup>
|
||||||
|
</ToolbarItem>
|
||||||
|
}
|
||||||
|
canSelectAll
|
||||||
isRadio={isRadio}
|
isRadio={isRadio}
|
||||||
loader={loader}
|
loader={loader}
|
||||||
ariaLabelKey="clients:roles"
|
ariaLabelKey="clients:roles"
|
||||||
|
|
|
@ -23,11 +23,38 @@ import { ListEmptyState } from "../list-empty-state/ListEmptyState";
|
||||||
|
|
||||||
export type CompositeRole = RoleRepresentation & {
|
export type CompositeRole = RoleRepresentation & {
|
||||||
parent: RoleRepresentation;
|
parent: RoleRepresentation;
|
||||||
|
isInherited?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Row = {
|
export type Row = {
|
||||||
client?: ClientRepresentation;
|
client?: ClientRepresentation;
|
||||||
role: CompositeRole | RoleRepresentation;
|
role: RoleRepresentation | CompositeRole;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mapRoles = (
|
||||||
|
assignedRoles: Row[],
|
||||||
|
effectiveRoles: Row[],
|
||||||
|
hide: boolean
|
||||||
|
) => {
|
||||||
|
return [
|
||||||
|
...(hide
|
||||||
|
? assignedRoles.map((row) => ({
|
||||||
|
...row,
|
||||||
|
role: {
|
||||||
|
...row.role,
|
||||||
|
isInherited: false,
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
: effectiveRoles.map((row) => ({
|
||||||
|
...row,
|
||||||
|
role: {
|
||||||
|
...row.role,
|
||||||
|
isInherited:
|
||||||
|
assignedRoles.find((r) => r.role.id === row.role.id) ===
|
||||||
|
undefined,
|
||||||
|
},
|
||||||
|
}))),
|
||||||
|
];
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ServiceRole = ({ role, client }: Row) => (
|
export const ServiceRole = ({ role, client }: Row) => (
|
||||||
|
@ -153,10 +180,13 @@ export const RoleMapping = ({
|
||||||
data-testid="assigned-roles"
|
data-testid="assigned-roles"
|
||||||
key={key}
|
key={key}
|
||||||
loader={loader}
|
loader={loader}
|
||||||
canSelectAll={hide}
|
canSelectAll
|
||||||
onSelect={hide ? (rows) => setSelected(rows) : undefined}
|
onSelect={(rows) => setSelected(rows)}
|
||||||
searchPlaceholderKey="clients:searchByName"
|
searchPlaceholderKey="clients:searchByName"
|
||||||
ariaLabelKey="clients:clientScopeList"
|
ariaLabelKey="clients:clientScopeList"
|
||||||
|
isRowDisabled={(value) =>
|
||||||
|
(value.role as CompositeRole).isInherited || false
|
||||||
|
}
|
||||||
toolbarItem={
|
toolbarItem={
|
||||||
<>
|
<>
|
||||||
<ToolbarItem>
|
<ToolbarItem>
|
||||||
|
@ -191,6 +221,16 @@ export const RoleMapping = ({
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
actions={[
|
||||||
|
{
|
||||||
|
title: t("unAssignRole"),
|
||||||
|
onRowClick: async (role) => {
|
||||||
|
setSelected([role]);
|
||||||
|
toggleDeleteDialog();
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
name: "role.name",
|
name: "role.name",
|
||||||
|
|
|
@ -133,6 +133,7 @@ export type DataListProps<T> = {
|
||||||
actionResolver?: IActionsResolver;
|
actionResolver?: IActionsResolver;
|
||||||
searchTypeComponent?: ReactNode;
|
searchTypeComponent?: ReactNode;
|
||||||
toolbarItem?: ReactNode;
|
toolbarItem?: ReactNode;
|
||||||
|
subToolbar?: ReactNode;
|
||||||
emptyState?: ReactNode;
|
emptyState?: ReactNode;
|
||||||
icon?: React.ComponentClass<SVGIconProps>;
|
icon?: React.ComponentClass<SVGIconProps>;
|
||||||
isNotCompact?: boolean;
|
isNotCompact?: boolean;
|
||||||
|
@ -178,6 +179,7 @@ export function KeycloakDataTable<T>({
|
||||||
actionResolver,
|
actionResolver,
|
||||||
searchTypeComponent,
|
searchTypeComponent,
|
||||||
toolbarItem,
|
toolbarItem,
|
||||||
|
subToolbar,
|
||||||
emptyState,
|
emptyState,
|
||||||
icon,
|
icon,
|
||||||
...props
|
...props
|
||||||
|
@ -388,6 +390,7 @@ export function KeycloakDataTable<T>({
|
||||||
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
inputGroupPlaceholder={t(searchPlaceholderKey || "")}
|
||||||
searchTypeComponent={searchTypeComponent}
|
searchTypeComponent={searchTypeComponent}
|
||||||
toolbarItem={toolbarItem}
|
toolbarItem={toolbarItem}
|
||||||
|
subToolbar={subToolbar}
|
||||||
>
|
>
|
||||||
{!loading && data && data.length > 0 && (
|
{!loading && data && data.length > 0 && (
|
||||||
<DataTable
|
<DataTable
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React from "react";
|
import React, { FormEvent, ReactNode } from "react";
|
||||||
import {
|
import {
|
||||||
Pagination,
|
Pagination,
|
||||||
ToggleTemplateProps,
|
ToggleTemplateProps,
|
||||||
|
@ -13,14 +13,15 @@ type TableToolbarProps = {
|
||||||
onNextClick: (page: number) => void;
|
onNextClick: (page: number) => void;
|
||||||
onPreviousClick: (page: number) => void;
|
onPreviousClick: (page: number) => void;
|
||||||
onPerPageSelect: (max: number, first: number) => void;
|
onPerPageSelect: (max: number, first: number) => void;
|
||||||
searchTypeComponent?: React.ReactNode;
|
searchTypeComponent?: ReactNode;
|
||||||
toolbarItem?: React.ReactNode;
|
toolbarItem?: ReactNode;
|
||||||
children: React.ReactNode;
|
subToolbar?: ReactNode;
|
||||||
|
children: ReactNode;
|
||||||
inputGroupName?: string;
|
inputGroupName?: string;
|
||||||
inputGroupPlaceholder?: string;
|
inputGroupPlaceholder?: string;
|
||||||
inputGroupOnChange?: (
|
inputGroupOnChange?: (
|
||||||
newInput: string,
|
newInput: string,
|
||||||
event: React.FormEvent<HTMLInputElement>
|
event: FormEvent<HTMLInputElement>
|
||||||
) => void;
|
) => void;
|
||||||
inputGroupOnEnter?: (value: string) => void;
|
inputGroupOnEnter?: (value: string) => void;
|
||||||
};
|
};
|
||||||
|
@ -34,6 +35,7 @@ export const PaginatingTableToolbar = ({
|
||||||
onPerPageSelect,
|
onPerPageSelect,
|
||||||
searchTypeComponent,
|
searchTypeComponent,
|
||||||
toolbarItem,
|
toolbarItem,
|
||||||
|
subToolbar,
|
||||||
children,
|
children,
|
||||||
inputGroupName,
|
inputGroupName,
|
||||||
inputGroupPlaceholder,
|
inputGroupPlaceholder,
|
||||||
|
@ -70,6 +72,7 @@ export const PaginatingTableToolbar = ({
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
|
subToolbar={subToolbar}
|
||||||
toolbarItemFooter={
|
toolbarItemFooter={
|
||||||
count !== 0 ? <ToolbarItem>{pagination("bottom")}</ToolbarItem> : <></>
|
count !== 0 ? <ToolbarItem>{pagination("bottom")}</ToolbarItem> : <></>
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { FormEvent, Fragment, ReactNode } from "react";
|
import React, { FormEvent, Fragment, ReactNode, useState } from "react";
|
||||||
import {
|
import {
|
||||||
Toolbar,
|
Toolbar,
|
||||||
ToolbarContent,
|
ToolbarContent,
|
||||||
|
@ -14,6 +14,7 @@ import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
type TableToolbarProps = {
|
type TableToolbarProps = {
|
||||||
toolbarItem?: ReactNode;
|
toolbarItem?: ReactNode;
|
||||||
|
subToolbar?: ReactNode;
|
||||||
toolbarItemFooter?: ReactNode;
|
toolbarItemFooter?: ReactNode;
|
||||||
children: ReactNode;
|
children: ReactNode;
|
||||||
searchTypeComponent?: ReactNode;
|
searchTypeComponent?: ReactNode;
|
||||||
|
@ -28,6 +29,7 @@ type TableToolbarProps = {
|
||||||
|
|
||||||
export const TableToolbar = ({
|
export const TableToolbar = ({
|
||||||
toolbarItem,
|
toolbarItem,
|
||||||
|
subToolbar,
|
||||||
toolbarItemFooter,
|
toolbarItemFooter,
|
||||||
children,
|
children,
|
||||||
searchTypeComponent,
|
searchTypeComponent,
|
||||||
|
@ -37,7 +39,7 @@ export const TableToolbar = ({
|
||||||
inputGroupOnEnter,
|
inputGroupOnEnter,
|
||||||
}: TableToolbarProps) => {
|
}: TableToolbarProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const [searchValue, setSearchValue] = React.useState<string>("");
|
const [searchValue, setSearchValue] = useState<string>("");
|
||||||
|
|
||||||
const onSearch = () => {
|
const onSearch = () => {
|
||||||
if (searchValue !== "") {
|
if (searchValue !== "") {
|
||||||
|
@ -99,6 +101,11 @@ export const TableToolbar = ({
|
||||||
{toolbarItem}
|
{toolbarItem}
|
||||||
</ToolbarContent>
|
</ToolbarContent>
|
||||||
</Toolbar>
|
</Toolbar>
|
||||||
|
{subToolbar && (
|
||||||
|
<Toolbar>
|
||||||
|
<ToolbarContent>{subToolbar}</ToolbarContent>
|
||||||
|
</Toolbar>
|
||||||
|
)}
|
||||||
<Divider />
|
<Divider />
|
||||||
{children}
|
{children}
|
||||||
<Toolbar>{toolbarItemFooter}</Toolbar>
|
<Toolbar>{toolbarItemFooter}</Toolbar>
|
||||||
|
|
|
@ -21,6 +21,7 @@ import {
|
||||||
FormattedLink,
|
FormattedLink,
|
||||||
FormattedLinkProps,
|
FormattedLinkProps,
|
||||||
} from "../external-link/FormattedLink";
|
} from "../external-link/FormattedLink";
|
||||||
|
import { HelpItem } from "../help-enabler/HelpItem";
|
||||||
|
|
||||||
export type ViewHeaderProps = {
|
export type ViewHeaderProps = {
|
||||||
titleKey: string;
|
titleKey: string;
|
||||||
|
@ -36,6 +37,7 @@ export type ViewHeaderProps = {
|
||||||
isEnabled?: boolean;
|
isEnabled?: boolean;
|
||||||
onToggle?: (value: boolean) => void;
|
onToggle?: (value: boolean) => void;
|
||||||
divider?: boolean;
|
divider?: boolean;
|
||||||
|
helpTextKey?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const ViewHeader = ({
|
export const ViewHeader = ({
|
||||||
|
@ -51,6 +53,7 @@ export const ViewHeader = ({
|
||||||
isEnabled = true,
|
isEnabled = true,
|
||||||
onToggle,
|
onToggle,
|
||||||
divider = true,
|
divider = true,
|
||||||
|
helpTextKey,
|
||||||
}: ViewHeaderProps) => {
|
}: ViewHeaderProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { enabled } = useContext(HelpContext);
|
const { enabled } = useContext(HelpContext);
|
||||||
|
@ -106,6 +109,13 @@ export const ViewHeader = ({
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
{helpTextKey && (
|
||||||
|
<HelpItem
|
||||||
|
helpText={t(helpTextKey)}
|
||||||
|
forLabel={t("common:enabled")}
|
||||||
|
forID={`${titleKey}-switch`}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ToolbarItem>
|
</ToolbarItem>
|
||||||
)}
|
)}
|
||||||
{dropdownItems && (
|
{dropdownItems && (
|
||||||
|
|
Loading…
Reference in a new issue