Created add service account roles assign screen (#465)
* service account dialog * create test * fixed types * fixed realm roles selection * disable when no rows are selected Co-authored-by: Eugenia <32821331+jenny-s51@users.noreply.github.com> Co-authored-by: Eugenia <32821331+jenny-s51@users.noreply.github.com>
This commit is contained in:
parent
a0faba0f97
commit
84bf7925a6
11 changed files with 814 additions and 380 deletions
|
@ -8,6 +8,7 @@ import AdvancedTab from "../support/pages/admin_console/manage/clients/AdvancedT
|
|||
import AdminClient from "../support/util/AdminClient";
|
||||
import InitialAccessTokenTab from "../support/pages/admin_console/manage/clients/InitialAccessTokenTab";
|
||||
import { keycloakBefore } from "../support/util/keycloak_before";
|
||||
import ServiceAccountTab from "../support/pages/admin_console/manage/clients/ServiceAccountTab";
|
||||
|
||||
let itemId = "client_crud";
|
||||
const loginPage = new LoginPage();
|
||||
|
@ -162,4 +163,39 @@ describe("Clients test", function () {
|
|||
advancedTab.checkAccessTokenSignatureAlgorithm(algorithm);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Service account tab test", () => {
|
||||
const serviceAccountTab = new ServiceAccountTab();
|
||||
const serviceAccountName = "service-account-client";
|
||||
|
||||
beforeEach(() => {
|
||||
keycloakBefore();
|
||||
loginPage.logIn();
|
||||
sidebarPage.goToClients();
|
||||
});
|
||||
|
||||
before(async () => {
|
||||
await new AdminClient().createClient({
|
||||
protocol: "openid-connect",
|
||||
clientId: serviceAccountName,
|
||||
publicClient: false,
|
||||
authorizationServicesEnabled: true,
|
||||
serviceAccountsEnabled: true,
|
||||
standardFlowEnabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
after(() => {
|
||||
new AdminClient().deleteClient(serviceAccountName);
|
||||
});
|
||||
|
||||
it("list", () => {
|
||||
listingPage
|
||||
.searchItem(serviceAccountName)
|
||||
.goToItemDetails(serviceAccountName);
|
||||
serviceAccountTab
|
||||
.goToTab()
|
||||
.checkRoles(["manage-account", "offline_access", "uma_authorization"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -64,6 +64,13 @@ export default class CreateClientPage {
|
|||
return this;
|
||||
}
|
||||
|
||||
changeSwitches(switches: string[]) {
|
||||
for (const uiSwitch of switches) {
|
||||
cy.getId(uiSwitch).check({ force: true });
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
checkClientTypeRequiredMessage(exist = true) {
|
||||
cy.get(this.clientTypeError).should((!exist ? "not." : "") + "exist");
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
const expect = chai.expect;
|
||||
export default class ServiceAccountTab {
|
||||
private tab = "#pf-tab-serviceAccount-serviceAccount";
|
||||
private assignedRolesTable = "assigned-roles";
|
||||
private namesColumn = 'td[data-label="Name"]:visible';
|
||||
|
||||
goToTab() {
|
||||
cy.get(this.tab).click();
|
||||
return this;
|
||||
}
|
||||
|
||||
checkRoles(roleNames: string[]) {
|
||||
cy.getId(this.assignedRolesTable)
|
||||
.get(this.namesColumn)
|
||||
.should((roles) => {
|
||||
for (let index = 0; index < roleNames.length; index++) {
|
||||
const roleName = roleNames[index];
|
||||
expect(roles).to.contain(roleName);
|
||||
}
|
||||
});
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
import KeycloakAdminClient from "keycloak-admin";
|
||||
import UserRepresentation from "keycloak-admin/lib/defs/userRepresentation";
|
||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
|
||||
export default class AdminClient {
|
||||
private client: KeycloakAdminClient;
|
||||
|
@ -24,6 +25,10 @@ export default class AdminClient {
|
|||
await this.client.realms.del({ realm });
|
||||
}
|
||||
|
||||
async createClient(client: ClientRepresentation) {
|
||||
await this.login();
|
||||
await this.client.clients.create(client);
|
||||
}
|
||||
async deleteClient(clientName: string) {
|
||||
await this.login();
|
||||
const client = (
|
||||
|
|
|
@ -10,6 +10,7 @@ import {
|
|||
ButtonVariant,
|
||||
ExpandableSection,
|
||||
FormGroup,
|
||||
PageSection,
|
||||
Split,
|
||||
SplitItem,
|
||||
Text,
|
||||
|
@ -176,257 +177,261 @@ export const AdvancedTab = ({
|
|||
}
|
||||
|
||||
return (
|
||||
<ScrollForm sections={sections}>
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
<Trans i18nKey="clients-help:notBeforeIntro">
|
||||
In order to successfully push setup url on
|
||||
<Link to={`/${realm}/clients/${id}/settings`}>{t("settings")}</Link>
|
||||
tab
|
||||
</Trans>
|
||||
</Text>
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("notBefore")}
|
||||
fieldId="kc-not-before"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:notBefore"
|
||||
forLabel={t("notBefore")}
|
||||
forID="kc-not-before"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="kc-not-before"
|
||||
name="notBefore"
|
||||
isReadOnly
|
||||
value={formatDate()}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
id="setToNow"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
setNotBefore(moment.now() / 1000, "notBeforeSetToNow");
|
||||
}}
|
||||
>
|
||||
{t("setToNow")}
|
||||
</Button>
|
||||
<Button
|
||||
id="clear"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
setNotBefore(0, "notBeforeNowClear");
|
||||
}}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
<Button id="push" variant="secondary" onClick={push}>
|
||||
{t("push")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</>
|
||||
<>
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("nodeReRegistrationTimeout")}
|
||||
fieldId="kc-node-reregistration-timeout"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:nodeReRegistrationTimeout"
|
||||
forLabel={t("nodeReRegistrationTimeout")}
|
||||
forID="nodeReRegistrationTimeout"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Split hasGutter>
|
||||
<SplitItem>
|
||||
<Controller
|
||||
name="nodeReRegistrationTimeout"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Button
|
||||
variant={ButtonVariant.secondary}
|
||||
onClick={() => save()}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</FormGroup>
|
||||
</FormAccess>
|
||||
<PageSection variant="light">
|
||||
<ScrollForm sections={sections}>
|
||||
<>
|
||||
<DeleteNodeConfirm />
|
||||
<AddHostDialog
|
||||
clientId={id!}
|
||||
isOpen={addNodeOpen}
|
||||
onAdded={(node) => {
|
||||
nodes[node] = moment.now() / 1000;
|
||||
refresh();
|
||||
}}
|
||||
onClose={() => setAddNodeOpen(false)}
|
||||
/>
|
||||
<ExpandableSection
|
||||
toggleText={t("registeredClusterNodes")}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
isExpanded={expanded}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
ariaLabelKey="registeredClusterNodes"
|
||||
loader={() =>
|
||||
Promise.resolve(
|
||||
Object.entries(nodes || {}).map((entry) => {
|
||||
return { host: entry[0], registration: entry[1] };
|
||||
})
|
||||
)
|
||||
<Text className="pf-u-py-lg">
|
||||
<Trans i18nKey="clients-help:notBeforeIntro">
|
||||
In order to successfully push setup url on
|
||||
<Link to={`/${realm}/clients/${id}/settings`}>
|
||||
{t("settings")}
|
||||
</Link>
|
||||
tab
|
||||
</Trans>
|
||||
</Text>
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("notBefore")}
|
||||
fieldId="kc-not-before"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:notBefore"
|
||||
forLabel={t("notBefore")}
|
||||
forID="kc-not-before"
|
||||
/>
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
id="testClusterAvailability"
|
||||
onClick={testCluster}
|
||||
variant={ButtonVariant.secondary}
|
||||
isDisabled={Object.keys(nodes).length === 0}
|
||||
>
|
||||
{t("testClusterAvailability")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
id="registerNodeManually"
|
||||
onClick={() => setAddNodeOpen(true)}
|
||||
variant={ButtonVariant.tertiary}
|
||||
>
|
||||
{t("registerNodeManually")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: (node) => {
|
||||
setSelectedNode(node.host);
|
||||
toggleDeleteNodeConfirm();
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
name: "host",
|
||||
displayKey: "clients:nodeHost",
|
||||
},
|
||||
{
|
||||
name: "registration",
|
||||
displayKey: "clients:lastRegistration",
|
||||
cellFormatters: [
|
||||
(value) =>
|
||||
value
|
||||
? moment(parseInt(value.toString()) * 1000).format(
|
||||
"LLL"
|
||||
)
|
||||
: "",
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ExpandableSection>
|
||||
>
|
||||
<TextInput
|
||||
type="text"
|
||||
id="kc-not-before"
|
||||
name="notBefore"
|
||||
isReadOnly
|
||||
value={formatDate()}
|
||||
/>
|
||||
</FormGroup>
|
||||
<ActionGroup>
|
||||
<Button
|
||||
id="setToNow"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
setNotBefore(moment.now() / 1000, "notBeforeSetToNow");
|
||||
}}
|
||||
>
|
||||
{t("setToNow")}
|
||||
</Button>
|
||||
<Button
|
||||
id="clear"
|
||||
variant="tertiary"
|
||||
onClick={() => {
|
||||
setNotBefore(0, "notBeforeNowClear");
|
||||
}}
|
||||
>
|
||||
{t("clear")}
|
||||
</Button>
|
||||
<Button id="push" variant="secondary" onClick={push}>
|
||||
{t("push")}
|
||||
</Button>
|
||||
</ActionGroup>
|
||||
</FormAccess>
|
||||
</>
|
||||
<>
|
||||
<FormAccess role="manage-clients" isHorizontal>
|
||||
<FormGroup
|
||||
label={t("nodeReRegistrationTimeout")}
|
||||
fieldId="kc-node-reregistration-timeout"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:nodeReRegistrationTimeout"
|
||||
forLabel={t("nodeReRegistrationTimeout")}
|
||||
forID="nodeReRegistrationTimeout"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Split hasGutter>
|
||||
<SplitItem>
|
||||
<Controller
|
||||
name="nodeReRegistrationTimeout"
|
||||
defaultValue=""
|
||||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<TimeSelector value={value} onChange={onChange} />
|
||||
)}
|
||||
/>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<Button
|
||||
variant={ButtonVariant.secondary}
|
||||
onClick={() => save()}
|
||||
>
|
||||
{t("common:save")}
|
||||
</Button>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</FormGroup>
|
||||
</FormAccess>
|
||||
<>
|
||||
<DeleteNodeConfirm />
|
||||
<AddHostDialog
|
||||
clientId={id!}
|
||||
isOpen={addNodeOpen}
|
||||
onAdded={(node) => {
|
||||
nodes[node] = moment.now() / 1000;
|
||||
refresh();
|
||||
}}
|
||||
onClose={() => setAddNodeOpen(false)}
|
||||
/>
|
||||
<ExpandableSection
|
||||
toggleText={t("registeredClusterNodes")}
|
||||
onToggle={() => setExpanded(!expanded)}
|
||||
isExpanded={expanded}
|
||||
>
|
||||
<KeycloakDataTable
|
||||
key={key}
|
||||
ariaLabelKey="registeredClusterNodes"
|
||||
loader={() =>
|
||||
Promise.resolve(
|
||||
Object.entries(nodes || {}).map((entry) => {
|
||||
return { host: entry[0], registration: entry[1] };
|
||||
})
|
||||
)
|
||||
}
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
id="testClusterAvailability"
|
||||
onClick={testCluster}
|
||||
variant={ButtonVariant.secondary}
|
||||
isDisabled={Object.keys(nodes).length === 0}
|
||||
>
|
||||
{t("testClusterAvailability")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button
|
||||
id="registerNodeManually"
|
||||
onClick={() => setAddNodeOpen(true)}
|
||||
variant={ButtonVariant.tertiary}
|
||||
>
|
||||
{t("registerNodeManually")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
actions={[
|
||||
{
|
||||
title: t("common:delete"),
|
||||
onRowClick: (node) => {
|
||||
setSelectedNode(node.host);
|
||||
toggleDeleteNodeConfirm();
|
||||
},
|
||||
},
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
name: "host",
|
||||
displayKey: "clients:nodeHost",
|
||||
},
|
||||
{
|
||||
name: "registration",
|
||||
displayKey: "clients:lastRegistration",
|
||||
cellFormatters: [
|
||||
(value) =>
|
||||
value
|
||||
? moment(parseInt(value.toString()) * 1000).format(
|
||||
"LLL"
|
||||
)
|
||||
: "",
|
||||
],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</ExpandableSection>
|
||||
</>
|
||||
</>
|
||||
<>
|
||||
{protocol === openIdConnect && (
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
|
||||
</Text>
|
||||
<FineGrainOpenIdConnect
|
||||
control={control}
|
||||
save={() => save()}
|
||||
reset={() =>
|
||||
convertToFormValues(attributes, "attributes", setValue)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{protocol !== openIdConnect && (
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
{t("clients-help:fineGrainSamlEndpointConfig")}
|
||||
</Text>
|
||||
<FineGrainSamlEndpointConfig
|
||||
control={control}
|
||||
save={() => save()}
|
||||
reset={() =>
|
||||
convertToFormValues(attributes, "attributes", setValue)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</>
|
||||
<>
|
||||
{protocol === openIdConnect && (
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
{t("clients-help:fineGrainOpenIdConnectConfiguration")}
|
||||
{t("clients-help:openIdConnectCompatibilityModes")}
|
||||
</Text>
|
||||
<FineGrainOpenIdConnect
|
||||
<OpenIdConnectCompatibilityModes
|
||||
control={control}
|
||||
save={() => save()}
|
||||
reset={() =>
|
||||
convertToFormValues(attributes, "attributes", setValue)
|
||||
resetFields(["exclude-session-state-from-auth-response"])
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{protocol !== openIdConnect && (
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
{t("clients-help:fineGrainSamlEndpointConfig")}
|
||||
</Text>
|
||||
<FineGrainSamlEndpointConfig
|
||||
control={control}
|
||||
save={() => save()}
|
||||
reset={() =>
|
||||
convertToFormValues(attributes, "attributes", setValue)
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
{protocol === openIdConnect && (
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
{t("clients-help:openIdConnectCompatibilityModes")}
|
||||
{t("clients-help:advancedSettings" + toUpperCase(protocol!))}
|
||||
</Text>
|
||||
<OpenIdConnectCompatibilityModes
|
||||
<AdvancedSettings
|
||||
protocol={protocol}
|
||||
control={control}
|
||||
save={() => save()}
|
||||
reset={() =>
|
||||
resetFields(["exclude-session-state-from-auth-response"])
|
||||
}
|
||||
reset={() => {
|
||||
resetFields([
|
||||
"saml-assertion-lifespan",
|
||||
"access-token-lifespan",
|
||||
"tls-client-certificate-bound-access-tokens",
|
||||
"pkce-code-challenge-method",
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
{t("clients-help:advancedSettings" + toUpperCase(protocol!))}
|
||||
</Text>
|
||||
<AdvancedSettings
|
||||
protocol={protocol}
|
||||
control={control}
|
||||
save={() => save()}
|
||||
reset={() => {
|
||||
resetFields([
|
||||
"saml-assertion-lifespan",
|
||||
"access-token-lifespan",
|
||||
"tls-client-certificate-bound-access-tokens",
|
||||
"pkce-code-challenge-method",
|
||||
]);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
{t("clients-help:authenticationOverrides")}
|
||||
</Text>
|
||||
<AuthenticationOverrides
|
||||
protocol={protocol}
|
||||
control={control}
|
||||
save={() => save()}
|
||||
reset={() => {
|
||||
setValue(
|
||||
"authenticationFlowBindingOverrides.browser",
|
||||
authenticationFlowBindingOverrides?.browser
|
||||
);
|
||||
setValue(
|
||||
"authenticationFlowBindingOverrides.direct_grant",
|
||||
authenticationFlowBindingOverrides?.direct_grant
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</ScrollForm>
|
||||
<>
|
||||
<Text className="pf-u-py-lg">
|
||||
{t("clients-help:authenticationOverrides")}
|
||||
</Text>
|
||||
<AuthenticationOverrides
|
||||
protocol={protocol}
|
||||
control={control}
|
||||
save={() => save()}
|
||||
reset={() => {
|
||||
setValue(
|
||||
"authenticationFlowBindingOverrides.browser",
|
||||
authenticationFlowBindingOverrides?.browser
|
||||
);
|
||||
setValue(
|
||||
"authenticationFlowBindingOverrides.direct_grant",
|
||||
authenticationFlowBindingOverrides?.direct_grant
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
</ScrollForm>
|
||||
</PageSection>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -44,6 +44,7 @@ export const CapabilityConfig = ({
|
|||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
data-testid="authentication"
|
||||
id="kc-authentication"
|
||||
name="publicClient"
|
||||
label={t("common:on")}
|
||||
|
@ -65,6 +66,7 @@ export const CapabilityConfig = ({
|
|||
control={control}
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
data-testid="authorization"
|
||||
id="kc-authorization"
|
||||
name="authorizationServicesEnabled"
|
||||
label={t("common:on")}
|
||||
|
@ -95,6 +97,7 @@ export const CapabilityConfig = ({
|
|||
render={({ onChange, value }) => (
|
||||
<InputGroup>
|
||||
<Checkbox
|
||||
data-testid="standard"
|
||||
label={t("standardFlow")}
|
||||
id="kc-flow-standard"
|
||||
name="standardFlowEnabled"
|
||||
|
@ -118,6 +121,7 @@ export const CapabilityConfig = ({
|
|||
render={({ onChange, value }) => (
|
||||
<InputGroup>
|
||||
<Checkbox
|
||||
data-testid="direct"
|
||||
label={t("directAccess")}
|
||||
id="kc-flow-direct"
|
||||
name="directAccessGrantsEnabled"
|
||||
|
@ -141,6 +145,7 @@ export const CapabilityConfig = ({
|
|||
render={({ onChange, value }) => (
|
||||
<InputGroup>
|
||||
<Checkbox
|
||||
data-testid="implicit"
|
||||
label={t("implicitFlow")}
|
||||
id="kc-flow-implicit"
|
||||
name="implicitFlowEnabled"
|
||||
|
@ -164,6 +169,7 @@ export const CapabilityConfig = ({
|
|||
render={({ onChange, value }) => (
|
||||
<InputGroup>
|
||||
<Checkbox
|
||||
data-testid="service-account"
|
||||
label={t("serviceAccount")}
|
||||
id="kc-flow-service-account"
|
||||
name="serviceAccountsEnabled"
|
||||
|
@ -207,6 +213,7 @@ export const CapabilityConfig = ({
|
|||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
data-testid="encrypt"
|
||||
id="kc-encrypt"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
|
@ -233,6 +240,7 @@ export const CapabilityConfig = ({
|
|||
defaultValue="false"
|
||||
render={({ onChange, value }) => (
|
||||
<Switch
|
||||
data-testid="client-signature"
|
||||
id="kc-client-signature"
|
||||
label={t("common:on")}
|
||||
labelOff={t("common:off")}
|
||||
|
|
|
@ -105,6 +105,14 @@
|
|||
"directAccess": "Direct access",
|
||||
"serviceAccount": "Service account roles",
|
||||
"enableServiceAccount": "Enable service account roles",
|
||||
"assignRolesTo": "Assign roles to {{client}} account",
|
||||
"searchByRoleName": "Search by role name",
|
||||
"filterByOrigin": "Filter by Origin",
|
||||
"realmRoles": "Realm roles",
|
||||
"clients": "Clients",
|
||||
"assign": "Assign",
|
||||
"roleMappingUpdatedSuccess": "Role mapping updated",
|
||||
"roleMappingUpdatedError": "Could not update role mapping {{error}}",
|
||||
"displayOnClient": "Display client on screen",
|
||||
"consentScreenText": "Client consent screen text",
|
||||
"loginSettings": "Login settings",
|
||||
|
|
|
@ -9,6 +9,7 @@ import {
|
|||
FormGroup,
|
||||
Grid,
|
||||
GridItem,
|
||||
PageSection,
|
||||
Select,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
|
@ -253,92 +254,95 @@ export const EvaluateScopes = ({ clientId, protocol }: EvaluateScopesProps) => {
|
|||
|
||||
return (
|
||||
<>
|
||||
<TextContent className="keycloak__scopes_evaluate__intro">
|
||||
<Text>
|
||||
<QuestionCircleIcon /> {t("clients-help:evaluateExplain")}
|
||||
</Text>
|
||||
</TextContent>
|
||||
<Form isHorizontal>
|
||||
<FormGroup
|
||||
label={t("scopeParameter")}
|
||||
fieldId="scopeParameter"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:scopeParameter"
|
||||
forLabel={t("scopeParameter")}
|
||||
forID="scopeParameter"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Split hasGutter>
|
||||
<SplitItem isFilled>
|
||||
<Select
|
||||
toggleId="scopeParameter"
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
typeAheadAriaLabel={t("scopeParameter")}
|
||||
onToggle={() => setIsScopeOpen(!isScopeOpen)}
|
||||
isOpen={isScopeOpen}
|
||||
selections={selected}
|
||||
onSelect={(_, value) => {
|
||||
const option = value as string;
|
||||
if (selected.includes(option)) {
|
||||
if (option !== prefix) {
|
||||
setSelected(selected.filter((item) => item !== option));
|
||||
<PageSection variant="light">
|
||||
<TextContent className="keycloak__scopes_evaluate__intro">
|
||||
<Text>
|
||||
<QuestionCircleIcon /> {t("clients-help:evaluateExplain")}
|
||||
</Text>
|
||||
</TextContent>
|
||||
<Form isHorizontal>
|
||||
<FormGroup
|
||||
label={t("scopeParameter")}
|
||||
fieldId="scopeParameter"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:scopeParameter"
|
||||
forLabel={t("scopeParameter")}
|
||||
forID="scopeParameter"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Split hasGutter>
|
||||
<SplitItem isFilled>
|
||||
<Select
|
||||
toggleId="scopeParameter"
|
||||
variant={SelectVariant.typeaheadMulti}
|
||||
typeAheadAriaLabel={t("scopeParameter")}
|
||||
onToggle={() => setIsScopeOpen(!isScopeOpen)}
|
||||
isOpen={isScopeOpen}
|
||||
selections={selected}
|
||||
onSelect={(_, value) => {
|
||||
const option = value as string;
|
||||
if (selected.includes(option)) {
|
||||
if (option !== prefix) {
|
||||
setSelected(selected.filter((item) => item !== option));
|
||||
}
|
||||
} else {
|
||||
setSelected([...selected, option]);
|
||||
}
|
||||
} else {
|
||||
setSelected([...selected, option]);
|
||||
}
|
||||
}}
|
||||
aria-labelledby={t("scopeParameter")}
|
||||
placeholderText={t("scopeParameterPlaceholder")}
|
||||
>
|
||||
{selectableScopes.map((option, index) => (
|
||||
<SelectOption key={index} value={option.name} />
|
||||
))}
|
||||
</Select>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<ClipboardCopy className="keycloak__scopes_evaluate__clipboard-copy">
|
||||
{selected.join(" ")}
|
||||
</ClipboardCopy>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("user")}
|
||||
fieldId="user"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:user"
|
||||
forLabel={t("user")}
|
||||
forID="user"
|
||||
}}
|
||||
aria-labelledby={t("scopeParameter")}
|
||||
placeholderText={t("scopeParameterPlaceholder")}
|
||||
>
|
||||
{selectableScopes.map((option, index) => (
|
||||
<SelectOption key={index} value={option.name} />
|
||||
))}
|
||||
</Select>
|
||||
</SplitItem>
|
||||
<SplitItem>
|
||||
<ClipboardCopy className="keycloak__scopes_evaluate__clipboard-copy">
|
||||
{selected.join(" ")}
|
||||
</ClipboardCopy>
|
||||
</SplitItem>
|
||||
</Split>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
label={t("user")}
|
||||
fieldId="user"
|
||||
labelIcon={
|
||||
<HelpItem
|
||||
helpText="clients-help:user"
|
||||
forLabel={t("user")}
|
||||
forID="user"
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
toggleId="user"
|
||||
variant={SelectVariant.typeahead}
|
||||
typeAheadAriaLabel={t("user")}
|
||||
onToggle={() => setIsUserOpen(!isUserOpen)}
|
||||
onFilter={(e) => {
|
||||
const value = e?.target.value || "";
|
||||
setUserSearch(value);
|
||||
return userItems;
|
||||
}}
|
||||
onClear={() => {
|
||||
setUser(undefined);
|
||||
setUserSearch("");
|
||||
}}
|
||||
selections={[user]}
|
||||
onSelect={(_, value) => {
|
||||
setUser(value as UserRepresentation);
|
||||
setUserSearch("");
|
||||
setIsUserOpen(false);
|
||||
}}
|
||||
isOpen={isUserOpen}
|
||||
/>
|
||||
}
|
||||
>
|
||||
<Select
|
||||
toggleId="user"
|
||||
variant={SelectVariant.typeahead}
|
||||
typeAheadAriaLabel={t("user")}
|
||||
onToggle={() => setIsUserOpen(!isUserOpen)}
|
||||
onFilter={(e) => {
|
||||
const value = e?.target.value || "";
|
||||
setUserSearch(value);
|
||||
return userItems;
|
||||
}}
|
||||
onClear={() => {
|
||||
setUser(undefined);
|
||||
setUserSearch("");
|
||||
}}
|
||||
selections={[user]}
|
||||
onSelect={(_, value) => {
|
||||
setUser(value as UserRepresentation);
|
||||
setUserSearch("");
|
||||
setIsUserOpen(false);
|
||||
}}
|
||||
isOpen={isUserOpen}
|
||||
/>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</FormGroup>
|
||||
</Form>
|
||||
</PageSection>
|
||||
|
||||
<Grid hasGutter className="keycloak__scopes_evaluate__tabs">
|
||||
<GridItem span={8}>
|
||||
<TabContent
|
||||
|
|
269
src/clients/service-account/AddServiceAccountModal.tsx
Normal file
269
src/clients/service-account/AddServiceAccountModal.tsx
Normal file
|
@ -0,0 +1,269 @@
|
|||
import React, { useEffect, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useErrorHandler } from "react-error-boundary";
|
||||
import _ from "lodash";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Chip,
|
||||
ChipGroup,
|
||||
Divider,
|
||||
Modal,
|
||||
ModalVariant,
|
||||
Select,
|
||||
SelectGroup,
|
||||
SelectOption,
|
||||
SelectVariant,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
|
||||
import {
|
||||
asyncStateFetch,
|
||||
useAdminClient,
|
||||
} from "../../context/auth/AdminClient";
|
||||
import ClientRepresentation from "keycloak-admin/lib/defs/clientRepresentation";
|
||||
import { FilterIcon } from "@patternfly/react-icons";
|
||||
import { Row, ServiceRole } from "./ServiceAccount";
|
||||
|
||||
type AddServiceAccountModalProps = {
|
||||
clientId: string;
|
||||
serviceAccountId: string;
|
||||
onAssign: (rows: Row[]) => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
type ClientRole = ClientRepresentation & {
|
||||
numberOfRoles: number;
|
||||
};
|
||||
|
||||
const realmRole = {
|
||||
name: "realmRoles",
|
||||
} as ClientRepresentation;
|
||||
|
||||
export const AddServiceAccountModal = ({
|
||||
clientId,
|
||||
serviceAccountId,
|
||||
onAssign,
|
||||
onClose,
|
||||
}: AddServiceAccountModalProps) => {
|
||||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const errorHandler = useErrorHandler();
|
||||
|
||||
const [clients, setClients] = useState<ClientRole[]>([]);
|
||||
const [searchToggle, setSearchToggle] = useState(false);
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const [selectedClients, setSelectedClients] = useState<ClientRole[]>([]);
|
||||
const [selectedRows, setSelectedRows] = useState<Row[]>();
|
||||
|
||||
useEffect(
|
||||
() =>
|
||||
asyncStateFetch(
|
||||
async () => {
|
||||
const clients = await adminClient.clients.find();
|
||||
return (
|
||||
await Promise.all(
|
||||
clients.map(async (client) => {
|
||||
const roles = await adminClient.users.listAvailableClientRoleMappings(
|
||||
{
|
||||
id: serviceAccountId,
|
||||
clientUniqueId: client.id!,
|
||||
}
|
||||
);
|
||||
return {
|
||||
roles,
|
||||
client,
|
||||
};
|
||||
})
|
||||
)
|
||||
)
|
||||
.flat()
|
||||
.filter((row) => row.roles.length !== 0)
|
||||
.map((row) => {
|
||||
return { ...row.client, numberOfRoles: row.roles.length };
|
||||
});
|
||||
},
|
||||
(clients) => {
|
||||
setClients(clients);
|
||||
},
|
||||
errorHandler
|
||||
),
|
||||
[]
|
||||
);
|
||||
|
||||
useEffect(refresh, [searchToggle]);
|
||||
|
||||
const removeClient = (client: ClientRole) => {
|
||||
setSelectedClients(selectedClients.filter((item) => item.id !== client.id));
|
||||
};
|
||||
|
||||
const loader = async () => {
|
||||
const realmRolesSelected = _.findIndex(
|
||||
selectedClients,
|
||||
(client) => client.name === "realmRoles"
|
||||
);
|
||||
let selected = selectedClients;
|
||||
if (realmRolesSelected !== -1) {
|
||||
selected = selectedClients.filter(
|
||||
(client) => client.name !== "realmRoles"
|
||||
);
|
||||
}
|
||||
const realmRoles = (
|
||||
await adminClient.users.listAvailableRealmRoleMappings({
|
||||
id: serviceAccountId,
|
||||
})
|
||||
).map((role) => {
|
||||
return {
|
||||
role,
|
||||
client: undefined,
|
||||
};
|
||||
});
|
||||
|
||||
const allClients =
|
||||
selectedClients.length !== 0
|
||||
? selected
|
||||
: await adminClient.clients.find();
|
||||
|
||||
const roles = (
|
||||
await Promise.all(
|
||||
allClients.map(async (client) =>
|
||||
(
|
||||
await adminClient.users.listAvailableClientRoleMappings({
|
||||
id: serviceAccountId,
|
||||
clientUniqueId: client.id!,
|
||||
})
|
||||
).map((role) => {
|
||||
return {
|
||||
role,
|
||||
client,
|
||||
};
|
||||
})
|
||||
)
|
||||
)
|
||||
).flat();
|
||||
|
||||
return [
|
||||
...(realmRolesSelected !== -1 || selected.length === 0 ? realmRoles : []),
|
||||
...roles,
|
||||
];
|
||||
};
|
||||
|
||||
const createSelectGroup = (clients: ClientRepresentation[]) => [
|
||||
<SelectGroup key="role" label={t("realmRoles")}>
|
||||
<SelectOption key="realmRoles" value={realmRole}>
|
||||
{t("realmRoles")}
|
||||
</SelectOption>
|
||||
</SelectGroup>,
|
||||
<Divider key="divider" />,
|
||||
<SelectGroup key="group" label={t("clients")}>
|
||||
{clients.map((client) => (
|
||||
<SelectOption key={client.id} value={client}>
|
||||
{client.clientId}
|
||||
</SelectOption>
|
||||
))}
|
||||
</SelectGroup>,
|
||||
];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
variant={ModalVariant.large}
|
||||
title={t("assignRolesTo", { client: clientId })}
|
||||
isOpen={true}
|
||||
onClose={onClose}
|
||||
actions={[
|
||||
<Button
|
||||
data-testid="assign"
|
||||
key="confirm"
|
||||
isDisabled={selectedRows?.length === 0}
|
||||
variant="primary"
|
||||
onClick={() => {
|
||||
onAssign(selectedRows!);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{t("assign")}
|
||||
</Button>,
|
||||
<Button
|
||||
data-testid="cancel"
|
||||
key="cancel"
|
||||
variant="secondary"
|
||||
onClick={onClose}
|
||||
>
|
||||
{t("common:cancel")}
|
||||
</Button>,
|
||||
]}
|
||||
>
|
||||
<Select
|
||||
toggleId="role"
|
||||
onToggle={() => setSearchToggle(!searchToggle)}
|
||||
isOpen={searchToggle}
|
||||
variant={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
|
||||
key={key}
|
||||
onSelect={(rows) => setSelectedRows([...rows])}
|
||||
searchPlaceholderKey="clients:searchByRoleName"
|
||||
canSelectAll={false}
|
||||
loader={loader}
|
||||
ariaLabelKey="clients:roles"
|
||||
columns={[
|
||||
{
|
||||
name: "name",
|
||||
cellRenderer: ServiceRole,
|
||||
},
|
||||
{
|
||||
name: "role.description",
|
||||
displayKey: t("description"),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
|
@ -1,25 +1,50 @@
|
|||
import React, { useContext, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Badge, Button, Checkbox, ToolbarItem } from "@patternfly/react-core";
|
||||
import {
|
||||
AlertVariant,
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
ToolbarItem,
|
||||
} from "@patternfly/react-core";
|
||||
|
||||
import RoleRepresentation from "keycloak-admin/lib/defs/roleRepresentation";
|
||||
import RoleRepresentation, {
|
||||
RoleMappingPayload,
|
||||
} 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 { AddServiceAccountModal } from "./AddServiceAccountModal";
|
||||
|
||||
import "./service-account.css";
|
||||
import { useAlerts } from "../../components/alert/Alerts";
|
||||
|
||||
type ServiceAccountProps = {
|
||||
clientId: string;
|
||||
};
|
||||
|
||||
type Row = {
|
||||
client: ClientRepresentation;
|
||||
role: CompositeRole;
|
||||
export type Row = {
|
||||
client?: ClientRepresentation;
|
||||
role: CompositeRole | RoleRepresentation;
|
||||
};
|
||||
|
||||
export const ServiceRole = ({ role, client }: Row) => (
|
||||
<>
|
||||
{client && (
|
||||
<Badge
|
||||
key={`${client.id}-${role.id}`}
|
||||
isRead
|
||||
className="keycloak-admin--service-account__client-name"
|
||||
>
|
||||
{client.clientId}
|
||||
</Badge>
|
||||
)}
|
||||
{role.name}
|
||||
</>
|
||||
);
|
||||
|
||||
type CompositeRole = RoleRepresentation & {
|
||||
parent: RoleRepresentation;
|
||||
};
|
||||
|
@ -28,13 +53,20 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
const { t } = useTranslation("clients");
|
||||
const adminClient = useAdminClient();
|
||||
const { realm } = useContext(RealmContext);
|
||||
const { addAlert } = useAlerts();
|
||||
|
||||
const [key, setKey] = useState(0);
|
||||
const refresh = () => setKey(new Date().getTime());
|
||||
|
||||
const [hide, setHide] = useState(false);
|
||||
const [serviceAccountId, setServiceAccountId] = useState("");
|
||||
const [showAssign, setShowAssign] = useState(false);
|
||||
|
||||
const loader = async () => {
|
||||
const serviceAccount = await adminClient.clients.getServiceAccountUser({
|
||||
id: clientId,
|
||||
});
|
||||
setServiceAccountId(serviceAccount.id!);
|
||||
const effectiveRoles = await adminClient.users.listCompositeRealmRoleMappings(
|
||||
{ id: serviceAccount.id! }
|
||||
);
|
||||
|
@ -65,7 +97,6 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
};
|
||||
|
||||
const clientRolesFlat = clientRoles.map((row) => row.roles).flat();
|
||||
console.log(clientRolesFlat);
|
||||
|
||||
const addInherentData = await (async () =>
|
||||
Promise.all(
|
||||
|
@ -99,59 +130,90 @@ export const ServiceAccount = ({ clientId }: ServiceAccountProps) => {
|
|||
});
|
||||
};
|
||||
|
||||
const RoleLink = ({ role, client }: Row) => (
|
||||
const assignRoles = async (rows: Row[]) => {
|
||||
try {
|
||||
const realmRoles = rows
|
||||
.filter((row) => row.client === undefined)
|
||||
.map((row) => row.role as RoleMappingPayload)
|
||||
.flat();
|
||||
adminClient.users.addRealmRoleMappings({
|
||||
id: serviceAccountId,
|
||||
roles: realmRoles,
|
||||
});
|
||||
await Promise.all(
|
||||
rows
|
||||
.filter((row) => row.client !== undefined)
|
||||
.map((row) =>
|
||||
adminClient.users.addClientRoleMappings({
|
||||
id: serviceAccountId,
|
||||
clientUniqueId: row.client!.id!,
|
||||
roles: [row.role as RoleMappingPayload],
|
||||
})
|
||||
)
|
||||
);
|
||||
addAlert(t("roleMappingUpdatedSuccess"), AlertVariant.success);
|
||||
refresh();
|
||||
} catch (error) {
|
||||
addAlert(
|
||||
t("roleMappingUpdatedError", {
|
||||
error: error.response?.data?.errorMessage || error,
|
||||
}),
|
||||
AlertVariant.danger
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<>
|
||||
{client && (
|
||||
<Badge
|
||||
key={client.id}
|
||||
isRead
|
||||
className="keycloak-admin--service-account__client-name"
|
||||
>
|
||||
{client.clientId}
|
||||
</Badge>
|
||||
{showAssign && (
|
||||
<AddServiceAccountModal
|
||||
clientId={clientId}
|
||||
serviceAccountId={serviceAccountId}
|
||||
onAssign={assignRoles}
|
||||
onClose={() => setShowAssign(false)}
|
||||
/>
|
||||
)}
|
||||
{role.name}
|
||||
<KeycloakDataTable
|
||||
data-testid="assigned-roles"
|
||||
key={key}
|
||||
loader={loader}
|
||||
onSelect={() => {}}
|
||||
searchPlaceholderKey="clients:searchByName"
|
||||
ariaLabelKey="clients:clientScopeList"
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
label={t("hideInheritedRoles")}
|
||||
id="hideInheritedRoles"
|
||||
isChecked={hide}
|
||||
onChange={setHide}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button onClick={() => setShowAssign(true)}>
|
||||
{t("assignRole")}
|
||||
</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
columns={[
|
||||
{
|
||||
name: "role.name",
|
||||
displayKey: t("name"),
|
||||
cellRenderer: ServiceRole,
|
||||
},
|
||||
{
|
||||
name: "role.parent.name",
|
||||
displayKey: t("inherentFrom"),
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
{
|
||||
name: "role.description",
|
||||
displayKey: t("description"),
|
||||
cellFormatters: [emptyFormatter()],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
||||
return (
|
||||
<KeycloakDataTable
|
||||
loader={loader}
|
||||
onSelect={() => {}}
|
||||
searchPlaceholderKey="clients:searchByName"
|
||||
ariaLabelKey="clients:clientScopeList"
|
||||
toolbarItem={
|
||||
<>
|
||||
<ToolbarItem>
|
||||
<Checkbox
|
||||
label={t("hideInheritedRoles")}
|
||||
id="hideInheritedRoles"
|
||||
isChecked={hide}
|
||||
onChange={setHide}
|
||||
/>
|
||||
</ToolbarItem>
|
||||
<ToolbarItem>
|
||||
<Button>{t("assignRole")}</Button>
|
||||
</ToolbarItem>
|
||||
</>
|
||||
}
|
||||
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()],
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -43,10 +43,12 @@ function DataTable<T>({
|
|||
ariaLabelKey,
|
||||
onSelect,
|
||||
canSelectAll,
|
||||
...props
|
||||
}: DataTableProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
return (
|
||||
<Table
|
||||
{...props}
|
||||
variant={TableVariant.compact}
|
||||
onSelect={
|
||||
onSelect
|
||||
|
@ -130,6 +132,7 @@ export function KeycloakDataTable<T>({
|
|||
searchTypeComponent,
|
||||
toolbarItem,
|
||||
emptyState,
|
||||
...props
|
||||
}: DataListProps<T>) {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState<T[]>([]);
|
||||
|
@ -281,6 +284,7 @@ export function KeycloakDataTable<T>({
|
|||
>
|
||||
{!loading && (filteredData || rows).length > 0 && (
|
||||
<DataTable
|
||||
{...props}
|
||||
canSelectAll={canSelectAll}
|
||||
onSelect={onSelect ? _onSelect : undefined}
|
||||
actions={convertAction()}
|
||||
|
@ -290,14 +294,17 @@ export function KeycloakDataTable<T>({
|
|||
ariaLabelKey={ariaLabelKey}
|
||||
/>
|
||||
)}
|
||||
{!loading && rows.length === 0 && search !== "" && (
|
||||
<ListEmptyState
|
||||
hasIcon={true}
|
||||
isSearchVariant={true}
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noSearchResultsInstructions")}
|
||||
/>
|
||||
)}
|
||||
{!loading &&
|
||||
rows.length === 0 &&
|
||||
search !== "" &&
|
||||
searchPlaceholderKey && (
|
||||
<ListEmptyState
|
||||
hasIcon={true}
|
||||
isSearchVariant={true}
|
||||
message={t("noSearchResults")}
|
||||
instructions={t("noSearchResultsInstructions")}
|
||||
/>
|
||||
)}
|
||||
{loading && <Loading />}
|
||||
</PaginatingTableToolbar>
|
||||
)}
|
||||
|
|
Loading…
Reference in a new issue