Realm settings(Client policies): Update client-scopes condition to match new design (#1575)

This commit is contained in:
Jenny 2021-11-23 09:59:04 -05:00 committed by GitHub
parent d97ffecc29
commit 999b502d44
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 523 additions and 69 deletions

View file

@ -118,7 +118,7 @@ describe("Realm settings tests", () => {
masthead.checkNotificationMessage("Realm successfully updated"); masthead.checkNotificationMessage("Realm successfully updated");
}); });
it.skip("Go to login tab", () => { it("Go to login tab", () => {
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
cy.findByTestId("rs-login-tab").click(); cy.findByTestId("rs-login-tab").click();
realmSettingsPage.toggleSwitch(realmSettingsPage.userRegSwitch); realmSettingsPage.toggleSwitch(realmSettingsPage.userRegSwitch);
@ -126,7 +126,7 @@ describe("Realm settings tests", () => {
realmSettingsPage.toggleSwitch(realmSettingsPage.rememberMeSwitch); realmSettingsPage.toggleSwitch(realmSettingsPage.rememberMeSwitch);
}); });
it.skip("Check login tab values", () => { it("Check login tab values", () => {
sidebarPage.goToRealmSettings(); sidebarPage.goToRealmSettings();
cy.findByTestId("rs-login-tab").click(); cy.findByTestId("rs-login-tab").click();
@ -605,20 +605,32 @@ describe("Realm settings tests", () => {
realmSettingsPage.shouldCancelAddingCondition(); realmSettingsPage.shouldCancelAddingCondition();
}); });
it("Should add a new condition to a client profile", () => { it("Should add a new client-roles condition to a client profile", () => {
realmSettingsPage.shouldAddCondition(); realmSettingsPage.shouldAddClientRolesCondition();
}); });
it("Should edit the condition of a client profile", () => { it("Should add a new client-scopes condition to a client profile", () => {
realmSettingsPage.shouldEditCondition(); realmSettingsPage.shouldAddClientScopesCondition();
});
it("Should edit the client-roles condition of a client profile", () => {
realmSettingsPage.shouldEditClientRolesCondition();
});
it("Should edit the client-scopes condition of a client profile", () => {
realmSettingsPage.shouldEditClientScopesCondition();
}); });
it("Should cancel deleting condition from a client profile", () => { it("Should cancel deleting condition from a client profile", () => {
realmSettingsPage.shouldCancelDeletingCondition(); realmSettingsPage.shouldCancelDeletingCondition();
}); });
it("Should delete condition from a client profile", () => { it("Should delete client-roles condition from a client profile", () => {
realmSettingsPage.shouldDeleteCondition(); realmSettingsPage.shouldDeleteClientRolesCondition();
});
it("Should delete client-scopes condition from a client profile", () => {
realmSettingsPage.shouldDeleteClientScopesCondition();
}); });
it("Check cancelling the client policy deletion", () => { it("Check cancelling the client policy deletion", () => {

View file

@ -197,6 +197,8 @@ export default class RealmSettingsPage {
private addConditionCancelBtn = "addCondition-cancelBtn"; private addConditionCancelBtn = "addCondition-cancelBtn";
private addConditionSaveBtn = "addCondition-saveBtn"; private addConditionSaveBtn = "addCondition-saveBtn";
private conditionTypeLink = "condition-type-link"; private conditionTypeLink = "condition-type-link";
private clientRolesConditionLink = "client-roles-condition-link";
private clientScopesConditionLink = "client-scopes-condition-link";
private eventListenersFormLabel = ".pf-c-form__label-text"; private eventListenersFormLabel = ".pf-c-form__label-text";
private eventListenersDrpDwn = ".pf-c-select.kc_eventListeners_select"; private eventListenersDrpDwn = ".pf-c-select.kc_eventListeners_select";
private eventListenersSaveBtn = "saveEventListenerBtn"; private eventListenersSaveBtn = "saveEventListenerBtn";
@ -208,6 +210,9 @@ export default class RealmSettingsPage {
".pf-c-button.pf-c-select__toggle-button.pf-m-plain"; ".pf-c-button.pf-c-select__toggle-button.pf-m-plain";
private eventListenerRemove = '[data-ouia-component-id="Remove"]'; private eventListenerRemove = '[data-ouia-component-id="Remove"]';
private roleSelect = ".pf-c-select.kc-role-select"; private roleSelect = ".pf-c-select.kc-role-select";
private selectScopeButton = "select-scope-button";
private deleteClientRolesCondition = "delete-client-roles-condition";
private deleteClientScopesCondition = "delete-client-scopes-condition";
selectLoginThemeType(themeType: string) { selectLoginThemeType(themeType: string) {
cy.get(this.selectLoginTheme).click(); cy.get(this.selectLoginTheme).click();
@ -966,7 +971,7 @@ export default class RealmSettingsPage {
); );
} }
shouldAddCondition() { shouldAddClientRolesCondition() {
cy.get(this.clientPolicy).click(); cy.get(this.clientPolicy).click();
cy.findByTestId(this.addCondition).click(); cy.findByTestId(this.addCondition).click();
cy.get(this.addConditionDrpDwn).click(); cy.get(this.addConditionDrpDwn).click();
@ -989,10 +994,37 @@ export default class RealmSettingsPage {
cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles"); cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles");
} }
shouldEditCondition() { addClientScopes() {
cy.findByTestId(this.selectScopeButton).click();
cy.get(".pf-c-table__check > input[name=checkrow0]").click();
cy.get(".pf-c-table__check > input[name=checkrow1]").click();
cy.get(".pf-c-table__check > input[name=checkrow2]").click();
cy.findByTestId("modalConfirm").contains("Add").click();
}
shouldAddClientScopesCondition() {
cy.get(this.clientPolicy).click();
cy.findByTestId(this.addCondition).click();
cy.get(this.addConditionDrpDwn).click();
cy.findByTestId(this.addConditionDrpDwnOption)
.contains("client-scopes")
.click();
this.addClientScopes();
cy.findByTestId(this.addConditionSaveBtn).click();
cy.get(this.alertMessage).should(
"be.visible",
"Success! Condition created successfully"
);
cy.get('ul[class*="pf-c-data-list"]').contains("client-scopes");
}
shouldEditClientRolesCondition() {
cy.get(this.clientPolicy).click(); cy.get(this.clientPolicy).click();
cy.findByTestId(this.conditionTypeLink).contains("client-roles").click(); cy.findByTestId(this.clientRolesConditionLink).click();
cy.get(this.roleSelect).click(); cy.get(this.roleSelect).click();
cy.get(this.roleSelect).contains("create-client").click(); cy.get(this.roleSelect).contains("create-client").click();
@ -1006,26 +1038,53 @@ export default class RealmSettingsPage {
); );
} }
shouldEditClientScopesCondition() {
cy.get(this.clientPolicy).click();
cy.findByTestId(this.clientScopesConditionLink).click();
cy.wait(200);
this.addClientScopes();
cy.findByTestId(this.addConditionSaveBtn).click();
cy.get(this.alertMessage).should(
"be.visible",
"Success! Condition updated successfully"
);
}
shouldCancelDeletingCondition() { shouldCancelDeletingCondition() {
cy.get(this.clientPolicy).click(); cy.get(this.clientPolicy).click();
cy.get('svg[class*="kc-conditionType-trash-icon"]').click(); cy.findByTestId(this.deleteClientRolesCondition).click();
cy.get(this.deleteDialogTitle).contains("Delete condition?"); cy.get(this.deleteDialogTitle).contains("Delete condition?");
cy.get(this.deleteDialogBodyText).contains( cy.get(this.deleteDialogBodyText).contains(
"This action will permanently delete client-roles. This cannot be undone." "This action will permanently delete client-roles. This cannot be undone."
); );
cy.findByTestId("modalConfirm").contains("Delete"); cy.findByTestId("modalConfirm").contains("Delete");
cy.get(this.deleteDialogCancelBtn).contains("Cancel").click(); cy.get(this.deleteDialogCancelBtn).contains("Cancel").click();
cy.get('ul[class*="pf-c-data-list"]').should("have.text", "client-roles"); cy.get('ul[class*="pf-c-data-list"]').contains("client-roles");
} }
shouldDeleteCondition() { shouldDeleteClientRolesCondition() {
cy.get(this.clientPolicy).click(); cy.get(this.clientPolicy).click();
cy.get('svg[class*="kc-conditionType-trash-icon"]').click(); cy.findByTestId(this.deleteClientRolesCondition).click();
cy.get(this.deleteDialogTitle).contains("Delete condition?"); cy.get(this.deleteDialogTitle).contains("Delete condition?");
cy.get(this.deleteDialogBodyText).contains( cy.get(this.deleteDialogBodyText).contains(
"This action will permanently delete client-roles. This cannot be undone." "This action will permanently delete client-roles. This cannot be undone."
); );
cy.findByTestId("modalConfirm").contains("Delete").click(); cy.findByTestId("modalConfirm").contains("Delete").click();
cy.get('ul[class*="pf-c-data-list"]').contains("client-scopes");
}
shouldDeleteClientScopesCondition() {
cy.get(this.clientPolicy).click();
cy.findByTestId(this.deleteClientScopesCondition).click();
cy.get(this.deleteDialogTitle).contains("Delete condition?");
cy.get(this.deleteDialogBodyText).contains(
"This action will permanently delete client-scopes. This cannot be undone."
);
cy.findByTestId("modalConfirm").contains("Delete").click();
cy.get('h6[class*="kc-emptyConditions"]').should( cy.get('h6[class*="kc-emptyConditions"]').should(
"have.text", "have.text",
"No conditions configured" "No conditions configured"

View file

@ -19,7 +19,7 @@ import { useAlerts } from "../components/alert/Alerts";
import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../components/table-toolbar/KeycloakDataTable";
import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog"; import { useConfirmDialog } from "../components/confirm-dialog/ConfirmDialog";
import { useRealm } from "../context/realm-context/RealmContext"; import { useRealm } from "../context/realm-context/RealmContext";
import { upperCaseFormatter, emptyFormatter } from "../util"; import { emptyFormatter } from "../util";
import { import {
CellDropdown, CellDropdown,
ClientScope, ClientScope,
@ -44,6 +44,7 @@ import {
typeFilter, typeFilter,
} from "./details/SearchFilter"; } from "./details/SearchFilter";
import type { Row } from "../clients/scopes/ClientScopes"; import type { Row } from "../clients/scopes/ClientScopes";
import { getProtocolName } from "../clients/utils";
export default function ClientScopesSection() { export default function ClientScopesSection() {
const { realm } = useRealm(); const { realm } = useRealm();
@ -267,7 +268,8 @@ export default function ClientScopesSection() {
{ {
name: "protocol", name: "protocol",
displayKey: "client-scopes:protocol", displayKey: "client-scopes:protocol",
cellFormatters: [upperCaseFormatter()], cellRenderer: (client) =>
getProtocolName(t, client.protocol ?? "openid-connect"),
transforms: [cellWidth(15)], transforms: [cellWidth(15)],
}, },
{ {

View file

@ -458,6 +458,7 @@ export default function ClientDetails() {
title={<TabTitleText>{t("setup")}</TabTitleText>} title={<TabTitleText>{t("setup")}</TabTitleText>}
> >
<ClientScopes <ClientScopes
clientName={client.clientId!}
clientId={clientId} clientId={clientId}
protocol={client!.protocol!} protocol={client!.protocol!}
/> />

View file

@ -1,9 +1,11 @@
export default { export default {
clients: { clients: {
protocol: { protocolTypes: {
openIdConnect: "OpenID Connect", openIdConnect: "OpenID Connect",
saml: "SAML", saml: "SAML",
all: "All",
}, },
protocol: "Protocol",
clientType: "Client type", clientType: "Client type",
clientAuthorization: "Authorization", clientAuthorization: "Authorization",
implicitFlow: "Implicit flow", implicitFlow: "Implicit flow",
@ -27,7 +29,7 @@ export default {
"You haven't created any roles for this client. Create a role to get started.", "You haven't created any roles for this client. Create a role to get started.",
clientScopes: "Client scopes", clientScopes: "Client scopes",
addClientScope: "Add client scope", addClientScope: "Add client scope",
addClientScopesTo: "Add client scopes to {{clientId}}", addClientScopesTo: "Add client scopes to {{clientName}}",
clientScopeRemoveSuccess: "Scope mapping successfully removed", clientScopeRemoveSuccess: "Scope mapping successfully removed",
clientScopeRemoveError: "Could not remove the scope mapping {{error}}", clientScopeRemoveError: "Could not remove the scope mapping {{error}}",
clientScopeSuccess: "Scope mapping successfully updated", clientScopeSuccess: "Scope mapping successfully updated",

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 { import {
Button, Button,
@ -8,8 +8,17 @@ import {
Modal, Modal,
ModalVariant, ModalVariant,
DropdownDirection, DropdownDirection,
DropdownItem,
Select,
SelectOption,
SelectVariant,
SelectDirection,
} from "@patternfly/react-core"; } from "@patternfly/react-core";
import { CaretUpIcon } from "@patternfly/react-icons"; import {
CaretDownIcon,
CaretUpIcon,
FilterIcon,
} from "@patternfly/react-icons";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation"; import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
import { import {
@ -19,27 +28,65 @@ import {
import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable"; import { KeycloakDataTable } from "../../components/table-toolbar/KeycloakDataTable";
import "./client-scopes.css"; import "./client-scopes.css";
import { getProtocolName } from "../utils";
export type AddScopeDialogProps = { export type AddScopeDialogProps = {
clientScopes: ClientScopeRepresentation[]; clientScopes: ClientScopeRepresentation[];
clientName?: string;
open: boolean; open: boolean;
toggleDialog: () => void; toggleDialog: () => void;
onAdd: ( onAdd: (
scopes: { scope: ClientScopeRepresentation; type: ClientScopeType }[] scopes: { scope: ClientScopeRepresentation; type?: ClientScopeType }[]
) => void; ) => void;
isClientScopesConditionType?: boolean;
}; };
enum FilterType {
Name = "Name",
Protocol = "Protocol",
}
enum ProtocolType {
All = "All",
SAML = "SAML",
OpenIDConnect = "OpenID Connect",
}
export const AddScopeDialog = ({ export const AddScopeDialog = ({
clientScopes, clientScopes,
clientName,
open, open,
toggleDialog, toggleDialog,
onAdd, onAdd,
isClientScopesConditionType,
}: AddScopeDialogProps) => { }: AddScopeDialogProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const [addToggle, setAddToggle] = useState(false); const [addToggle, setAddToggle] = useState(false);
const [rows, setRows] = useState<ClientScopeRepresentation[]>([]); const [rows, setRows] = useState<ClientScopeRepresentation[]>([]);
const [filterType, setFilterType] = useState(FilterType.Name);
const [protocolType, setProtocolType] = useState(ProtocolType.All);
const [key, setKey] = useState(0);
const refresh = () => setKey(key + 1);
const loader = () => Promise.resolve(clientScopes); const [isFilterTypeDropdownOpen, setIsFilterTypeDropdownOpen] =
useState(false);
const [isProtocolTypeDropdownOpen, setIsProtocolTypeDropdownOpen] =
useState(false);
useEffect(() => {
refresh();
}, [filterType, protocolType]);
const loader = async () => {
if (protocolType === ProtocolType.OpenIDConnect) {
return clientScopes.filter((item) => item.protocol === "openid-connect");
} else if (protocolType === ProtocolType.SAML) {
return clientScopes.filter((item) => item.protocol === "saml");
}
return clientScopes;
};
const action = (scope: ClientScopeType) => { const action = (scope: ClientScopeType) => {
const scopes = rows.map((row) => { const scopes = rows.map((row) => {
@ -50,13 +97,87 @@ export const AddScopeDialog = ({
toggleDialog(); toggleDialog();
}; };
const onFilterTypeDropdownToggle = () => {
setIsFilterTypeDropdownOpen(!isFilterTypeDropdownOpen);
};
const onProtocolTypeDropdownToggle = () => {
setIsProtocolTypeDropdownOpen(!isProtocolTypeDropdownOpen);
};
const onFilterTypeDropdownSelect = (filterType: string) => {
if (filterType === FilterType.Name) {
setFilterType(FilterType.Protocol);
} else if (filterType === FilterType.Protocol) {
setFilterType(FilterType.Name);
}
setIsFilterTypeDropdownOpen(!isFilterTypeDropdownOpen);
};
const onProtocolTypeDropdownSelect = (protocolType: string) => {
if (protocolType === ProtocolType.SAML) {
setProtocolType(ProtocolType.SAML);
} else if (protocolType === ProtocolType.OpenIDConnect) {
setProtocolType(ProtocolType.OpenIDConnect);
} else if (protocolType === ProtocolType.All) {
setProtocolType(ProtocolType.All);
}
setIsProtocolTypeDropdownOpen(!isProtocolTypeDropdownOpen);
};
const protocolTypeOptions = [
<SelectOption key={1} value={ProtocolType.SAML}>
{t("protocolTypes.saml")}
</SelectOption>,
<SelectOption key={2} value={ProtocolType.OpenIDConnect}>
{t("protocolTypes.openIdConnect")}
</SelectOption>,
<SelectOption key={3} value={ProtocolType.All} isPlaceholder>
{t("protocolTypes.all")}
</SelectOption>,
];
return ( return (
<Modal <Modal
variant={ModalVariant.medium} variant={ModalVariant.medium}
title={t("addClientScopesTo", { clientId: "test" })} title={
isClientScopesConditionType
? t("addClientScope")
: t("addClientScopesTo", { clientName })
}
isOpen={open} isOpen={open}
onClose={toggleDialog} onClose={toggleDialog}
actions={[ actions={
isClientScopesConditionType
? [
<Button
id="modal-add"
data-testid="modalConfirm"
key="add"
variant={ButtonVariant.primary}
onClick={() => {
const scopes = rows.map((scope) => ({ scope }));
onAdd(scopes);
toggleDialog();
}}
>
{t("common:add")}
</Button>,
<Button
id="modal-cancel"
key="cancel"
variant={ButtonVariant.link}
onClick={() => {
setRows([]);
toggleDialog();
}}
>
{t("common:cancel")}
</Button>,
]
: [
<Dropdown <Dropdown
className="keycloak__client-scopes-add__add-dropdown" className="keycloak__client-scopes-add__add-dropdown"
id="add-dropdown" id="add-dropdown"
@ -87,18 +208,102 @@ export const AddScopeDialog = ({
> >
{t("common:cancel")} {t("common:cancel")}
</Button>, </Button>,
]} ]
}
> >
<KeycloakDataTable <KeycloakDataTable
loader={loader} loader={loader}
ariaLabelKey="client-scopes:chooseAMapperType" ariaLabelKey="client-scopes:chooseAMapperType"
searchPlaceholderKey="client-scopes:searchFor" searchPlaceholderKey={
filterType === FilterType.Name ? "client-scopes:searchFor" : undefined
}
searchTypeComponent={
<Dropdown
onSelect={() => {
onFilterTypeDropdownSelect(filterType);
}}
data-testid="filter-type-dropdown"
toggle={
<DropdownToggle
id="toggle-id-9"
onToggle={onFilterTypeDropdownToggle}
toggleIndicator={CaretDownIcon}
icon={<FilterIcon />}
>
{filterType}
</DropdownToggle>
}
isOpen={isFilterTypeDropdownOpen}
dropdownItems={[
<DropdownItem
data-testid="filter-type-dropdown-item"
key="filter-type"
>
{filterType === FilterType.Name
? t("protocol")
: t("common:name")}
</DropdownItem>,
]}
/>
}
key={key}
toolbarItem={
filterType === FilterType.Protocol && (
<>
<Dropdown
onSelect={() => {
onFilterTypeDropdownSelect(filterType);
}}
data-testid="filter-type-dropdown"
toggle={
<DropdownToggle
id="toggle-id-9"
onToggle={onFilterTypeDropdownToggle}
toggleIndicator={CaretDownIcon}
icon={<FilterIcon />}
>
{filterType}
</DropdownToggle>
}
isOpen={isFilterTypeDropdownOpen}
dropdownItems={[
<DropdownItem
data-testid="filter-type-dropdown-item"
key="filter-type"
>
{t("common:name")}
</DropdownItem>,
]}
/>
<Select
variant={SelectVariant.single}
className="kc-protocolType-select"
aria-label="Select Input"
onToggle={onProtocolTypeDropdownToggle}
onSelect={(_, value) =>
onProtocolTypeDropdownSelect(value.toString())
}
selections={protocolType}
isOpen={isProtocolTypeDropdownOpen}
direction={SelectDirection.down}
>
{protocolTypeOptions}
</Select>
</>
)
}
canSelectAll canSelectAll
onSelect={(rows) => setRows(rows)} onSelect={(rows) => setRows(rows)}
columns={[ columns={[
{ {
name: "name", name: "name",
}, },
{
name: "protocol",
displayKey: "clients:protocol",
cellRenderer: (client) =>
getProtocolName(t, client.protocol ?? "openid-connect"),
},
{ {
name: "description", name: "description",
}, },

View file

@ -38,6 +38,7 @@ import { ChangeTypeDropdown } from "../../client-scopes/ChangeTypeDropdown";
export type ClientScopesProps = { export type ClientScopesProps = {
clientId: string; clientId: string;
protocol: string; protocol: string;
clientName: string;
}; };
export type Row = ClientScopeRepresentation & { export type Row = ClientScopeRepresentation & {
@ -45,7 +46,11 @@ export type Row = ClientScopeRepresentation & {
description?: string; description?: string;
}; };
export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => { export const ClientScopes = ({
clientId,
protocol,
clientName,
}: ClientScopesProps) => {
const { t } = useTranslation("clients"); const { t } = useTranslation("clients");
const adminClient = useAdminClient(); const adminClient = useAdminClient();
const { addAlert, addError } = useAlerts(); const { addAlert, addError } = useAlerts();
@ -135,6 +140,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
{rest && ( {rest && (
<AddScopeDialog <AddScopeDialog
clientScopes={rest} clientScopes={rest}
clientName={clientName!}
open={addDialogOpen} open={addDialogOpen}
toggleDialog={() => setAddDialogOpen(!addDialogOpen)} toggleDialog={() => setAddDialogOpen(!addDialogOpen)}
onAdd={async (scopes) => { onAdd={async (scopes) => {
@ -146,7 +152,7 @@ export const ClientScopes = ({ clientId, protocol }: ClientScopesProps) => {
adminClient, adminClient,
clientId, clientId,
scope.scope, scope.scope,
scope.type scope.type!
) )
) )
); );

View file

@ -6,3 +6,7 @@
.keycloak__client-scopes-add__add-dropdown { .keycloak__client-scopes-add__add-dropdown {
margin-right: var(--pf-global--spacer--md); margin-right: var(--pf-global--spacer--md);
} }
.kc-protocolType-select {
max-width: 25%;
}

View file

@ -12,9 +12,9 @@ export const isRealmClient = (client: ClientRepresentation) => !client.protocol;
export const getProtocolName = (t: TFunction<"clients">, protocol: string) => { export const getProtocolName = (t: TFunction<"clients">, protocol: string) => {
switch (protocol) { switch (protocol) {
case "openid-connect": case "openid-connect":
return t("clients:protocol:openIdConnect"); return t("clients:protocolTypes:openIdConnect");
case "saml": case "saml":
return t("clients:protocol:saml"); return t("clients:protocolTypes:saml");
} }
return protocol; return protocol;

View file

@ -0,0 +1,129 @@
import React, { useState } from "react";
import { useTranslation } from "react-i18next";
import { Controller, useFormContext } from "react-hook-form";
import {
Button,
Chip,
ChipGroup,
FormGroup,
TextInput,
} from "@patternfly/react-core";
import { HelpItem } from "../help-enabler/HelpItem";
import type { ComponentProps } from "./components";
import { AddScopeDialog } from "../../clients/scopes/AddScopeDialog";
import { useAdminClient, useFetch } from "../../context/auth/AdminClient";
import type ClientScopeRepresentation from "@keycloak/keycloak-admin-client/lib/defs/clientScopeRepresentation";
import { useParams } from "react-router";
import type { EditClientPolicyConditionParams } from "../../realm-settings/routes/EditCondition";
export const MultivaluedScopesComponent = ({
defaultValue,
name,
}: ComponentProps) => {
const { t } = useTranslation("dynamic");
const { control } = useFormContext();
const { conditionName } = useParams<EditClientPolicyConditionParams>();
const adminClient = useAdminClient();
const [open, setOpen] = useState(false);
const [clientScopes, setClientScopes] = useState<ClientScopeRepresentation[]>(
[]
);
useFetch(
() => adminClient.clientScopes.find(),
(clientScopes) => {
setClientScopes(clientScopes);
},
[]
);
const toggleModal = () => {
setOpen(!open);
};
return (
<FormGroup
label={t("realm-settings:clientScopesCondition")}
id="expected-scopes"
labelIcon={
<HelpItem
helpText={t("realm-settings-help:clientScopesConditionTooltip")}
forLabel={t("clientScopes")}
forID={name!}
/>
}
fieldId={name!}
>
<Controller
name={`config.scopes`}
control={control}
defaultValue={[defaultValue]}
rules={{ required: true }}
render={({ onChange, value }) => {
return (
<>
{open && (
<AddScopeDialog
clientScopes={clientScopes.filter(
(scope) => !value.includes(scope.name!)
)}
isClientScopesConditionType
open={open}
toggleDialog={() => setOpen(!open)}
onAdd={(scopes) => {
onChange([
...value,
...scopes
.map((scope) => scope.scope)
.map((item) => item.name!),
]);
}}
/>
)}
{value.length === 0 && !conditionName && (
<TextInput
type="text"
id="kc-scopes"
value={value}
data-testid="client-scope-input"
name="config.client-scopes"
isDisabled
/>
)}
<ChipGroup
className="kc-client-scopes-chip-group"
isClosable
onClick={() => {
onChange([]);
}}
>
{value.map((currentChip: string) => (
<Chip
key={currentChip}
onClick={() => {
onChange(
value.filter((item: string) => item !== currentChip)
);
}}
>
{currentChip}
</Chip>
))}
</ChipGroup>
<Button
data-testid="select-scope-button"
variant="secondary"
onClick={() => {
toggleModal();
}}
>
{t("common:select")}
</Button>
</>
);
}}
/>
</FormGroup>
);
};

View file

@ -34,12 +34,12 @@ import {
convertToMultiline, convertToMultiline,
toValue, toValue,
} from "../components/multi-line-input/MultiLineInput"; } from "../components/multi-line-input/MultiLineInput";
import { MultivaluedRoleComponent } from "../components/dynamic/MultivaluedRoleComponent";
import { import {
COMPONENTS, COMPONENTS,
isValidComponentType, isValidComponentType,
} from "../components/dynamic/components"; } from "../components/dynamic/components";
import { MultivaluedScopesComponent } from "../components/dynamic/MultivaluedScopesComponent";
import { MultivaluedRoleComponent } from "../components/dynamic/MultivaluedRoleComponent";
export type ItemType = { value: string }; export type ItemType = { value: string };
type ConfigProperty = ConfigPropertyRepresentation & { type ConfigProperty = ConfigPropertyRepresentation & {
@ -54,6 +54,7 @@ export default function NewClientPolicyCondition() {
const [openConditionType, setOpenConditionType] = useState(false); const [openConditionType, setOpenConditionType] = useState(false);
const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>([]); const [policies, setPolicies] = useState<ClientPolicyRepresentation[]>([]);
const [condition, setCondition] = useState< const [condition, setCondition] = useState<
ClientPolicyConditionRepresentation[] ClientPolicyConditionRepresentation[]
>([]); >([]);
@ -87,9 +88,15 @@ export default function NewClientPolicyCondition() {
Object.entries(condition.configuration!).map(([key, value]) => { Object.entries(condition.configuration!).map(([key, value]) => {
const formKey = `config.${key}`; const formKey = `config.${key}`;
const property = properties.find((p) => p.name === key); const property = properties.find((p) => p.name === key);
if (property?.type === "MultivaluedString") { if (
property?.type === "MultivaluedString" &&
property.name !== "scopes"
) {
form.setValue(formKey, convertToMultiline(value)); form.setValue(formKey, convertToMultiline(value));
} else if (property?.name === "client-scopes") {
form.setValue("config.scopes", value);
} else { } else {
form.setValue(formKey, value); form.setValue(formKey, value);
} }
@ -98,8 +105,10 @@ export default function NewClientPolicyCondition() {
useFetch( useFetch(
() => adminClient.clientPolicies.listPolicies(), () => adminClient.clientPolicies.listPolicies(),
(policies) => { (policies) => {
setPolicies(policies.policies ?? []); setPolicies(policies.policies ?? []);
if (conditionName) { if (conditionName) {
const currentPolicy = policies.policies?.find( const currentPolicy = policies.policies?.find(
(item) => item.name === policyName (item) => item.name === policyName
@ -124,13 +133,14 @@ export default function NewClientPolicyCondition() {
const save = async (configPolicy: ConfigProperty) => { const save = async (configPolicy: ConfigProperty) => {
const configValues = configPolicy.config; const configValues = configPolicy.config;
const writeConfig = () => const writeConfig = () => {
conditionProperties.reduce((r: any, p) => { return conditionProperties.reduce((r: any, p) => {
p.type === "MultivaluedString" p.type === "MultivaluedString" && p.name !== "scopes"
? (r[p.name!] = toValue(configValues[p.name!])) ? (r[p.name!] = toValue(configValues[p.name!]))
: (r[p.name!] = configValues[p.name!]); : (r[p.name!] = configValues[p.name!]);
return r; return r;
}, {}); }, {});
};
const updatedPolicies = policies.map((policy) => { const updatedPolicies = policies.map((policy) => {
if (policy.name !== policyName) { if (policy.name !== policyName) {
@ -284,6 +294,17 @@ export default function NewClientPolicyCondition() {
conditionName === "client-roles") conditionName === "client-roles")
) { ) {
return <MultivaluedRoleComponent {...property} />; return <MultivaluedRoleComponent {...property} />;
} else if (
property.name === "scopes" &&
(conditionType === "client-scopes" ||
conditionName === "client-scopes")
) {
return (
<MultivaluedScopesComponent
defaultValue="offline_access"
{...property}
/>
);
} else if (isValidComponentType(componentType)) { } else if (isValidComponentType(componentType)) {
const Component = COMPONENTS[componentType]; const Component = COMPONENTS[componentType];
return <Component key={property.name} {...property} />; return <Component key={property.name} {...property} />;

View file

@ -551,7 +551,7 @@ export default function NewClientPolicyForm() {
0 ? ( 0 ? (
<Link <Link
key={condition.condition} key={condition.condition}
data-testid="condition-type-link" data-testid={`${condition.condition}-condition-link`}
to={toEditClientPolicyCondition({ to={toEditClientPolicyCondition({
realm, realm,
conditionName: condition.condition!, conditionName: condition.condition!,
@ -581,7 +581,7 @@ export default function NewClientPolicyForm() {
icon={ icon={
<TrashIcon <TrashIcon
className="kc-conditionType-trash-icon" className="kc-conditionType-trash-icon"
data-testid="deleteClientProfileDropdown" data-testid={`delete-${condition.condition}-condition`}
onClick={() => { onClick={() => {
toggleDeleteConditionDialog(); toggleDeleteConditionDialog();
setConditionToDelete({ setConditionToDelete({

View file

@ -252,3 +252,16 @@ article.pf-c-card.pf-m-flat.kc-login-settings-template
.kc_eventListeners_select { .kc_eventListeners_select {
width: 35rem; width: 35rem;
} }
input#kc-scopes {
width: 630px;
margin-right: 24px;
}
.kc-client-scopes-chip-group {
margin-right: var(--pf-global--spacer--2xl);
max-width: 585px;
min-width: 585px;
padding-left: none;
}